mega-plant/scripts/import-plant-sites.ts
Dave 1429c8087b feat(plant): vehicle site history from Fuel Logs
/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.
2026-11-05 00:18:24 +02:00

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;});