/* =========================================================================== launch-app.jsx — ARGT Launch Command Center (LCC) shell + Today page. Simplified IA: 7 destinations, each a hub with internal tabs that absorb the 34 reconciled pages. Reuses argt-ds.css tokens + lib.jsx components + mock-db.jsx fold seam. Hubs register into window.LCC_PAGES from sibling files. =========================================================================== */ /* --------------------------------------------------------------------------- IA — the seven destinations. Each accent paints --role-accent for its hub, giving the per-section rhythm without inventing colors. --------------------------------------------------------------------------- */ const LCC_ACCENTS = { magenta: { hex: "#B5006A", bright: "#D8288C" }, teal: { hex: "#007A8A", bright: "#10B5C7" }, purple: { hex: "#4C1285", bright: "#7A4FBF" }, amber: { hex: "#C49800", bright: "#E0B638" }, }; const LCC_NAV = [ { id: "today", label: "Today", icon: "home", accent: "magenta", desc: "This week" }, { id: "pipelines", label: "Pipelines", icon: "users", accent: "teal", desc: "People & partners" }, { id: "marketing", label: "Marketing", icon: "megaphone", accent: "purple", desc: "Comms & content" }, { id: "finance", label: "Finance", icon: "wallet", accent: "amber", desc: "Budget & cashflow" }, { id: "onsite", label: "On-Site", icon: "shield", accent: "magenta", desc: "Festival weekend" }, { id: "workspace", label: "Workspace", icon: "list", accent: "teal", desc: "Team & files" }, { id: "settings", label: "Settings", icon: "gear", accent: "purple", desc: "Configuration" }, ]; const LCC_TABS = { today: [], pipelines: [ { id: "board", label: "Stage board" }, { id: "talent", label: "Talent advance" }, { id: "analytics", label: "Analytics" } ], marketing: [ { id: "comms", label: "Comms calendar" }, { id: "studio", label: "Content studio" }, { id: "planners", label: "Planners" }, { id: "public", label: "Public surfaces" } ], finance: [ { id: "overview", label: "Overview" }, { id: "receivables", label: "Money in" }, { id: "payables", label: "Money out" } ], onsite: [ { id: "command", label: "Day-of" }, { id: "crew", label: "Crew call" }, { id: "map", label: "Venue map" }, { id: "incidents", label: "Incidents" }, { id: "weather", label: "Weather" }, { id: "phases", label: "Ticket phases" }, { id: "recap", label: "Recap" } ], workspace: [ { id: "team", label: "Team" }, { id: "checklists", label: "Checklists" }, { id: "files", label: "Files" }, { id: "timeline", label: "Timeline" } ], settings: [ { id: "festival", label: "Festival" }, { id: "timeline", label: "Timeline & phases" }, { id: "engines", label: "Engines" }, { id: "roles", label: "Team & roles" }, { id: "integrations", label: "Integrations" }, { id: "alerts", label: "Alerts" }, { id: "branding", label: "Branding" }, { id: "data", label: "Data" } ], }; /* --------------------------------------------------------------------------- Shared helpers — exported to window so hub files reuse them. --------------------------------------------------------------------------- */ const ENGINE_COLORS = { social: "#D8288C", digital: "#7A4FBF", sponsors: "#E0B638", tickets: "#10B5C7", content: "#F25BA7", "day-of": "#00A8BC" }; const ENGINE_LABELS = { social: "Social", digital: "Digital", sponsors: "Sponsors", tickets: "Tickets", content: "Content", "day-of": "Day-Of" }; const money = (cents) => "$" + Math.round(cents / 100).toLocaleString(); const moneyK = (cents) => { const neg = cents < 0; const d = Math.abs(cents) / 100; const s = d >= 1000 ? "$" + (d / 1000).toFixed(d >= 10000 ? 0 : 1) + "k" : "$" + Math.round(d); return (neg ? "-" : "") + s; }; const FEST = new Date("2026-09-18T18:00:00-04:00"); const daysToGates = () => Math.max(0, Math.ceil((FEST.getTime() - Date.now()) / 86400000)); const weeksToGates = () => Math.max(0, Math.ceil((FEST.getTime() - Date.now()) / (86400000 * 7))); const Avatar = ({ name, color, size = 26 }) => ( {(name || "?").split(" ").map((w) => w[0]).slice(0, 2).join("")} ); const Bar = ({ pct, color }) => (
); const EngineChip = ({ ek }) => ( {ENGINE_LABELS[ek] || ek} ); /* Section card with a display heading + optional right action. */ const Section = ({ title, action, children, pad = true, style }) => (
{title && (

{title}

{action}
)} {children}
); /* Page scaffold: eyebrow + title + lede + actions, then body. */ const LccPage = ({ eyebrow, title, lede, actions, kpis, children }) => (
{eyebrow && {eyebrow}}

{title}

{lede &&

{lede}

}
{actions &&
{actions}
}
{kpis &&
{kpis.map((k, i) => )}
} {children}
); const LccTabs = ({ tabs, active, onChange }) => (
{tabs.map((t) => ( ))}
); const act = (msg) => () => { if (window.argtPing) window.argtPing(msg); }; /* Small square action button (edit/delete/etc) — stops row-click propagation. */ const SmallBtn = ({ title, onClick, children, danger, disabled }) => ( ); const lccInput = () => ({ background: "var(--pvt-surface-mid)", border: "1px solid var(--pvt-border-light)", borderRadius: "var(--r-md)", color: "var(--pvt-text)", padding: "11px 13px", fontSize: 13, fontFamily: "var(--pvt-font-body)", outline: "none", width: "100%" }); const LccField = ({ label, children }) => ( ); function LccModal({ open, title, onClose, onSubmit, submitLabel = "Save", children }) { React.useEffect(() => { if (!open) return; const onKey = (e) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [open]); if (!open) return null; return (
e.stopPropagation()} style={{ width: "min(520px, 96vw)", background: "var(--pvt-surface-raised)", border: "1px solid var(--pvt-border-light)", borderRadius: "var(--r-lg)", boxShadow: "0 32px 80px rgba(0,0,0,0.6)", overflow: "hidden" }}>

{title}

{children}
); } Object.assign(window, { LCC_ACCENTS, LCC_NAV, LCC_TABS, ENGINE_COLORS, ENGINE_LABELS, money, moneyK, FEST, daysToGates, weeksToGates, Avatar, Bar, EngineChip, Section, LccPage, LccTabs, act, lccInput, LccField, LccModal, SmallBtn }); /* =========================================================================== TODAY — the daily-driver. Absorbs This Week + Inbox + Engines + Digest. =========================================================================== */ function StatusBadge({ status }) { const map = { blocked: ["rejected", "Blocked"], doing: ["review", "In progress"], todo: ["draft", "To do"], done: ["approved", "Done"] }; const [k, l] = map[status] || ["draft", status]; return {l}; } function TaskRow({ task, onCycle, onEdit, onDelete }) { const team = window.ARGT_DB.get("launchTeam") || []; const owner = team.find((m) => m.name.split(" ")[0] === task.owner) || { name: task.owner }; return (
onEdit(task)} title="Click to edit · tap the status to advance it">
{task.title}
{task.kpi}
{task.risk === "high" && HIGH RISK} {task.due} onDelete(task.id)}>×
); } /* Derive an engine's LIVE state from the task list — progress, status, and counts are computed, not seeded, so the board never drifts from reality. progress = weighted completion (done = 1, in-progress = 0.5) over the engine's tasks; status escalates objectively: paused if disabled, attention if any task is blocked, else healthy. */ function engineLive(engine, tasks) { const list = (tasks || []).filter((t) => t.engine === engine.key); const total = list.length; const done = list.filter((t) => t.status === "done").length; const doing = list.filter((t) => t.status === "doing").length; const blocked = list.filter((t) => t.status === "blocked").length; const openCount = list.filter((t) => t.status !== "done").length; const pct = total ? Math.round((done + doing * 0.5) / total * 100) : 0; const status = engine.enabled === false ? "paused" : (blocked > 0 ? "attention" : "healthy"); return { list, total, done, doing, blocked, openCount, pct, status }; } function EngineDrawer({ engine, tasks, onClose, onCycle, onEdit, onDelete, onAdd }) { React.useEffect(() => { const onKey = (e) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, []); if (!engine) return null; const { list, openCount, blocked, done, total, pct, status } = engineLive(engine, tasks); const tone = status === "healthy" ? "#10B5C7" : status === "attention" ? "#E0B638" : "var(--pvt-text-dim)"; const color = ENGINE_COLORS[engine.key] || "var(--role-accent)"; return (
e.stopPropagation()} style={{ width: "min(460px, 96vw)", height: "100%", background: "var(--pvt-surface-raised)", borderLeft: "1px solid var(--pvt-border-light)", boxShadow: "-32px 0 80px rgba(0,0,0,0.5)", display: "flex", flexDirection: "column", animation: "lccDrawerIn 220ms ease" }}>
{engine.label} engine
{status} · {engine.owner} {engine.target && (<>·{engine.target})}
Task completion {pct}% · {done}/{total} done

{engine.note}

{[["Open", openCount], ["Blocked", blocked], ["Done", done]].map(([l, n]) => (
0 ? "#F25BA7" : "var(--pvt-text)" }}>{n}
{l}
))}
Tasks
{list.map((t) => )} {list.length === 0 &&
No tasks on this engine yet. Add the first one above.
}
); } function TodayPage({ go }) { const [tasks, setTasks] = window.useDB("launchTasks"); const [engines] = window.useDB("launchEngines"); const [inbox, setInbox] = window.useDB("launchInbox"); const [phases] = window.useDB("ticketPhases"); const [cats] = window.useDB("budgetCategories"); const [showAdd, setShowAdd] = React.useState(false); const [mine, setMine] = React.useState(false); const [drawerEngine, setDrawerEngine] = React.useState(null); const [blockersOnly, setBlockersOnly] = React.useState(false); const [editTask, setEditTask] = React.useState(null); const [draft, setDraft] = React.useState({ title: "", engine: "social", due: "" }); const team = window.ARGT_DB.get("launchTeam") || []; const order = { blocked: 0, doing: 1, todo: 2, done: 3 }; const sorted = [...tasks].sort((a, b) => order[a.status] - order[b.status]); const blockers = tasks.filter((t) => t.status === "blocked").length; const founders = phases.find((p) => p.phase === "Founders") || { sold: 0 }; const revIn = cats.filter((c) => c.kind === "revenue").reduce((s, c) => s + c.currentCents, 0); const revTgt = cats.filter((c) => c.kind === "revenue").reduce((s, c) => s + c.targetCents, 0); const cycle = (id) => setTasks((ts) => ts.map((t) => t.id === id ? { ...t, status: ({ blocked: "doing", doing: "done", done: "todo", todo: "blocked" })[t.status] } : t)); const delTask = (id) => setTasks((ts) => ts.filter((t) => t.id !== id)); const saveTask = () => { setTasks((ts) => ts.map((t) => t.id === editTask.id ? { ...editTask } : t)); if (window.argtPing) window.argtPing("Task updated"); setEditTask(null); }; const markRead = (id) => setInbox((xs) => xs.map((x) => x.id === id ? { ...x, readAt: "seen" } : x)); const addTask = () => { if (!draft.title.trim()) return; setTasks((ts) => [{ id: "t" + Date.now(), title: draft.title.trim(), engine: draft.engine, owner: "Kam", tMinusWeek: weeksToGates(), due: draft.due || "TBD", status: "todo", kpi: "New", risk: "low" }, ...ts]); if (window.argtPing) window.argtPing("Task added"); setShowAdd(false); setDraft({ title: "", engine: "social", due: "" }); }; const openAddForEngine = (key) => { setDraft({ title: "", engine: key, due: "" }); setShowAdd(true); }; let shown = mine ? sorted.filter((t) => t.owner === "Kam") : sorted; if (blockersOnly) shown = shown.filter((t) => t.status === "blocked"); const kpis = [ { label: "Days to gates", num: String(daysToGates()), delta: "Sep 18-20, 2026", glyph: "calendar", onClick: () => go && go("onsite", "command") }, { label: "Open blockers", num: String(blockers), delta: blockersOnly ? "Showing blockers · clear" : (blockers ? "Show blockers" : "All clear"), deltaDir: blockers ? "down" : "up", glyph: "shield", onClick: () => setBlockersOnly((b) => !b) }, { label: "Founders sold", num: founders.sold + "%", delta: "Open ticket phases", deltaDir: "up", glyph: "ticket", onClick: () => go && go("onsite", "phases") }, { label: "Revenue in", num: moneyK(revIn), delta: "of " + moneyK(revTgt) + " target", deltaDir: "up", glyph: "wallet", onClick: () => go && go("finance", "overview") }, ]; return ( } kpis={kpis} >
{/* main */}
setBlockersOnly(false)}>Clear filter : {tasks.filter((t) => t.status !== "done").length} OPEN}>
{shown.map((t) => )}{shown.length === 0 &&
No tasks match this filter.
}
{engines.filter((e) => engineLive(e, tasks).status === "healthy").length + "/" + engines.length + " HEALTHY"}}>
{engines.map((e) => { const L = engineLive(e, tasks); const tone = L.status === "healthy" ? "#10B5C7" : L.status === "attention" ? "#E0B638" : "var(--pvt-text-dim)"; return (
setDrawerEngine(e.key)} title={"Open the " + e.label + " engine"} style={{ background: "var(--pvt-surface-mid)", borderRadius: "var(--r-md)", padding: 14, display: "flex", flexDirection: "column", gap: 10, border: "1px solid var(--pvt-border-light)", cursor: "pointer", transition: "border-color 140ms, transform 140ms" }} onMouseEnter={(ev) => { ev.currentTarget.style.borderColor = "var(--role-accent)"; ev.currentTarget.style.transform = "translateY(-2px)"; }} onMouseLeave={(ev) => { ev.currentTarget.style.borderColor = "var(--pvt-border-light)"; ev.currentTarget.style.transform = "none"; }}>
{e.label} {L.status}
{L.openCount} open{e.owner}

{e.note}

); })}
{/* right rail */}
COUNTDOWN
{daysToGates()}days
to gates · Harbour Pointe, Fort Pierce
{inbox.filter((i) => !i.readAt).length} NEW}>
{inbox.map((n) => (
markRead(n.id)} style={{ display: "flex", gap: 10, padding: "10px 6px", cursor: "pointer", borderRadius: 10, background: n.readAt ? "transparent" : "var(--pvt-surface-mid)" }}>
{n.summary}
{n.kind} · {n.at}
))}

