Files
canvas-frontend/qa/seeds/003_attendance_sites.mjs
T
shad b5d1ea54b0 feat(geo): EA2-backed attendance sites + versioned seed scripts
Closes Oracle deferred caveats:
- raw-SQL persona promotion (now qa/seeds/001_personas.mjs, idempotent)
- no scripted multi-persona interaction (now qa/seeds/002_persona_
  interactions.mjs - HR onboarding tx, CEO budget tx, IT provisioning
  tx + 3 cross-persona chat threads, verified end-to-end)
- geo-attendance mock data (now src/lib/attendanceApi.ts pulls 8 sites
  via flow.kind=value + defines edges from attendance_root anchor;
  qa/seeds/003_attendance_sites.mjs seeds them idempotently)

Each site is a flow.kind=value doc with config.attendance = { lat,
lng, kind, status, who, city, label }. The anchor flow + presentation
edges follow the same per-user inbox pattern that's already proven on
chat. No hardcoded JS array left in src/scenes/GeoAttendance.tsx.
2026-06-14 14:07:57 +04:00

92 lines
4.7 KiB
JavaScript

// Versioned seed for the EA2-backed attendance roster.
//
// Creates the attendance_root anchor + 8 site flow docs (kind=value,
// source_context=CANVAS_ATTENDANCE_SITE) + presentation edges linking
// each site to the anchor.
//
// Idempotent: re-running reconciles each site by deterministic _key.
const BASE = "https://demo.flow-master.ai";
const ANCHOR = "attendance_root";
const SOURCE_CTX = "CANVAS_ATTENDANCE_SITE";
const SITES = [
{ key: "hq_london", label: "HQ · London", kind: "office", lat: 51.5074, lng: -0.1278, status: "checked_in", who: "Mariana Cole (CEO) — on-site", city: "London" },
{ key: "store_204_manchester", label: "Store 204 · Manchester", kind: "store", lat: 53.4808, lng: -2.2426, status: "checked_in", who: "Manager Priya Sahota — opened 08:02", city: "Manchester" },
{ key: "store_118_birmingham", label: "Store 118 · Birmingham", kind: "store", lat: 52.4862, lng: -1.8904, status: "late", who: "Manager Tom Reilly — late check-in 09:24", city: "Birmingham" },
{ key: "store_052_edinburgh", label: "Store 052 · Edinburgh", kind: "store", lat: 55.9533, lng: -3.1883, status: "checked_in", who: "Manager Iona MacDonald — opened 07:58", city: "Edinburgh" },
{ key: "office_berlin", label: "Office · Berlin", kind: "office", lat: 52.52, lng: 13.405, status: "checked_in", who: "Aisha Khan (HR Director) — remote-on", city: "Berlin" },
{ key: "warehouse_rotterdam", label: "Warehouse · Rotterdam", kind: "warehouse", lat: 51.9244, lng: 4.4777, status: "checked_in", who: "Shift A — 24 staff on-floor", city: "Rotterdam" },
{ key: "office_bangalore", label: "Office · Bangalore", kind: "office", lat: 12.9716, lng: 77.5946, status: "checked_in", who: "Rohan Patel (IT Director) — on-site", city: "Bangalore" },
{ key: "store_088_cardiff", label: "Store 088 · Cardiff", kind: "store", lat: 51.4816, lng: -3.1791, status: "checked_out", who: "Manager Daniel Owens — closed 21:14 yesterday", city: "Cardiff" },
];
function reqId(tag) {
return `attendance-seed-${tag}-${Math.random().toString(36).slice(2, 10)}aaaaa`;
}
async function devLogin(email) {
const r = await fetch(`${BASE}/api/v1/auth/dev-login`, {
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email }),
});
if (!r.ok) throw new Error(`dev-login: ${r.status}`);
return (await r.json()).access_token;
}
async function fetchEa2(token, method, path, body) {
const init = { method, headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" } };
if (body !== undefined) init.body = JSON.stringify(body);
const r = await fetch(`${BASE}${path}`, init);
return { ok: r.ok, status: r.status, json: r.ok ? await r.json().catch(() => null) : null, text: r.ok ? null : await r.text().catch(() => "") };
}
const token = await devLogin("hr-head@flow-master.ai");
console.log("[attendance] signed in");
const anchorHead = await fetchEa2(token, "GET", `/api/ea2/flow/${ANCHOR}`);
if (!anchorHead.ok) {
const r = await fetchEa2(token, "POST", "/api/ea2/apply-batch", {
request_id: reqId("anchor"),
ops: [{
op: "create", coll: "flow",
data: {
_key: ANCHOR, kind: "value", status: "published",
name: ANCHOR, display_name: "Attendance · root",
description: "Anchor doc for attendance sites", source_context: "CANVAS_ATTENDANCE_ROOT",
},
}],
});
if (!r.ok) throw new Error(`anchor create failed: ${r.status} ${r.text}`);
console.log("[attendance] anchor created");
} else {
console.log("[attendance] anchor exists");
}
for (const s of SITES) {
const existing = await fetchEa2(token, "GET", `/api/ea2/flow/${s.key}`);
if (existing.ok) { console.log(` ${s.key}: exists`); continue; }
const make = await fetchEa2(token, "POST", "/api/ea2/apply-batch", {
request_id: reqId(s.key),
ops: [
{
op: "create", coll: "flow",
data: {
_key: s.key, kind: "value", status: "published",
name: s.key, display_name: s.label,
description: `${s.kind} site at ${s.city}`,
source_context: SOURCE_CTX,
config: { attendance: { kind: s.kind, lat: s.lat, lng: s.lng, status: s.status, who: s.who, city: s.city, label: s.label } },
},
},
{ op: "create_edge", edge_coll: "defines", from: `flow/${ANCHOR}`, to: `flow/${s.key}`, role: "presentation" },
],
});
if (!make.ok) {
console.log(` ${s.key}: create failed → ${make.status} ${make.text.slice(0, 200)}`);
} else {
console.log(` ${s.key}: created + linked`);
}
}
console.log("\nattendance seed complete.");