Files
2026-07-17 21:48:37 +05:30

163 lines
7.6 KiB
JavaScript

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