{blockers} blocker{blockers === 1 ? "" : "s"} this week, {tasks.filter((t) => t.status === "done").length} task{tasks.filter((t) => t.status === "done").length === 1 ? "" : "s"} closed. Founders phase at {founders.sold}%. {(() => { const atRisk = engines.filter((e) => engineLive(e, tasks).status === "attention").map((e) => e.label); if (!atRisk.length) return "No engines are blocked right now."; const names = atRisk.length > 2 ? atRisk.slice(0, 2).join(", ") + " and " + (atRisk.length - 2) + " more" : atRisk.join(" and "); return names + (atRisk.length === 1 ? " needs" : " need") + " the most attention."; })()}

setShowAdd(false)} onSubmit={addTask} submitLabel="Add task"> setDraft({ ...draft, title: e.target.value })} />
setDraft({ ...draft, due: e.target.value })} />
setEditTask(null)} onSubmit={saveTask} submitLabel="Save"> {editTask && (<> setEditTask({ ...editTask, title: e.target.value })} />
setEditTask({ ...editTask, due: e.target.value })} />
{window.TaskSyncControl ? : null} )}
e.key === drawerEngine) : null} tasks={tasks} onClose={() => setDrawerEngine(null)} onCycle={cycle} onEdit={setEditTask} onDelete={delTask} onAdd={openAddForEngine} />
); } window.LCC_PAGES = Object.assign(window.LCC_PAGES || {}, { today: (tab, go) => }); /* =========================================================================== COMMAND PALETTE (⌘K) — jump to any destination or tab. The simplifier: you never have to hunt the sidebar. =========================================================================== */ function CommandPalette({ open, onClose, onGo, allow: props_allow }) { const [q, setQ] = React.useState(""); const inputRef = React.useRef(null); React.useEffect(() => { if (open) { setQ(""); setTimeout(() => inputRef.current && inputRef.current.focus(), 30); } }, [open]); const allow = props_allow || LCC_NAV.map((n) => n.id); const items = []; LCC_NAV.filter((n) => allow.includes(n.id)).forEach((n) => { items.push({ page: n.id, tab: null, label: n.label, hint: n.desc, icon: n.icon }); (LCC_TABS[n.id] || []).forEach((t) => items.push({ page: n.id, tab: t.id, label: n.label + " · " + t.label, hint: "Tab", icon: n.icon })); }); const filtered = q ? items.filter((i) => i.label.toLowerCase().includes(q.toLowerCase())) : items; React.useEffect(() => { if (!open) return; const onKey = (e) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [open]); if (!open) return null; return (
e.stopPropagation()} style={{ width: "min(620px, 92vw)", background: "var(--pvt-surface-raised)", border: "1px solid var(--pvt-border-light)", borderRadius: "var(--r-lg)", boxShadow: "0 32px 80px rgba(0,0,0,0.6)", overflow: "hidden" }}>
setQ(e.target.value)} placeholder="Jump to anything in the command center…" style={{ flex: 1, background: "transparent", border: 0, outline: 0, color: "var(--pvt-text)", fontSize: 15, fontFamily: "var(--pvt-font-body)" }} /> ESC
{filtered.length === 0 &&
Nothing matches “{q}”.
} {filtered.map((it, i) => (
{ onGo(it.page, it.tab); onClose(); }} style={{ display: "flex", alignItems: "center", gap: 12, padding: "10px 12px", borderRadius: "var(--r-md)", cursor: "pointer" }} onMouseEnter={(e) => e.currentTarget.style.background = "var(--pvt-surface-mid)"} onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}> {it.label} {it.hint}
))}
); } /* =========================================================================== SHELL — labeled sidebar + topbar + hub/tab routing. =========================================================================== */ function LaunchApp() { const initial = (() => { const h = (location.hash || "").replace(/^#/, "").split("/"); const page = LCC_NAV.find((n) => n.id === h[0]) ? h[0] : "today"; return { page, tab: h[1] || (LCC_TABS[page][0] && LCC_TABS[page][0].id) || null }; })(); const [page, setPage] = React.useState(initial.page); const [tab, setTab] = React.useState(initial.tab); const [palette, setPalette] = React.useState(false); const [collapsed, setCollapsed] = React.useState(false); const [access] = window.useDB("launchAccess"); const [inbox, setInbox] = window.useDB("launchInbox"); const [menu, setMenu] = React.useState(null); // null | "role" | "notif" | "acct" const allow = (access.access && access.access[access.currentRole]) || LCC_NAV.map((n) => n.id); const setRole = (r) => { window.ARGT_DB.set("launchAccess", (a) => ({ ...a, currentRole: r })); setMenu(null); }; const unread = inbox.filter((n) => !n.readAt).length; const markRead = (id) => setInbox((xs) => xs.map((x) => x.id === id ? { ...x, readAt: "seen" } : x)); const markAllRead = () => setInbox((xs) => xs.map((x) => ({ ...x, readAt: x.readAt || "seen" }))); const signOut = () => { try { localStorage.removeItem("argt-mock-user"); } catch (e) {} window.location.href = "../login.html"; }; const go = (p, t) => { const nt = t || (LCC_TABS[p][0] && LCC_TABS[p][0].id) || null; setPage(p); setTab(nt); location.hash = p + (nt ? "/" + nt : ""); window.scrollTo({ top: 0 }); }; React.useEffect(() => { const onKey = (e) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { e.preventDefault(); setPalette((v) => !v); } }; const onPal = () => setPalette(true); document.addEventListener("keydown", onKey); window.addEventListener("argt:command-palette", onPal); return () => { document.removeEventListener("keydown", onKey); window.removeEventListener("argt:command-palette", onPal); }; }, []); // bounce to Today if the current role can't open this destination React.useEffect(() => { if (!allow.includes(page)) go("today"); }, [access.currentRole]); // close any open top-bar menu on outside click React.useEffect(() => { if (!menu) return; const onDoc = (e) => { if (!e.target.closest(".lcc-menu")) setMenu(null); }; document.addEventListener("mousedown", onDoc); return () => document.removeEventListener("mousedown", onDoc); }, [menu]); const nav = LCC_NAV.find((n) => n.id === page) || LCC_NAV[0]; const accent = LCC_ACCENTS[nav.accent]; const tabs = LCC_TABS[page] || []; const PageComp = (window.LCC_PAGES || {})[page]; return (
setPalette(false)} onGo={go} allow={allow} /> {/* SIDEBAR */} {/* MAIN */}
Launch/{nav.label}
{/* Role switcher (demo) */}
{menu === "role" && (
Viewing as
{access.roles.map((r) => ( ))}
)}
{allow.includes("settings") && } {/* Notifications */}
{menu === "notif" && (
Notifications{unread > 0 && {unread} new} {unread > 0 && }
{inbox.map((n) => ( ))}
)}
{/* Account */}
{menu === "acct" && (
KS Kam Sandberg{access.currentRole} · ARGT 2026
{allow.includes("settings") && } Admin console Exit to main site
)}
{PageComp ? PageComp(tab, go) :
Coming soon.
}
); } window.LaunchApp = LaunchApp;