// Seed the live IIOS service with a full workspace, then write a manifest that // maps the real IIOS thread ids into a channel directory the app can show. // // node scripts/seed-iios.mjs // // Requires the IIOS service running on IIOS_URL (default :3200) with dev tokens on. import { writeFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; const IIOS_URL = process.env.IIOS_URL ?? "http://localhost:3200"; const APP_ID = process.env.IIOS_APP_ID ?? "portal-demo"; const ORG_ID = process.env.IIOS_ORG_ID ?? "org_A"; const __dir = dirname(fileURLToPath(import.meta.url)); const OUT = join(__dir, "..", "apps", "web", "lib", "iios", "seed-manifest.json"); const USERS = [ { userId: "james", name: "James Carter", handle: "james", avatarText: "JC", avatarColor: "#FDA913", title: "Property Owner", presence: "active" }, { userId: "ava", name: "Ava Mitchell", handle: "ava", avatarText: "AM", avatarColor: "#5b9dff", title: "Ops Lead", presence: "active" }, { userId: "noah", name: "Noah Bennett", handle: "noah", avatarText: "NB", avatarColor: "#3ecf8e", title: "Field Supervisor", presence: "away" }, { userId: "mia", name: "Mia Torres", handle: "mia", avatarText: "MT", avatarColor: "#bb98ff", title: "Estimator", presence: "active" }, { userId: "zoe", name: "Zoe Nguyen", handle: "zoe", avatarText: "ZN", avatarColor: "#f2555a", title: "Customer Success", presence: "active" }, { userId: "sara", name: "Sara Khan", handle: "sara", avatarText: "SK", avatarColor: "#f5b544", title: "Account Manager", presence: "active" }, ]; const CHANNELS = [ { key: "general", name: "general", topic: "Company-wide announcements and work-based matters", starred: true, msgs: [ ["ava", "Morning team šŸ‘‹ Reminder: Q3 territory review is Thursday 10am CT."], ["sara", "Thanks Ava. I'll add the Acme renewal numbers."], ["james", "On it — reviewing the estimate template now. Looks solid :rocket:"], ["noah", "Crew 2 wrapped the Henderson job early. Photos uploaded."], ]}, { key: "field-ops", name: "field-ops", topic: "Crews, schedules, and on-site coordination", starred: true, msgs: [ ["noah", "Dispatch board for today is set. 6 jobs, 3 crews."], ["mia", "Uploaded 3 new estimates for review."], ["james", "Approved. Nice work team :tick:"], ]}, { key: "leads-pipeline", name: "leads-pipeline", topic: "New leads and verification queue", msgs: [ ["zoe", "12 new leads from the storm campaign overnight šŸŒ©ļø"], ["sara", "Verified 8 of them. 4 flagged for manual review."], ]}, { key: "estimates", name: "estimates", topic: "ProCanvas estimates & approvals", msgs: [ ["mia", "Estimate 4471 and 4472 are ready for sign-off."], ["ava", "Approved 4471. 4473 needs a material adjustment."], ]}, { key: "random", name: "random", topic: "Non-work banter", msgs: [ ["zoe", "Team lunch Friday? šŸ•"], ["noah", "I'm in!"], ]}, ]; const TICKETS = [ { subject: "Mobile app not syncing new leads", priority: "URGENT" }, { subject: "Estimate PDF export missing logo", priority: "NORMAL" }, { subject: "Request: SMS notifications for dispatch", priority: "LOW" }, ]; const tokens = {}; async function mint(userId) { if (tokens[userId]) return tokens[userId]; const res = await fetch(`${IIOS_URL}/v1/dev/token`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ appId: APP_ID, userId, orgId: ORG_ID }), }); if (!res.ok) throw new Error(`mint ${userId}: ${res.status} ${await res.text()}`); tokens[userId] = (await res.json()).token; return tokens[userId]; } async function ingest(author, chKey, text, idx) { const token = await mint("james"); // any valid caller; author comes from source.externalId const res = await fetch(`${IIOS_URL}/v1/interactions/ingest`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "idempotency-key": `seed-${chKey}-${idx}` }, body: JSON.stringify({ scope: { orgId: ORG_ID, appId: APP_ID }, channel: { type: "PORTAL" }, source: { handleKind: "PORTAL_USER", externalId: author, displayName: USERS.find((u) => u.userId === author)?.name }, thread: { externalThreadId: `seed-${chKey}`, subject: chKey }, parts: [{ kind: "TEXT", bodyText: text }], occurredAt: new Date(Date.parse("2026-07-11T12:00:00Z") + idx * 60000).toISOString(), providerEventId: `seed-${chKey}-${idx}`, }), }); if (!res.ok) throw new Error(`ingest ${chKey}#${idx}: ${res.status} ${await res.text()}`); return res.json(); } async function getMessages(token, threadId) { const res = await fetch(`${IIOS_URL}/v1/threads/${threadId}/messages`, { headers: { authorization: `Bearer ${token}` } }); if (!res.ok) return { messages: [] }; return res.json(); } async function main() { console.log(`Seeding IIOS at ${IIOS_URL} …`); const jamesTok = await mint("james"); const actorName = {}; // actorId -> user const channels = []; for (const ch of CHANNELS) { let threadId; for (let i = 0; i < ch.msgs.length; i++) { const [author, text] = ch.msgs[i]; const r = await ingest(author, ch.key, text, i); threadId = r.threadId; } // map actor ids → authors by aligning message order with what we posted const { messages } = await getMessages(jamesTok, threadId); messages.forEach((m, i) => { const author = ch.msgs[i]?.[0]; if (author && m.actorId) actorName[m.actorId] = author; }); channels.push({ threadId, key: ch.key, name: ch.name, topic: ch.topic, starred: !!ch.starred }); console.log(` #${ch.name} → ${threadId} (${ch.msgs.length} msgs)`); } // tickets for (const t of TICKETS) { const res = await fetch(`${IIOS_URL}/v1/support/tickets`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${jamesTok}` }, body: JSON.stringify(t), }); console.log(` ticket "${t.subject}" → ${res.status}`); } // build user list from discovered actor ids (fall back: include all defined users w/ synthetic ids) const usedActors = Object.entries(actorName); // [actorId, userId] const users = usedActors.map(([actorId, userId]) => { const u = USERS.find((x) => x.userId === userId); return { id: actorId, name: u.name, handle: u.handle, avatarText: u.avatarText, avatarColor: u.avatarColor, title: u.title, presence: u.presence }; }); const jamesActor = usedActors.find(([, uid]) => uid === "james")?.[0] ?? users[0]?.id ?? "u_me"; const allActorIds = users.map((u) => u.id); const manifest = { seeded: true, generatedAt: new Date().toISOString(), workspace: { id: "ws_lynkd", name: "LynkedUp Pro", initials: "LP", accent: "#FDA913", plan: "Business+" }, workspaces: [{ id: "ws_lynkd", name: "LynkedUp Pro", initials: "LP", accent: "#FDA913", plan: "Business+" }], me: users.find((u) => u.id === jamesActor) ?? users[0], users, sections: [{ id: "starred", label: "Starred" }, { id: "channels", label: "Channels" }, { id: "dms", label: "Direct messages" }], channels: channels.map((c) => ({ id: c.threadId, kind: "channel", name: c.name, topic: c.topic, memberIds: allActorIds, isStarred: c.starred, sectionId: c.starred ? "starred" : "channels", unreadCount: 0, })), }; writeFileSync(OUT, JSON.stringify(manifest, null, 2)); console.log(`\nāœ“ Seeded ${channels.length} channels, ${users.length} users, ${TICKETS.length} tickets.`); console.log(`āœ“ Manifest written to ${OUT}`); console.log(`\nNow set BFF_BACKEND=iios in apps/web/.env.local and restart the app.`); } main().catch((e) => { console.error("SEED FAILED:", e); process.exit(1); });