/* ============================================================ PULSE — Shared components, icons, demo data, API layer Globals exposed at end. ============================================================ */ const { useState, useEffect, useRef, useMemo } = React; /* ------------------------------ API layer ------------------------------ */ const api = { _token: () => localStorage.getItem("pulse:token") || "", async login(email, password) { const r = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }), }); if (!r.ok) { const err = await r.json().catch(() => ({})); throw new Error(err.detail || "Login failed"); } return r.json(); }, async get(path) { const r = await fetch(path, { headers: { Authorization: `Bearer ${this._token()}` }, }); if (r.status === 401) { localStorage.removeItem("pulse:token"); window.dispatchEvent(new Event("pulse:logout")); throw new Error("Unauthorized"); } if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }, async put(path, body) { const r = await fetch(path, { method: "PUT", headers: { Authorization: `Bearer ${this._token()}`, "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }, async post(path, body) { const r = await fetch(path, { method: "POST", headers: { Authorization: `Bearer ${this._token()}`, "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }, streamRun(runId, onEvent) { const token = this._token(); const es = new EventSource(`/api/generate/${runId}/stream?token=${encodeURIComponent(token)}`); es.onmessage = (e) => { try { const data = JSON.parse(e.data); onEvent(data); if (data.done) es.close(); } catch (_) {} }; es.onerror = () => es.close(); return es; }, }; /* ------------------------------ Timezone (global) ------------------------------ */ let _appTz = Intl.DateTimeFormat().resolvedOptions().timeZone; function appTz() { return _appTz; } function setAppTz(tz) { if (tz) _appTz = tz; } /* ------------------------------ Icons (lucide-style, stroked) ------------------------------ */ const iconProps = { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.75, strokeLinecap: "round", strokeLinejoin: "round" }; const I = { Pulse: (p) => , Home: (p) => , Play: (p) => , Archive:(p) => , Settings:(p)=> , Search:(p) => , Plus:(p) => , Check:(p) => , X:(p) => , ChevRight:(p) => , ChevDown:(p) => , ArrowRight:(p) => , ArrowUpRight:(p) => , Download:(p) => , Share:(p) => , Calendar:(p) => , Clock:(p) => , Filter:(p) => , Trend:(p) => , Alert:(p) => , Shield:(p) => , History:(p) => , File:(p) => , Moon:(p) => , Sun:(p) => , Sliders:(p) => , Dot:(p) => , Folder:(p) => , LogOut:(p) => , Zap:(p) => , }; /* ------------------------------ RAG helpers ------------------------------ */ const RAG = { red: { cls: "red", label: "RED", pillCls: "pill-red", dotCls: "dot-red" }, amber: { cls: "amber", label: "AMBER", pillCls: "pill-amber", dotCls: "dot-amber" }, green: { cls: "green", label: "GREEN", pillCls: "pill-green", dotCls: "dot-green" }, }; function RagDot({ status, withLabel }) { const r = RAG[status]; return ( {withLabel && {r.label}} ); } function RagPill({ status }) { const r = RAG[status]; return {r.label}; } /* ------------------------------ Report types ------------------------------ */ const REPORT_TYPES = [ { id: "rag", name: "RAG Status", shortName: "RAG", icon: I.Shield, tagline: "Weekly stakeholder-ready narrative with RAG across all workstreams.", desc: "Aggregates Jira data into a narrative status report with RAG indicators per workstream. Claude generates executive summary, risks and next-week priorities.", runtime: "~55s", color: "var(--text)", }, { id: "velocity", name: "Velocity Anomaly", shortName: "Velocity", icon: I.Trend, tagline: "Time-overrun and burn-rate analysis against estimates.", desc: "Detects tasks where logged time exceeds estimate by >15%. Computes per-workstream velocity and flags systemic drift.", runtime: "~22s", color: "var(--amber)", }, { id: "raid", name: "RAID Log", shortName: "RAID", icon: I.Alert, tagline: "Risks, Assumptions, Issues, Dependencies — scored & prioritised.", desc: "Classifies every open issue into R/A/I/D, scores probability × impact (P1-P3) and outputs a sortable log with owners.", runtime: "~34s", color: "var(--red)", }, { id: "pattern", name: "Historical Pattern", shortName: "Pattern", icon: I.History, tagline: "Match current sprint fingerprint to past sprint outcomes.", desc: "Compares current velocity/completion/overdue/blocker/bug ratios against historical sprints to detect failed-sprint patterns before they happen.", runtime: "~41s", color: "var(--blue)", }, ]; /* ------------------------------ Demo generated reports ------------------------------ */ function mkReport(date, overall, metrics = {}) { return { date, overall, generatedAt: `${date} 09:12`, author: "maks@pulse.app", project: "BuildLog (BL)", metrics: { totalIssues: 58, done: 16, inProgress: 20, todo: 22, overdue: 21, blockers: 3, bugs: 10, velocityPct: 23, velocitySp: "84 / 368", ...metrics, }, workstreams: [ { name: "Backend", rag: overall, velocity: 22, completion: 29, done: "6/21", overdue: 7, blockers: 1, bugs: 3, sp: "34 / 156" }, { name: "Frontend", rag: overall === "red" ? "red" : "amber", velocity: 31, completion: 30, done: "6/20", overdue: 7, blockers: 0, bugs: 4, sp: "31 / 100" }, { name: "QA", rag: overall, velocity: 22, completion: 29, done: "4/14", overdue: 4, blockers: 2, bugs: 1, sp: "19 / 88" }, ], }; } const DEMO_REPORTS = [ { id: "r-2026-04-18", type: "rag", ...mkReport("2026-04-18", "red") }, { id: "v-2026-04-18", type: "velocity", ...mkReport("2026-04-18", "red") }, { id: "d-2026-04-18", type: "raid", ...mkReport("2026-04-18", "red") }, { id: "p-2026-04-18", type: "pattern", ...mkReport("2026-04-18", "red") }, { id: "r-2026-04-11", type: "rag", ...mkReport("2026-04-11", "amber", { velocityPct: 42 }) }, { id: "v-2026-04-11", type: "velocity", ...mkReport("2026-04-11", "amber", { velocityPct: 42 }) }, { id: "d-2026-04-11", type: "raid", ...mkReport("2026-04-11", "amber") }, { id: "p-2026-04-11", type: "pattern", ...mkReport("2026-04-11", "amber") }, { id: "r-2026-04-04", type: "rag", ...mkReport("2026-04-04", "amber", { velocityPct: 58 }) }, { id: "v-2026-04-04", type: "velocity", ...mkReport("2026-04-04", "amber", { velocityPct: 58 }) }, { id: "d-2026-04-04", type: "raid", ...mkReport("2026-04-04", "amber") }, { id: "p-2026-04-04", type: "pattern", ...mkReport("2026-04-04", "amber") }, { id: "r-2026-03-28", type: "rag", ...mkReport("2026-03-28", "green", { velocityPct: 82 }) }, { id: "v-2026-03-28", type: "velocity", ...mkReport("2026-03-28", "green", { velocityPct: 82 }) }, { id: "d-2026-03-28", type: "raid", ...mkReport("2026-03-28", "green") }, { id: "p-2026-03-28", type: "pattern", ...mkReport("2026-03-28", "green") }, { id: "r-2026-03-21", type: "rag", ...mkReport("2026-03-21", "green", { velocityPct: 79 }) }, { id: "v-2026-03-21", type: "velocity", ...mkReport("2026-03-21", "green", { velocityPct: 79 }) }, ]; function reportTypeMeta(typeId) { return REPORT_TYPES.find(r => r.id === typeId); } /* ------------------------------ Common UI pieces ------------------------------ */ function Logo({ size = 22 }) { return (