From 44656171344cf5f74d9c5c84e9bdaae5d6f099a3 Mon Sep 17 00:00:00 2001 From: Jonathan Singer Date: Mon, 20 Jul 2026 16:48:52 -0400 Subject: [PATCH] Polish the local viewer: inline verify, clean titles, smart default view - Verify your email right on the Past runs page instead of being sent off to the Email report page; the list unlocks in place once verified. - Show a clean target-based title (like arch.co) and a relative time instead of the raw run folder name everywhere runs are listed. - Land on Agents while a scan is still live and on Overview once it is done, without ever overriding where you navigate yourself. --- strix/viewer_src/src/App.tsx | 78 ++++++--- .../src/components/EmailVerifyInline.tsx | 163 ++++++++++++++++++ .../src/components/PastRunsView.tsx | 64 +++++-- strix/viewer_src/src/lib/target-utils.ts | 10 ++ 4 files changed, 278 insertions(+), 37 deletions(-) create mode 100644 strix/viewer_src/src/components/EmailVerifyInline.tsx diff --git a/strix/viewer_src/src/App.tsx b/strix/viewer_src/src/App.tsx index 74ce7e2c..23e75993 100644 --- a/strix/viewer_src/src/App.tsx +++ b/strix/viewer_src/src/App.tsx @@ -39,6 +39,7 @@ import { type RunsPayload, } from "@/data/serverSource"; import { SIGNUP_URL, ctaUrl, trackCta } from "@/lib/cta"; +import { runTitle } from "@/lib/target-utils"; import Sidebar from "@/components/Sidebar"; import PastRunsView from "@/components/PastRunsView"; import EmailReportView from "@/components/EmailReportView"; @@ -175,43 +176,73 @@ export default function App() { const agentCount = run?.transcript.agents.length ?? 0; const verified = auth?.verified === true; + // Per-run guard for the default view: land on Agents while a scan is live, + // Overview once it finishes. Applied at most once per run and never once the + // user has navigated manually (userSetView flips the guard). + const initialViewAppliedRef = useRef(false); + + // Reset the guard whenever the active run changes so the newly selected run + // gets its own default. + useEffect(() => { + initialViewAppliedRef.current = false; + }, [activeRun]); + + useEffect(() => { + if (initialViewAppliedRef.current || !run) return; + if (run.finished) { + initialViewAppliedRef.current = true; + setView("overview"); + } else if (agentCount > 0) { + // Live and agents have appeared: default to the agent graph. If it is + // live but no agents exist yet, wait (do not apply, do not set the flag). + initialViewAppliedRef.current = true; + setView("agents"); + } + }, [run, agentCount]); + + // User-initiated navigation: mark the default guard applied so the per-run + // default effect never yanks the user off the view they chose. + const userSetView = useCallback((v: View) => { + initialViewAppliedRef.current = true; + setView(v); + }, []); + const selectRun = useCallback((name: string) => { setActiveRun(name); setSelectedId(null); setRun(null); setError(null); - setView("overview"); + // Reset the guard so the per-run default applies to the newly selected run. + initialViewAppliedRef.current = false; }, []); const goEmail = useCallback((skipDisclosure: boolean, surface: string) => { trackCta("email_report", surface); setEmailPurpose("report"); setEmailSkipDisclosure(skipDisclosure); - setView("email"); - }, []); + userSetView("email"); + }, [userSetView]); // Sidebar entry keeps the disclosure (first place those users see it); const openEmail = useCallback(() => goEmail(false, "sidebar"), [goEmail]); // the Overview CTA already states the tradeoff, so it starts the flow directly. const openEmailFromOverview = useCallback(() => goEmail(true, "overview"), [goEmail]); - const openVerify = useCallback(() => { - trackCta("history_unlock", "past_runs"); - setEmailPurpose("verify"); - setEmailSkipDisclosure(false); - setView("email"); - }, []); - const openHistory = useCallback(() => { void refreshRuns(); - setView("history"); - }, [refreshRuns]); + userSetView("history"); + }, [refreshRuns, userSetView]); + + const onPastRunsVerified = useCallback(async () => { + await refreshAuth(); + await refreshRuns(); + }, [refreshAuth, refreshRuns]); const selectFeature = useCallback((slug: string) => { trackCta(slug, "sidebar_nav"); setActiveFeature(slug); - setView("feature"); - }, []); + userSetView("feature"); + }, [userSetView]); const onForget = useCallback(async () => { await forgetAuth(); @@ -225,7 +256,7 @@ export default function App() { view={view} onSelectView={(v) => { if (v === "history") openHistory(); - else setView(v); + else userSetView(v); }} activeFeature={activeFeature} onSelectFeature={selectFeature} @@ -261,7 +292,7 @@ export default function App() { )} @@ -319,7 +350,7 @@ export default function App() { runs={runs} activeRun={activeRun} onSelectRun={selectRun} - onVerifyClick={openVerify} + onVerified={() => void onPastRunsVerified()} /> ) : !run && !error ? ( @@ -333,14 +364,14 @@ export default function App() { {/* Tab strip: shown on small screens where the sidebar is hidden. */}
- setView("overview")}> + userSetView("overview")}> Overview - setView("issues")}> + userSetView("issues")}> Issues{run.vulnerabilities.length > 0 ? ` (${run.vulnerabilities.length})` : ""} {agentCount > 0 && ( - setView("agents")}> + userSetView("agents")}> Agents ({agentCount}) )} @@ -393,7 +424,8 @@ function RunSwitcher({ onSelect: (name: string) => void; }) { const [open, setOpen] = useState(false); - const current = activeRun ?? launchedName; + const activeEntry = runs.runs.find((r) => r.name === activeRun); + const current = activeEntry ? runTitle(activeEntry.target, activeEntry.name) : launchedName; return (
+ + ) : ( +
{ + e.preventDefault(); + void submitCode(); + }} + > + + + +
+ )} +
+ ); +} diff --git a/strix/viewer_src/src/components/PastRunsView.tsx b/strix/viewer_src/src/components/PastRunsView.tsx index c3703df2..ac043097 100644 --- a/strix/viewer_src/src/components/PastRunsView.tsx +++ b/strix/viewer_src/src/components/PastRunsView.tsx @@ -1,5 +1,9 @@ +import { useState } from "react"; import { History, ChevronRight, Terminal } from "lucide-react"; import type { RunListEntry, RunsPayload, RunSeverityCounts } from "@/data/serverSource"; +import { runTitle } from "@/lib/target-utils"; +import { trackCta } from "@/lib/cta"; +import EmailVerifyInline from "@/components/EmailVerifyInline"; /** * "Past runs" panel. Unverified users see a tease with the run count and a @@ -45,20 +49,41 @@ function formatDate(iso: string | null): string | null { }); } +/** + * Relative time ("just now" / "5m ago" / "3h ago" / "2d ago"), falling back to + * the absolute date for anything older than a week (mirrors the pro app). + */ +function formatTimeAgo(iso: string | null): string | null { + if (!iso) return null; + const normalized = iso.trim().replace(" UTC", "Z").replace(" ", "T"); + const d = new Date(normalized); + if (Number.isNaN(d.getTime())) return null; + const diffMs = Date.now() - d.getTime(); + const mins = Math.floor(diffMs / 60000); + if (mins < 1) return "just now"; + if (mins < 60) return `${mins}m ago`; + const hours = Math.floor(mins / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + if (days < 7) return `${days}d ago`; + return formatDate(iso); +} + interface PastRunsViewProps { runs: RunsPayload | null; activeRun: string | null; onSelectRun: (name: string) => void; - onVerifyClick: () => void; + onVerified: () => void; } export default function PastRunsView({ runs, activeRun, onSelectRun, - onVerifyClick, + onVerified, }: PastRunsViewProps) { const count = runs?.count ?? 0; + const [showVerify, setShowVerify] = useState(false); if (!runs || runs.locked) { return ( @@ -73,12 +98,24 @@ export default function PastRunsView({

You have {count} past {count === 1 ? "run" : "runs"} on this machine.

- + {showVerify ? ( + <> +

+ Verify your email with a one-time code to unlock the full history. +

+ + + ) : ( + + )}

{runs.runs.map((run: RunListEntry) => { const active = run.name === activeRun; - const date = formatDate(run.start_time) ?? formatDate(run.end_time); + const date = formatTimeAgo(run.start_time) ?? formatTimeAgo(run.end_time); + const title = runTitle(run.target, run.name); return (