/* Login + Dashboard + Generate screens */ /* ------------------------------ LOGIN ------------------------------ */ function LoginScreen({ onLogin }) { const [email, setEmail] = useState(""); const [pwd, setPwd] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const submit = async (e) => { e.preventDefault(); setLoading(true); setError(""); try { const data = await api.login(email, pwd); onLogin(data.token); } catch (err) { setError(err.message || "Login failed"); setLoading(false); } }; return (
{/* Left — form */}
Pulse

Sign in

Welcome back — continue to your project reports.

setEmail(e.target.value)} placeholder="you@company.com" />
Forgot?
setPwd(e.target.value)} />
{error && (
{error}
)}
or
Don't have an account? Request access
Pulse · secure session · SOC2 Type II
{/* Right — hero panel */}
Weekly report — ready in 55 seconds
Stakeholder-ready project reports,
without the spreadsheet gymnastics.
Pulse reads your Jira, computes RAG, detects velocity anomalies, scores your RAID log, and matches current sprint fingerprints against history — all in one run.
{/* Mini preview card */}
BuildLog — Week of Apr 18
report_2026-04-18.html
{[ { label: "Velocity", v: "23%", rag: "red" }, { label: "Overdue", v: "21", rag: "red" }, { label: "Blockers", v: "3", rag: "amber" }, ].map(m => (
{m.label}
{m.v}
))}
Workstreams
{[ { n: "Backend", v: 22, rag: "red" }, { n: "Frontend", v: 31, rag: "red" }, { n: "QA", v: 22, rag: "red" }, ].map(w => (
{w.n}
{w.v}%
))}
); } /* ------------------------------ DASHBOARD helpers ------------------------------ */ function getIsoWeek(d = new Date()) { const date = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())); const dayNum = date.getUTCDay() || 7; date.setUTCDate(date.getUTCDate() + 4 - dayNum); const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1)); return Math.ceil((((date - yearStart) / 86400000) + 1) / 7); } function timeAgo(isoStr) { if (!isoStr) return null; const diffMs = Date.now() - new Date(isoStr).getTime(); const mins = Math.round(diffMs / 60000); if (mins < 1) return "just now"; if (mins < 60) return `${mins}m ago`; const hrs = Math.round(mins / 60); if (hrs < 24) return `${hrs}h ago`; return `${Math.round(hrs / 24)}d ago`; } /* ------------------------------ DASHBOARD ------------------------------ */ function Dashboard({ onNav }) { const [dashData, setDashData] = useState(null); useEffect(() => { api.get("/api/dashboard").then(setDashData).catch(() => {}); }, []); // Fall back to demo if API has no data yet const latest = dashData?.latest?.length ? dashData.latest : DEMO_REPORTS.filter(r => r.date === "2026-04-18"); const velSpark = dashData?.velocity_spark?.length ? dashData.velocity_spark : [58, 42, 38, 31, 42, 23]; const ragReport = latest.find(r => r.type === "rag") || latest[0]; const overallRag = ragReport?.overall_rag || ragReport?.overall || "red"; const weekNum = getIsoWeek(); const lastRunAgo = timeAgo(ragReport?.generated_at); const pageSub = lastRunAgo ? `Here's where BuildLog stands going into week ${weekNum}. Last report: ${lastRunAgo}.` : `Here's where BuildLog stands going into week ${weekNum}.`; const metrics = ragReport?.metrics || {}; const velPct = metrics.velocityPct ?? 23; const overdueVal = metrics.overdue ?? 21; const completionPct = metrics.done != null && metrics.totalIssues ? Math.round((metrics.done / metrics.totalIssues) * 100) : metrics.velocityPct ?? 29; const overdueTrend = [5, 9, 12, 18, 17, overdueVal]; const completionTrend = [72, 64, 48, 40, 32, completionPct]; const kpis = [ { label: "Overall RAG", v: , spark: null, delta: "last report" }, { label: "Velocity", v: `${velPct}%`, rag: velPct < 60 ? "red" : velPct < 80 ? "amber" : "green", delta: "vs estimate", spark: velSpark, sparkColor: "var(--red)" }, { label: "Overdue items", v: String(overdueVal), rag: overdueVal > 5 ? "red" : overdueVal > 2 ? "amber" : "green", delta: "open issues", spark: overdueTrend, sparkColor: "var(--red)" }, { label: "Completion", v: `${completionPct}%`, rag: completionPct < 50 ? "red" : completionPct < 75 ? "amber" : "green", delta: "done / total", spark: completionTrend, sparkColor: "var(--red)" }, ]; return (
BuildLog BL
Good morning, Maks.
{pageSub}
{/* KPI strip */}
{kpis.map((k, i) => (
{k.label}
{typeof k.v === "string" ? ( <>
{k.v}
{k.rag &&
} ) : k.v}
{k.spark && }
{k.delta}
))}
{/* Two columns: Latest reports + Pattern alert */}
Latest run · {(ragReport?.date || latest[0]?.date || "—")}
{latest.map((r, idx) => { const t = reportTypeMeta(r.type); const Icon = t ? t.icon : I.File; const rag = r.overall_rag || r.overall || "amber"; return (
onNav("report", { reportId: r.id })} onMouseEnter={e => e.currentTarget.style.background = "var(--surface-2)"} onMouseLeave={e => e.currentTarget.style.background = "transparent"} >
{t ? t.name : r.type}
{t ? t.tagline : r.date}
{rag && }
); })}
{/* Pattern alert card */}
Pattern alert
99.8%
Matches BL Sprint 3
Current sprint fingerprint is 99.8% similar to a historically failed sprint — 22% completion, 35% overdue ratio. Blocker ratio is already higher than baseline.
{/* Workstream strip */}
Workstreams
18 Apr 2026
{[ { n: "Backend", rag: "red", v: 22, done: "6/21", overdue: 7, blockers: 1, bugs: 3, owner: "alex.petrov" }, { n: "Frontend", rag: "red", v: 31, done: "6/20", overdue: 7, blockers: 0, bugs: 4, owner: "natasha.web" }, { n: "QA", rag: "red", v: 22, done: "4/14", overdue: 4, blockers: 2, bugs: 1, owner: "dmitri.fox" }, ].map((w, i, arr) => (
{w.n}
{w.v}% velocity
{w.done} done
{[ { l: "Overdue", v: w.overdue, color: w.overdue > 0 ? "var(--red)" : "var(--text-2)" }, { l: "Blockers", v: w.blockers, color: w.blockers > 0 ? "var(--amber)" : "var(--text-2)" }, { l: "Open bugs", v: w.bugs, color: "var(--text-2)" }, ].map(m => (
{m.l}
{m.v}
))}
lead · {w.owner}
))}
{/* Recent activity */}
Recent activity
{[ { t: "3h ago", icon: I.Zap, text: <>Maks generated all 4 reports for week ending 18 Apr, meta: "RED overall · 56s" }, { t: "3h ago", icon: I.Alert, text: <>Pattern report flagged 99.8% match with BL Sprint 3, meta: "failed sprint historical" }, { t: "1d ago", icon: I.Shield, text: <>BL-41 escalated to Critical blocker in RAID log, meta: "alex.petrov" }, { t: "1w ago", icon: I.Zap, text: <>Scheduled weekly run completed · AMBER → RED transition, meta: "4 of 4 reports" }, { t: "2w ago", icon: I.Settings, text: <>Schedule updated — Fridays 17:00 Europe/Kyiv, meta: "" }, ].map((a, i) => (
{a.text}
{a.meta}
{a.t}
))}
); } /* ------------------------------ GENERATE (wizard with streaming) ------------------------------ */ function GenerateScreen({ onNav, initialSelection = [] }) { // step: "configure" | "running" | "done" const [step, setStep] = useState("configure"); const [selected, setSelected] = useState( initialSelection.length > 0 ? initialSelection : REPORT_TYPES.map(r => r.id) ); const [period, setPeriod] = useState("week"); const [projectKey, setProjectKey] = useState("BL"); const [notify, setNotify] = useState(true); const [dryRun, setDryRun] = useState(false); // run state const [progress, setProgress] = useState({}); // { id: 0..100 } const [logs, setLogs] = useState([]); const [currentIdx, setCurrentIdx] = useState(-1); const logsRef = useRef(null); const selectedTypes = REPORT_TYPES.filter(t => selected.includes(t.id)); const toggle = (id) => { setSelected(sel => sel.includes(id) ? sel.filter(x => x !== id) : [...sel, id]); }; const [runId, setRunId] = useState(null); const [runError, setRunError] = useState(""); const startRun = async () => { setStep("running"); setRunError(""); setProgress(Object.fromEntries(selectedTypes.map(t => [t.id, 0]))); setLogs([{ t: "00:00:00", msg: "Starting agents…" }]); setCurrentIdx(0); try { const agents = selected.length === REPORT_TYPES.length ? ["all"] : selected; const { run_id } = await api.post("/api/generate", { agents, dry_run: dryRun }); setRunId(run_id); } catch (err) { setRunError(err.message); setStep("configure"); } }; // SSE streaming useEffect(() => { if (!runId || step !== "running") return; let agentIdx = 0; const es = api.streamRun(runId, (data) => { if (data.done) { setProgress(p => Object.fromEntries(Object.keys(p).map(k => [k, 100]))); setTimeout(() => setStep("done"), 500); return; } const msg = data.msg || ""; setLogs(l => [...l, { t: data.t || "", msg, done: msg.includes("✓") }]); // Advance per-agent progress heuristically based on log content const type = selectedTypes[agentIdx]; if (type) { if (msg.includes("Fetching")) setProgress(p => ({ ...p, [type.id]: 20 })); else if (msg.includes("Computing") || msg.includes("Processing")) setProgress(p => ({ ...p, [type.id]: 50 })); else if (msg.includes("Claude") || msg.includes("narrative")) setProgress(p => ({ ...p, [type.id]: 75 })); else if (msg.includes("saved") || msg.includes("HTML")) { setProgress(p => ({ ...p, [type.id]: 100 })); agentIdx = Math.min(agentIdx + 1, selectedTypes.length - 1); } } }); return () => es.close(); }, [runId]); useEffect(() => { if (logsRef.current) logsRef.current.scrollTop = logsRef.current.scrollHeight; }, [logs]); if (step === "configure") { return (
Step 1 of 2 · Configure run
Generate reports
Pick what to run. Claude reads fresh Jira data and produces stakeholder-ready HTML.
{/* Report cards */}
Reports to generate
{REPORT_TYPES.map(t => { const active = selected.includes(t.id); const Icon = t.icon; return (
toggle(t.id)} className="card" style={{ padding: 18, cursor: "pointer", borderColor: active ? "var(--text)" : "var(--border)", boxShadow: active ? "0 0 0 3px var(--surface-3)" : "none", transition: "all 0.15s", }}>
{t.name}
{t.runtime}
{active && }
{t.desc}
); })}
{/* Options */}
Options
setProjectKey(e.target.value)} />
buildlog001.atlassian.net
{[{ id: "week", l: "This week" }, { id: "sprint", l: "Current sprint" }, { id: "month", l: "Last 30d" }].map(p => ( ))}
{[ { id: "notify", v: notify, set: setNotify, l: "Post to #buildlog-reports Slack channel when done" }, { id: "dry", v: dryRun, set: setDryRun, l: "Dry-run (generate without saving to archive)" }, ].map(o => ( ))}
{/* Footer action bar */}
{runError ? {runError} : <>{selected.length} of {REPORT_TYPES.length} reports ·{" "} ~{selectedTypes.reduce((s, t) => s + parseInt(t.runtime.replace(/\D/g, "")), 0)}s estimated }
); } // RUNNING or DONE const allDone = step === "done"; return (
Step 2 of 2 · {allDone ? "Completed" : "Running"}
{allDone ? "Your reports are ready." : "Generating reports…"}
{allDone ? `${selectedTypes.length} reports generated in ${((logs.length) * 0.6).toFixed(1)}s · saved to archive` : `Reading Jira, scoring, and invoking Claude. Don't close this tab.`}
{allDone && ( )}
{/* Progress list */}
{selectedTypes.map((t, i, arr) => { const pct = progress[t.id] || 0; const isActive = currentIdx === i && !allDone; const isDone = pct >= 100; const Icon = t.icon; return (
{isDone ? : }
{t.name}
{t.tagline}
{isDone ? "done" : isActive ? `${pct}%` : "queued"}
); })}
{/* Live log */}
Live log
agents_runner.py · pid 48291
{logs.map((l, i) => (
{l.t} {l.msg}
))} {!allDone && (
)}
{/* Done — report links */} {allDone && (
Generated reports
{selectedTypes.map((t, i, arr) => { const Icon = t.icon; return (
onNav("report", { reportId: `${t.id[0]}-${new Date().toISOString().slice(0,10)}` })} style={{ padding: "14px 20px", borderBottom: i < arr.length - 1 ? "1px solid var(--border)" : "none", display: "grid", gridTemplateColumns: "auto 1fr auto auto", alignItems: "center", gap: 16, cursor: "pointer" }} onMouseEnter={e => e.currentTarget.style.background = "var(--surface-2)"} onMouseLeave={e => e.currentTarget.style.background = "transparent"} >
{t.name}
{t.id}_{new Date().toISOString().slice(0,10)}.html
); })}
)}
); } Object.assign(window, { LoginScreen, Dashboard, GenerateScreen });