WFDASH-WEBAPP/scripts/migrations/2026-09-28-create-place-overrides.ts

114 lines
4 KiB
TypeScript
Raw Normal View History

/**
* One-shot migration: create the `place_overrides` collection.
*
* Run with:
*
* npx tsx scripts/migrations/2026-09-28-create-place-overrides.ts
*
* The operator's decisions about the places a vehicle tracker reports:
* which company a location belongs to, what to call it, and — because
* assigning the same company to two nearby locations merges their hours
* automatically — how to merge them.
*
* Rows are keyed by COORDINATES, not by the report's internal cluster id.
* That matters: clusters are recomputed on every read and their centroid
* drifts, so a stored cluster key would go stale. Matching an override to a
* cluster by proximity instead means a decision keeps working even after
* the underlying GPS jitters.
*
* Read side: `src/lib/server/rep-time-log-service.ts` applies overrides
* before labelling. Written by
* `POST /api/v1/reports/rep-performance/[repId]/time-log/places`.
*
* Idempotent — safe to re-run; exits early if the collection exists.
*/
import "./_env-loader";
import PocketBase from "pocketbase";
const PB_URL = process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://127.0.0.1:8090";
const ADMIN_EMAIL = process.env.PB_ADMIN_EMAIL;
const ADMIN_PASSWORD = process.env.PB_ADMIN_PASSWORD;
const text = (name: string, max: number, required = false) => ({
name,
type: "text",
required,
max,
});
async function main() {
if (!ADMIN_EMAIL || !ADMIN_PASSWORD) {
throw new Error(
"Set PB_ADMIN_EMAIL and PB_ADMIN_PASSWORD env vars (e.g. sourced from .env.local).",
);
}
const pb = new PocketBase(PB_URL);
await pb.collection("_superusers").authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD);
console.log(`[place-overrides] Authenticated against ${PB_URL}`);
const existing = await pb.collections
.getFullList()
.then((all) => all.find((c) => c.name === "place_overrides"));
if (existing) {
console.log("[place-overrides] place_overrides already exists — skipping.");
return;
}
await pb.collections.create({
name: "place_overrides",
type: "base",
// The operator is authenticated; reads happen through the admin client
// in the service, but the rule keeps the collection usable from the app.
listRule: "@request.auth.id != ''",
viewRule: "@request.auth.id != ''",
createRule: null,
updateRule: null,
deleteRule: null,
fields: [
// The place's anchor point. An override is matched to a tracker cluster
// within ~300 m, so small centroid drift is absorbed.
{ name: "lat", type: "number", required: true },
{ name: "lon", type: "number", required: true },
// Optional custom name, e.g. "Blyvoor main gate". Takes effect when no
// company is set, or as a display suffix when one is.
text("label", 200),
// The company this location belongs to. Setting the same company on two
// locations is how a MERGE is expressed: the report then rolls both up
// under one company key and their hours add together.
{
name: "company",
type: "relation",
required: false,
collectionId: "pbc_3866053794",
cascadeDelete: false,
minSelect: 0,
maxSelect: 1,
},
// The tracker's own address for this spot, kept purely as a hint in the
// admin UI so an operator can tell overrides apart.
text("source_address", 300),
// Who made the change, for the audit trail.
text("actor_id", 32),
text("actor_name", 200),
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
],
indexes: [
// Proximity lookups happen in memory after a full read, so an index is
// only needed to keep a future location filter cheap.
"CREATE INDEX idx_place_overrides_company ON place_overrides (company)",
],
});
console.log(
"[place-overrides] Created place_overrides collection. Migration complete.",
);
}
main().catch((err) => {
console.error("[place-overrides] Migration failed:", err);
process.exit(1);
});