/plant and /plant/map import Fuel Logs CSVs by exact QR and show each vehicle's current and previous distinct site with the dates they were observed. - plant_site_observations is durable, QR-keyed history. It is never wiped and a previous site is never manufactured. - An older or repeated snapshot must not move a vehicle backwards; untimed conflicting sites on one day are held for review. - A manual site change shares the same history and the same lock. - Current/previous metadata and the sticker QR are captured BEFORE the asset rows are deleted in the seed, so a re-seed cannot wipe them. Explicit empty values are preserved too: clearing a site is a deliberate manual observation, not permission to restore an old sheet value. - Imports change location metadata only. They never touch income, cost or fuel measurements. - A new named site is created only for a matched Plant vehicle. Missing coordinates mean "awaiting a pin", never a headquarters fallback. Owner-confirmed 2026-11-04: TLB06's QR is 1089, not the old meter reading 4704. lib/plant-site-policy.json holds the confirmed aliases (Equestria Bastion Job -> Equestria 215, Cosmo City / Lion Park -> Cosmo City); no other identity is guessed.
25 lines
2.7 KiB
JavaScript
25 lines
2.7 KiB
JavaScript
// Additive only. Observations use stable QRs rather than disposable assets relations.
|
|
import fs from 'node:fs';
|
|
import PocketBase from 'pocketbase';
|
|
try { process.loadEnvFile(new URL('../.env.local',import.meta.url)); } catch (error) { if(error.code!=='ENOENT')throw error; }
|
|
const pb=new PocketBase(process.env.NEXT_PUBLIC_PB_URL);pb.autoCancellation(false);
|
|
await pb.collection('_superusers').authWithPassword(process.env.PB_ADMIN,process.env.PB_PASS);
|
|
const [assets,sites]=await Promise.all([pb.collections.getOne('assets'),pb.collections.getOne('sites')]);
|
|
const out=process.argv.indexOf('--backup');
|
|
if(out>=0){if(!process.argv[out+1])throw Error('Supply a backup path.');fs.writeFileSync(process.argv[out+1],JSON.stringify({at:new Date().toISOString(),assetsSchema:assets,sitesSchema:sites,assets:await pb.collection('assets').getFullList(),sites:await pb.collection('sites').getFullList()},null,2),{flag:'wx'});}
|
|
const desired=[
|
|
...['site_label','previous_site_label','site_observed_on','previous_site_observed_on','site_source'].map(name=>({name,type:'text'})),
|
|
{name:'previous_site',type:'relation',collectionId:sites.id,maxSelect:1,cascadeDelete:false},
|
|
];
|
|
for(const field of desired){const prior=assets.fields.find(f=>f.name===field.name);if(prior&&(prior.type!==field.type||field.collectionId&&prior.collectionId!==field.collectionId))throw Error('Incompatible existing field '+field.name);}
|
|
const additions=desired.filter(f=>!assets.fields.some(a=>a.name===f.name));
|
|
if(additions.length)await pb.collections.update(assets.id,{fields:[...assets.fields,...additions]});
|
|
async function collection(name,fields,indexes=[]){let found;try{found=await pb.collections.getOne(name);}catch(error){if(error.status!==404)throw error;}
|
|
if(!found)await pb.collections.create({name,type:'base',listRule:null,viewRule:null,createRule:null,updateRule:null,deleteRule:null,fields,indexes});
|
|
else {const add=fields.filter(f=>!found.fields.some(a=>a.name===f.name));const ix=indexes.filter(x=>!found.indexes.includes(x));if(add.length||ix.length)await pb.collections.update(found.id,{fields:[...found.fields,...add],indexes:[...found.indexes,...ix]});}}
|
|
await collection('plant_site_observations',[
|
|
...['vehicle_qr','asset_code','site_name','observed_on','source','source_file','source_hash'].map(name=>({name,type:'text'})),
|
|
{name:'created',type:'autodate',onCreate:true,onUpdate:false},{name:'updated',type:'autodate',onCreate:true,onUpdate:true},
|
|
],['CREATE INDEX idx_plant_site_qr ON plant_site_observations (vehicle_qr)']);
|
|
await collection('plant_site_sync_locks',['owner','started_at'].map(name=>({name,type:'text'})));
|
|
console.log(JSON.stringify({ok:true,assetFieldsAdded:additions.map(f=>f.name),collections:['plant_site_observations','plant_site_sync_locks']}));
|