163 lines
7.6 KiB
JavaScript
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); });
|