/* ===========================================================================
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.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 */}
{PageComp ? PageComp(tab, go) : Coming soon.
}
);
}
window.LaunchApp = LaunchApp;