/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.
26 lines
2.3 KiB
TypeScript
26 lines
2.3 KiB
TypeScript
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import PocketBase from 'pocketbase';
|
|
import { previewPlantSites, applyPlantSites } from '../lib/plant-site-import';
|
|
|
|
async function main(){
|
|
const args=process.argv.slice(2);if(args.includes('--help')){console.log('node --import tsx scripts/import-plant-sites.ts --file SOURCE.csv --preview|--apply [--expected-hash SHA256] --result NEW.json');return;}
|
|
const options:Record<string,string|boolean>={};
|
|
for(let i=0;i<args.length;i++){const k=args[i];if(k==='--preview'||k==='--apply')options[k]=true;else if(['--file','--result','--expected-hash'].includes(k)&&args[i+1])options[k]=args[++i];else throw Error('Unknown option or missing value: '+k);}
|
|
if(!options['--file']||!options['--result']||Boolean(options['--preview'])===Boolean(options['--apply']))throw Error('Supply --file, --result, and exactly one of --preview / --apply.');
|
|
if(options['--apply']&&!/^[a-f0-9]{64}$/.test(String(options['--expected-hash']||'')))throw Error('Apply requires the preview --expected-hash.');
|
|
try{process.loadEnvFile(new URL('../.env.local',import.meta.url));}catch(error){if((error as NodeJS.ErrnoException).code!=='ENOENT')throw error;}
|
|
const target=path.resolve(String(options['--result']));
|
|
await fs.mkdir(path.dirname(target),{recursive:true});
|
|
const handle=await fs.open(target,'wx');
|
|
try{
|
|
const source=path.resolve(String(options['--file']));const bytes=await fs.readFile(source);if(bytes.length>5*1024*1024)throw Error('CSV exceeds 5 MB.');
|
|
const pb=new PocketBase(process.env.NEXT_PUBLIC_PB_URL);pb.autoCancellation(false);
|
|
if(!process.env.PB_ADMIN||!process.env.PB_PASS)throw Error('Plant PocketBase credentials are not configured.');
|
|
await pb.collection('_superusers').authWithPassword(process.env.PB_ADMIN,process.env.PB_PASS);
|
|
const result=options['--apply']?await applyPlantSites(pb,bytes.toString('utf8'),path.basename(source),String(options['--expected-hash'])):await previewPlantSites(pb,bytes.toString('utf8'),path.basename(source));
|
|
await handle.writeFile(JSON.stringify(result,null,2)+'\n');
|
|
console.log(JSON.stringify({result:target,...result}));
|
|
}catch(error){await handle.writeFile(JSON.stringify({ok:false,error:(error as Error).message})+'\n');throw error;}finally{await handle.close();}
|
|
}
|
|
main().catch(error=>{console.error(JSON.stringify({ok:false,error:error.message}));process.exitCode=1;});
|