failproofai 0.0.11-beta.4 → 0.0.11-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +9 -9
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +9 -9
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0sb_5m8._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__13ra2jq._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1_mqemn._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1b9z5-i._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rhmvod._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1-1804f._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +8 -8
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00jkjmt._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013du6r._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0-wdurj._.js → [root-of-the-server]__09sb8gm._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e85wxv._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gfxvb1._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__14dd6h8._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1cd25c7._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__06f-yix._.js → [root-of-the-server]__1hldld5._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1hlrq6y._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ihxdo5._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1mt35_w._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pcxxwg._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1vvfde2._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +15 -15
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_html2canvas_dist_html2canvas_esm_0hmsdod.js → node_modules_html2canvas_dist_html2canvas_esm_0jar0u6.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_1d_onnt._.js +2 -2
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +10 -10
- package/.next/standalone/.next/static/chunks/{2zjavpea8glet.js → 0faafgt7it9wg.js} +1 -1
- package/.next/standalone/.next/static/chunks/{36l507r2mbf5h.js → 1ha7bu3h33vlg.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1e0o-6kjfnwzh.js → 28ktbb6r1yts-.js} +1 -1
- package/.next/standalone/.next/static/chunks/{41jjdgqqpl7em.js → 2msp6lxlj6y58.js} +1 -1
- package/.next/standalone/.next/static/chunks/2posnfuhnwuc9.js +41 -0
- package/.next/standalone/.next/static/chunks/{20b5bz43so-1k.js → 3-hps4mh626t-.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3y3l184vijlfg.js → 35vxydl4wq7x_.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1btx2c49fk9xt.css → 3co9znvxzaj4r.css} +1 -1
- package/.next/standalone/.next/static/chunks/{0nbmu4_n1j2-c.js → 3yg0zz4h9pe--.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0fkntmojmibda.js → 445x6v-0e8vzo.js} +1 -1
- package/.next/standalone/app/actions/get-audit-result.ts +14 -3
- package/.next/standalone/app/audit/_components/audit-dashboard.tsx +108 -20
- package/.next/standalone/app/audit/_components/audit-progress-strip.tsx +98 -0
- package/.next/standalone/app/audit/_components/rerun-button.tsx +4 -1
- package/.next/standalone/app/audit/_components/return-section.tsx +17 -29
- package/.next/standalone/app/audit/_components/top-audit-bar.tsx +153 -0
- package/.next/standalone/app/audit/audit-styles.css +258 -0
- package/.next/standalone/app/audit/page.tsx +19 -9
- package/.next/standalone/lib/fetch-with-timeout.ts +37 -20
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/server.js +1 -1
- package/README.md +6 -0
- package/dist/cli.mjs +6 -3
- package/lib/fetch-with-timeout.ts +37 -20
- package/package.json +1 -1
- package/src/audit/archetypes.ts +24 -4
- package/src/audit/cache.ts +20 -1
- package/src/audit/dashboard-cache.ts +40 -0
- package/src/audit/features.ts +65 -19
- package/.next/standalone/.next/static/chunks/1bxdpd036n3l9.js +0 -41
- /package/.next/standalone/.next/static/{t4nu8RTRp3Z2srol-9pVb → eWoFUCYXiD-HNIeHw6fYp}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{t4nu8RTRp3Z2srol-9pVb → eWoFUCYXiD-HNIeHw6fYp}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{t4nu8RTRp3Z2srol-9pVb → eWoFUCYXiD-HNIeHw6fYp}/_ssgManifest.js +0 -0
|
@@ -30,6 +30,9 @@ import { ReturnSection } from "./return-section";
|
|
|
30
30
|
import { ReportFooter } from "./report-footer";
|
|
31
31
|
import { EmptyState } from "./empty-state";
|
|
32
32
|
import { RunProgress } from "./run-progress";
|
|
33
|
+
import { TopAuditBar, type TopAuditBarMode } from "./top-audit-bar";
|
|
34
|
+
import { AuditProgressStrip, type RerunStatus } from "./audit-progress-strip";
|
|
35
|
+
import { RerunError, triggerRun } from "./rerun-button";
|
|
33
36
|
|
|
34
37
|
// IMPORTANT: do NOT import BUILTIN_POLICIES or AUDIT_DETECTORS here.
|
|
35
38
|
// Both pull in node:fs and execSync (workflow policies), which Next.js
|
|
@@ -38,7 +41,12 @@ import { RunProgress } from "./run-progress";
|
|
|
38
41
|
|
|
39
42
|
type Initial =
|
|
40
43
|
| { status: "cached"; cachedAt: string; params: RunAuditOptions; result: AuditResult }
|
|
41
|
-
| { status: "empty" };
|
|
44
|
+
| { status: "empty"; expired: boolean; expiredAt: string | null };
|
|
45
|
+
|
|
46
|
+
/** Tag passed to the shared `startRerun()` handler so PostHog can tell
|
|
47
|
+
* whether the click came from the new top bar or the existing bottom
|
|
48
|
+
* return-section button. */
|
|
49
|
+
export type RerunSource = "top_bar" | "return_section" | "empty_state";
|
|
42
50
|
|
|
43
51
|
interface Props {
|
|
44
52
|
initial: Initial;
|
|
@@ -82,6 +90,7 @@ function inferProjectName(result: AuditResult, override?: string): string {
|
|
|
82
90
|
export function AuditDashboard({ initial, projectFromUrl, totalCatalogSize }: Props) {
|
|
83
91
|
const [cache, setCache] = useState<Initial>(initial);
|
|
84
92
|
const [running, setRunning] = useState(false);
|
|
93
|
+
const [rerunStatus, setRerunStatus] = useState<RerunStatus>({ kind: "idle" });
|
|
85
94
|
const { capture } = usePostHog();
|
|
86
95
|
const pageViewStateRef = useRef<string | null>(null);
|
|
87
96
|
const scrollTrackedRef = useRef(false);
|
|
@@ -90,7 +99,37 @@ export function AuditDashboard({ initial, projectFromUrl, totalCatalogSize }: Pr
|
|
|
90
99
|
|
|
91
100
|
const refreshFromCache = useCallback(async () => {
|
|
92
101
|
const payload = await getAuditResultAction();
|
|
93
|
-
|
|
102
|
+
setCache(payload);
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
/** Shared re-audit handler used by the top bar and the bottom button.
|
|
106
|
+
* Drives `rerunStatus` (which renders the sticky progress strip) and
|
|
107
|
+
* soft-refreshes the dashboard cache on success — no full page reload. */
|
|
108
|
+
const startRerun = useCallback(async (source: RerunSource) => {
|
|
109
|
+
if (running) return;
|
|
110
|
+
capture("audit_rerun_clicked", { source, since: "30d" });
|
|
111
|
+
setRunning(true);
|
|
112
|
+
setRerunStatus({ kind: "running", startedAt: Date.now() });
|
|
113
|
+
try {
|
|
114
|
+
await triggerRun({ cli: [], since: "30d" });
|
|
115
|
+
await refreshFromCache();
|
|
116
|
+
setRerunStatus({ kind: "idle" });
|
|
117
|
+
} catch (err) {
|
|
118
|
+
const kind = err instanceof RerunError ? err.kind : "network";
|
|
119
|
+
capture("audit_rerun_failed", {
|
|
120
|
+
kind,
|
|
121
|
+
source,
|
|
122
|
+
since: "30d",
|
|
123
|
+
cli_filter: "all",
|
|
124
|
+
});
|
|
125
|
+
setRerunStatus({ kind: "failed", reason: kind, failedAt: Date.now() });
|
|
126
|
+
} finally {
|
|
127
|
+
setRunning(false);
|
|
128
|
+
}
|
|
129
|
+
}, [capture, refreshFromCache, running]);
|
|
130
|
+
|
|
131
|
+
const dismissRerunStatus = useCallback(() => {
|
|
132
|
+
setRerunStatus({ kind: "idle" });
|
|
94
133
|
}, []);
|
|
95
134
|
|
|
96
135
|
// Body class for audit-only background + grain texture. Applied once on
|
|
@@ -152,11 +191,21 @@ export function AuditDashboard({ initial, projectFromUrl, totalCatalogSize }: Pr
|
|
|
152
191
|
};
|
|
153
192
|
}, [cache.status, capture, resultsCount, running, transcriptsScanned]);
|
|
154
193
|
|
|
194
|
+
// Compute the top-bar mode (only matters on empty paths — MainReport
|
|
195
|
+
// always renders the bar in "cached" mode below).
|
|
196
|
+
const emptyBarMode: TopAuditBarMode =
|
|
197
|
+
cache.status === "empty" && cache.expired ? "expired" : "empty";
|
|
198
|
+
const emptyBarCachedAt =
|
|
199
|
+
cache.status === "empty" && cache.expired ? cache.expiredAt : null;
|
|
200
|
+
|
|
155
201
|
/* ---- empty / first-run ----------------------------------------- */
|
|
156
202
|
if (cache.status === "empty" && !running) {
|
|
157
203
|
return (
|
|
158
204
|
<ShellEmpty
|
|
159
205
|
running={false}
|
|
206
|
+
topBar={{ mode: emptyBarMode, cachedAt: emptyBarCachedAt, isRunning: running, onRerun: () => startRerun("empty_state") }}
|
|
207
|
+
rerunStatus={rerunStatus}
|
|
208
|
+
onDismissRerun={dismissRerunStatus}
|
|
160
209
|
onStarted={() => setRunning(true)}
|
|
161
210
|
onCompleted={async () => { setRunning(false); await refreshFromCache(); }}
|
|
162
211
|
/>
|
|
@@ -166,6 +215,9 @@ export function AuditDashboard({ initial, projectFromUrl, totalCatalogSize }: Pr
|
|
|
166
215
|
return (
|
|
167
216
|
<ShellEmpty
|
|
168
217
|
running
|
|
218
|
+
topBar={{ mode: emptyBarMode, cachedAt: emptyBarCachedAt, isRunning: running, onRerun: () => startRerun("empty_state") }}
|
|
219
|
+
rerunStatus={rerunStatus}
|
|
220
|
+
onDismissRerun={dismissRerunStatus}
|
|
169
221
|
onStarted={() => {}}
|
|
170
222
|
onCompleted={async () => { setRunning(false); await refreshFromCache(); }}
|
|
171
223
|
/>
|
|
@@ -184,28 +236,20 @@ export function AuditDashboard({ initial, projectFromUrl, totalCatalogSize }: Pr
|
|
|
184
236
|
<ShellEmpty
|
|
185
237
|
running={running}
|
|
186
238
|
mode="zero-sessions"
|
|
239
|
+
topBar={{ mode: "cached", cachedAt, isRunning: running, onRerun: () => startRerun("empty_state") }}
|
|
240
|
+
rerunStatus={rerunStatus}
|
|
241
|
+
onDismissRerun={dismissRerunStatus}
|
|
187
242
|
onStarted={() => setRunning(true)}
|
|
188
243
|
onCompleted={async () => { setRunning(false); await refreshFromCache(); }}
|
|
189
244
|
/>
|
|
190
245
|
);
|
|
191
246
|
}
|
|
192
247
|
|
|
193
|
-
/* ---- in-flight re-run ------------------------------------------ */
|
|
194
|
-
if (running) {
|
|
195
|
-
return (
|
|
196
|
-
<div className="app">
|
|
197
|
-
<div className="scanline-overlay" />
|
|
198
|
-
<div className="app-shell">
|
|
199
|
-
<div className="report">
|
|
200
|
-
<RunProgress />
|
|
201
|
-
</div>
|
|
202
|
-
<ReportFooter cachedAt={cachedAt} />
|
|
203
|
-
</div>
|
|
204
|
-
</div>
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
248
|
/* ---- main report ----------------------------------------------- */
|
|
249
|
+
// Note: on re-audit from a cached state, we deliberately stay on the
|
|
250
|
+
// MainReport instead of swapping to RunProgress. The sticky
|
|
251
|
+
// AuditProgressStrip provides the in-flight indicator, and the soft
|
|
252
|
+
// refresh on completion (via refreshFromCache) avoids a full reload.
|
|
209
253
|
return (
|
|
210
254
|
<MainReport
|
|
211
255
|
result={result}
|
|
@@ -213,6 +257,10 @@ export function AuditDashboard({ initial, projectFromUrl, totalCatalogSize }: Pr
|
|
|
213
257
|
params={params}
|
|
214
258
|
projectFromUrl={projectFromUrl}
|
|
215
259
|
totalCatalogSize={totalCatalogSize}
|
|
260
|
+
isRunning={running}
|
|
261
|
+
rerunStatus={rerunStatus}
|
|
262
|
+
onRerun={(source) => startRerun(source)}
|
|
263
|
+
onDismissRerun={dismissRerunStatus}
|
|
216
264
|
/>
|
|
217
265
|
);
|
|
218
266
|
}
|
|
@@ -223,9 +271,23 @@ interface MainReportProps {
|
|
|
223
271
|
params: RunAuditOptions | undefined;
|
|
224
272
|
projectFromUrl?: string;
|
|
225
273
|
totalCatalogSize: number;
|
|
274
|
+
isRunning: boolean;
|
|
275
|
+
rerunStatus: RerunStatus;
|
|
276
|
+
onRerun: (source: RerunSource) => void;
|
|
277
|
+
onDismissRerun: () => void;
|
|
226
278
|
}
|
|
227
279
|
|
|
228
|
-
function MainReport({
|
|
280
|
+
function MainReport({
|
|
281
|
+
result,
|
|
282
|
+
cachedAt,
|
|
283
|
+
params,
|
|
284
|
+
projectFromUrl,
|
|
285
|
+
totalCatalogSize,
|
|
286
|
+
isRunning,
|
|
287
|
+
rerunStatus,
|
|
288
|
+
onRerun,
|
|
289
|
+
onDismissRerun,
|
|
290
|
+
}: MainReportProps) {
|
|
229
291
|
const { capture } = usePostHog();
|
|
230
292
|
const project = useMemo(() => inferProjectName(result, projectFromUrl), [result, projectFromUrl]);
|
|
231
293
|
// Seed classification with the project name so the behaviour fingerprint
|
|
@@ -288,9 +350,16 @@ function MainReport({ result, cachedAt, params, projectFromUrl, totalCatalogSize
|
|
|
288
350
|
|
|
289
351
|
return (
|
|
290
352
|
<div className="app">
|
|
353
|
+
<AuditProgressStrip status={rerunStatus} onDismiss={onDismissRerun} />
|
|
291
354
|
<div className="scanline-overlay" />
|
|
292
355
|
<div className="app-shell">
|
|
293
356
|
<div className="report">
|
|
357
|
+
<TopAuditBar
|
|
358
|
+
mode="cached"
|
|
359
|
+
cachedAt={cachedAt}
|
|
360
|
+
isRunning={isRunning}
|
|
361
|
+
onRerun={() => onRerun("top_bar")}
|
|
362
|
+
/>
|
|
294
363
|
<IdentitySection
|
|
295
364
|
ref={identityFrameRef}
|
|
296
365
|
archetypeKey={classification.archetype}
|
|
@@ -312,7 +381,11 @@ function MainReport({ result, cachedAt, params, projectFromUrl, totalCatalogSize
|
|
|
312
381
|
archetypeKey={classification.archetype}
|
|
313
382
|
project={project}
|
|
314
383
|
/>
|
|
315
|
-
<ReturnSection
|
|
384
|
+
<ReturnSection
|
|
385
|
+
result={result}
|
|
386
|
+
isRunning={isRunning}
|
|
387
|
+
onRerun={() => onRerun("return_section")}
|
|
388
|
+
/>
|
|
316
389
|
<FindingsSection findings={findings} />
|
|
317
390
|
<PoliciesSection result={result} projected={projected} projectedGrade={projectedGrade} />
|
|
318
391
|
</div>
|
|
@@ -333,19 +406,34 @@ function MainReport({ result, cachedAt, params, projectFromUrl, totalCatalogSize
|
|
|
333
406
|
interface ShellEmptyProps {
|
|
334
407
|
running: boolean;
|
|
335
408
|
mode?: "no-cache" | "zero-sessions";
|
|
409
|
+
topBar: {
|
|
410
|
+
mode: TopAuditBarMode;
|
|
411
|
+
cachedAt: string | null;
|
|
412
|
+
isRunning: boolean;
|
|
413
|
+
onRerun: () => void;
|
|
414
|
+
};
|
|
415
|
+
rerunStatus: RerunStatus;
|
|
416
|
+
onDismissRerun: () => void;
|
|
336
417
|
onStarted: () => void;
|
|
337
418
|
onCompleted: () => Promise<void> | void;
|
|
338
419
|
}
|
|
339
420
|
|
|
340
|
-
function ShellEmpty({ running, mode = "no-cache", onStarted, onCompleted }: ShellEmptyProps) {
|
|
421
|
+
function ShellEmpty({ running, mode = "no-cache", topBar, rerunStatus, onDismissRerun, onStarted, onCompleted }: ShellEmptyProps) {
|
|
341
422
|
// Use the archetype "optimist" sigil for the empty-state visual so the
|
|
342
423
|
// page doesn't render with a dead box. EmptyState itself is unchanged
|
|
343
424
|
// from the previous build.
|
|
344
425
|
return (
|
|
345
426
|
<div className="app">
|
|
427
|
+
<AuditProgressStrip status={rerunStatus} onDismiss={onDismissRerun} />
|
|
346
428
|
<div className="scanline-overlay" />
|
|
347
429
|
<div className="app-shell">
|
|
348
430
|
<div className="report">
|
|
431
|
+
<TopAuditBar
|
|
432
|
+
mode={topBar.mode}
|
|
433
|
+
cachedAt={topBar.cachedAt}
|
|
434
|
+
isRunning={topBar.isRunning}
|
|
435
|
+
onRerun={topBar.onRerun}
|
|
436
|
+
/>
|
|
349
437
|
{running ? (
|
|
350
438
|
<RunProgress />
|
|
351
439
|
) : (
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sticky banner pinned to the top of the viewport during a re-audit run.
|
|
5
|
+
* Lifted from the bottom `[ scanning… ]` button so the user has a clear,
|
|
6
|
+
* persistent signal that work is in progress regardless of scroll
|
|
7
|
+
* position.
|
|
8
|
+
*
|
|
9
|
+
* Visual vocabulary matches the page:
|
|
10
|
+
* - pink hard-offset left border (echoes `.share-btn:hover`),
|
|
11
|
+
* - mono lowercase,
|
|
12
|
+
* - thin pulse on the right edge driven by a CSS keyframe so we don't
|
|
13
|
+
* fake progress precision we don't have.
|
|
14
|
+
*
|
|
15
|
+
* Reuses the existing `RerunError.kind` discrimination to render a red
|
|
16
|
+
* error strip with kind-specific copy when a run dies.
|
|
17
|
+
*/
|
|
18
|
+
import React, { useEffect, useState } from "react";
|
|
19
|
+
import type { RerunError } from "./rerun-button";
|
|
20
|
+
|
|
21
|
+
export type RerunStatus =
|
|
22
|
+
| { kind: "idle" }
|
|
23
|
+
| { kind: "running"; startedAt: number }
|
|
24
|
+
| { kind: "failed"; reason: RerunError["kind"]; failedAt: number };
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
status: RerunStatus;
|
|
28
|
+
/** Lets the user dismiss the red error strip without re-trying. */
|
|
29
|
+
onDismiss: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const ERROR_COPY: Record<RerunError["kind"], string> = {
|
|
33
|
+
timeout: "audit took too long — try again",
|
|
34
|
+
network: "network hiccup — check your connection",
|
|
35
|
+
post_failed: "audit failed to start — try again",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function formatElapsed(ms: number): string {
|
|
39
|
+
if (ms < 0) return "00:00";
|
|
40
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
41
|
+
const mm = Math.floor(totalSeconds / 60).toString().padStart(2, "0");
|
|
42
|
+
const ss = (totalSeconds % 60).toString().padStart(2, "0");
|
|
43
|
+
return `${mm}:${ss}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function AuditProgressStrip({ status, onDismiss }: Props) {
|
|
47
|
+
// Tick once a second only while running. setInterval is cheaper than
|
|
48
|
+
// requestAnimationFrame for a one-second display update.
|
|
49
|
+
const [now, setNow] = useState(() => Date.now());
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (status.kind !== "running") return;
|
|
52
|
+
const t = setInterval(() => setNow(Date.now()), 1000);
|
|
53
|
+
return () => clearInterval(t);
|
|
54
|
+
}, [status.kind]);
|
|
55
|
+
|
|
56
|
+
if (status.kind === "idle") return null;
|
|
57
|
+
|
|
58
|
+
if (status.kind === "running") {
|
|
59
|
+
const elapsed = Math.max(0, now - status.startedAt);
|
|
60
|
+
return (
|
|
61
|
+
<div className="audit-progress-strip" role="status" aria-live="polite">
|
|
62
|
+
<div className="audit-progress-strip__inner">
|
|
63
|
+
<span className="audit-progress-strip__label">
|
|
64
|
+
<span className="audit-progress-strip__spinner" aria-hidden="true" />
|
|
65
|
+
re-auditing your sessions
|
|
66
|
+
</span>
|
|
67
|
+
<span className="audit-progress-strip__elapsed">
|
|
68
|
+
{formatElapsed(elapsed)}
|
|
69
|
+
</span>
|
|
70
|
+
</div>
|
|
71
|
+
<div className="audit-progress-strip__pulse" aria-hidden="true" />
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// kind === "failed"
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
className="audit-progress-strip audit-progress-strip--failed"
|
|
80
|
+
role="alert"
|
|
81
|
+
>
|
|
82
|
+
<div className="audit-progress-strip__inner">
|
|
83
|
+
<span className="audit-progress-strip__label">
|
|
84
|
+
<span className="audit-progress-strip__x" aria-hidden="true">×</span>
|
|
85
|
+
{ERROR_COPY[status.reason]}
|
|
86
|
+
</span>
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
className="audit-progress-strip__dismiss"
|
|
90
|
+
onClick={onDismiss}
|
|
91
|
+
aria-label="dismiss"
|
|
92
|
+
>
|
|
93
|
+
[ dismiss ]
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -63,7 +63,10 @@ export async function triggerRun(scanParams: ScanParams): Promise<void> {
|
|
|
63
63
|
throw new RerunError(isAbortError(err) ? "timeout" : "network", "audit run request failed");
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
// Poll status until running flips false.
|
|
66
|
+
// Poll status until running flips false. The status route returns
|
|
67
|
+
// `cachedAt`, which the polling caller can compare against the prior
|
|
68
|
+
// value to detect "fresh result available" — but for `triggerRun`'s
|
|
69
|
+
// purposes the `running: false` flip is the success signal.
|
|
67
70
|
const startedAt = Date.now();
|
|
68
71
|
while (Date.now() - startedAt < MAX_POLL_MS) {
|
|
69
72
|
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
@@ -25,10 +25,14 @@ import type { AuditResult } from "@/src/audit/types";
|
|
|
25
25
|
import { usePostHog } from "@/contexts/PostHogContext";
|
|
26
26
|
import { isAbortError } from "@/lib/fetch-with-timeout";
|
|
27
27
|
import { AuthDialog, type AuthedUser } from "./auth-dialog";
|
|
28
|
-
import { RerunError, triggerRun } from "./rerun-button";
|
|
29
28
|
|
|
30
29
|
interface Props {
|
|
31
30
|
result: AuditResult;
|
|
31
|
+
/** Lifted by AuditDashboard so the bottom button and the top-of-page
|
|
32
|
+
* re-audit bar share state — only one rerun at a time. */
|
|
33
|
+
isRunning: boolean;
|
|
34
|
+
/** Lifted handler. Drives the sticky progress strip + soft refresh. */
|
|
35
|
+
onRerun: () => void;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
const BULK_INSTALL_CMD = "failproofai policies --install";
|
|
@@ -59,7 +63,7 @@ function formatNextAudit(unixSecs: number): string {
|
|
|
59
63
|
});
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
export function ReturnSection({ result }: Props) {
|
|
66
|
+
export function ReturnSection({ result, isRunning, onRerun }: Props) {
|
|
63
67
|
const { capture } = usePostHog();
|
|
64
68
|
const hasUnenabled = result.results.some(
|
|
65
69
|
(r) => r.source === "builtin" && !r.enabledInConfig && r.hits > 0,
|
|
@@ -70,7 +74,6 @@ export function ReturnSection({ result }: Props) {
|
|
|
70
74
|
const [reminder, setReminder] = useState<Reminder | null>(null);
|
|
71
75
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
72
76
|
const [reminderBusy, setReminderBusy] = useState(false);
|
|
73
|
-
const [rerunBusy, setRerunBusy] = useState(false);
|
|
74
77
|
const ctaShownRef = useRef(false);
|
|
75
78
|
/** Throttle gate for focus/visibility-triggered refreshStatus calls —
|
|
76
79
|
* rapid alt-tabbing would otherwise hit /api/auth/status (two disk
|
|
@@ -233,30 +236,15 @@ export function ReturnSection({ result }: Props) {
|
|
|
233
236
|
[capture, persistReminder],
|
|
234
237
|
);
|
|
235
238
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
// Reload the page after the run so the cached result + dashboard cache
|
|
246
|
-
// get re-hydrated against the new scan. Cheaper than threading state.
|
|
247
|
-
window.location.reload();
|
|
248
|
-
} catch (err) {
|
|
249
|
-
const kind = err instanceof RerunError ? err.kind : "network";
|
|
250
|
-
capture("audit_rerun_failed", {
|
|
251
|
-
kind,
|
|
252
|
-
source: "return_section",
|
|
253
|
-
since: "30d",
|
|
254
|
-
cli_filter: "all",
|
|
255
|
-
});
|
|
256
|
-
} finally {
|
|
257
|
-
setRerunBusy(false);
|
|
258
|
-
}
|
|
259
|
-
}, [capture, rerunBusy]);
|
|
239
|
+
// Re-audit is now lifted into AuditDashboard so the top-of-page bar and
|
|
240
|
+
// this bottom button share state — concurrent clicks are impossible and
|
|
241
|
+
// success path soft-refreshes the report rather than reloading the page.
|
|
242
|
+
// PostHog `source: "return_section"` capture happens inside the lifted
|
|
243
|
+
// handler via the `source` argument it receives.
|
|
244
|
+
const handleRerun = useCallback(() => {
|
|
245
|
+
if (isRunning) return;
|
|
246
|
+
onRerun();
|
|
247
|
+
}, [isRunning, onRerun]);
|
|
260
248
|
|
|
261
249
|
const authed = authStatus.kind === "authed";
|
|
262
250
|
const hasReminder = authed && reminder !== null;
|
|
@@ -328,7 +316,7 @@ export function ReturnSection({ result }: Props) {
|
|
|
328
316
|
showSetReminder={!hasReminder}
|
|
329
317
|
setReminderDisabled={reminderBusy}
|
|
330
318
|
reminderBusy={reminderBusy}
|
|
331
|
-
rerunBusy={
|
|
319
|
+
rerunBusy={isRunning}
|
|
332
320
|
hasUnenabled={hasUnenabled}
|
|
333
321
|
copied={copied}
|
|
334
322
|
onSetReminder={handleSetReminder}
|
|
@@ -341,7 +329,7 @@ export function ReturnSection({ result }: Props) {
|
|
|
341
329
|
showSetReminder
|
|
342
330
|
setReminderDisabled={authStatus.kind === "unknown" || reminderBusy}
|
|
343
331
|
reminderBusy={reminderBusy}
|
|
344
|
-
rerunBusy={
|
|
332
|
+
rerunBusy={isRunning}
|
|
345
333
|
hasUnenabled={hasUnenabled}
|
|
346
334
|
copied={copied}
|
|
347
335
|
onSetReminder={handleSetReminder}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Top-of-page re-audit affordance. Renders as a thin strip directly above
|
|
5
|
+
* the IdentitySection — the first thing the eye lands on after the page
|
|
6
|
+
* background. Mirrors `.arch-mast`'s dashed-bottom-border vocabulary so
|
|
7
|
+
* the bar reads as a sibling band rather than a new component family.
|
|
8
|
+
*
|
|
9
|
+
* Three rendered states:
|
|
10
|
+
*
|
|
11
|
+
* - "cached" — shows `audited 3d ago` + a green pulse if fresh, an
|
|
12
|
+
* amber `expires in 14h — re-audit to refresh` chip when
|
|
13
|
+
* inside the last 24h of the 7-day window, and the
|
|
14
|
+
* `[ re-audit ]` action on the right.
|
|
15
|
+
* - "expired" — collapses to a single banner: `your audit expired —
|
|
16
|
+
* re-audit to refresh`, same action on the right.
|
|
17
|
+
* - "empty" — `no audit yet`, action on the right. Mirrors the
|
|
18
|
+
* existing empty-state CTA so the page never reads as
|
|
19
|
+
* dead at first glance.
|
|
20
|
+
*
|
|
21
|
+
* Action button reuses `.share-btn` for visual continuity with the
|
|
22
|
+
* existing `[ re-audit now ]` at the bottom of the report (return-section).
|
|
23
|
+
*/
|
|
24
|
+
import React, { useEffect, useState } from "react";
|
|
25
|
+
|
|
26
|
+
const HOUR_MS = 60 * 60_000;
|
|
27
|
+
const DAY_MS = 24 * HOUR_MS;
|
|
28
|
+
const TTL_MS = 7 * DAY_MS;
|
|
29
|
+
const EXPIRY_NOTE_THRESHOLD_MS = DAY_MS;
|
|
30
|
+
|
|
31
|
+
export type TopAuditBarMode = "cached" | "expired" | "empty";
|
|
32
|
+
|
|
33
|
+
export interface TopAuditBarProps {
|
|
34
|
+
mode: TopAuditBarMode;
|
|
35
|
+
/** ISO timestamp from the dashboard cache (or the expired-meta probe).
|
|
36
|
+
* Required for "cached" and "expired"; ignored for "empty". */
|
|
37
|
+
cachedAt: string | null;
|
|
38
|
+
/** True while a re-audit is in flight. Disables the button and swaps
|
|
39
|
+
* the label to `[ scanning… ]`. */
|
|
40
|
+
isRunning: boolean;
|
|
41
|
+
onRerun: () => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function relativeTimeAgo(now: number, then: number): string {
|
|
45
|
+
const ageMs = Math.max(0, now - then);
|
|
46
|
+
if (ageMs < 90_000) return "just now";
|
|
47
|
+
if (ageMs < 60 * 60_000) return `${Math.round(ageMs / 60_000)}m ago`;
|
|
48
|
+
if (ageMs < DAY_MS) return `${Math.round(ageMs / HOUR_MS)}h ago`;
|
|
49
|
+
return `${Math.round(ageMs / DAY_MS)}d ago`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function timeUntilExpiry(now: number, cachedAtMs: number): string {
|
|
53
|
+
const remainingMs = Math.max(0, cachedAtMs + TTL_MS - now);
|
|
54
|
+
if (remainingMs < 60_000) return "<1m";
|
|
55
|
+
if (remainingMs < 60 * 60_000) return `${Math.round(remainingMs / 60_000)}m`;
|
|
56
|
+
if (remainingMs < DAY_MS) return `${Math.round(remainingMs / HOUR_MS)}h`;
|
|
57
|
+
return `${Math.round(remainingMs / DAY_MS)}d`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function TopAuditBar({ mode, cachedAt, isRunning, onRerun }: TopAuditBarProps) {
|
|
61
|
+
// Re-render once a minute so the relative-time string stays current
|
|
62
|
+
// without forcing a full page poll. Cheap — one state bump per minute.
|
|
63
|
+
const [now, setNow] = useState(() => Date.now());
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const t = setInterval(() => setNow(Date.now()), 60_000);
|
|
66
|
+
return () => clearInterval(t);
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
const cachedAtMs = cachedAt ? new Date(cachedAt).getTime() : null;
|
|
70
|
+
const validCachedAt = cachedAtMs !== null && !Number.isNaN(cachedAtMs);
|
|
71
|
+
const ageMs = validCachedAt ? now - cachedAtMs : null;
|
|
72
|
+
const remainingMs = validCachedAt ? cachedAtMs + TTL_MS - now : null;
|
|
73
|
+
const expiringSoon =
|
|
74
|
+
mode === "cached"
|
|
75
|
+
&& remainingMs !== null
|
|
76
|
+
&& remainingMs <= EXPIRY_NOTE_THRESHOLD_MS
|
|
77
|
+
&& remainingMs > 0;
|
|
78
|
+
|
|
79
|
+
const buttonLabel = isRunning ? "[ scanning… ]" : "[ re-audit ]";
|
|
80
|
+
|
|
81
|
+
// The bar is a single horizontal band: left = status, center = optional
|
|
82
|
+
// amber note (only when expiring soon), right = action button. On narrow
|
|
83
|
+
// viewports it wraps to two rows.
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
className={
|
|
87
|
+
"top-audit-bar"
|
|
88
|
+
+ (mode === "expired" ? " top-audit-bar--expired" : "")
|
|
89
|
+
+ (mode === "empty" ? " top-audit-bar--empty" : "")
|
|
90
|
+
}
|
|
91
|
+
role="region"
|
|
92
|
+
aria-label="audit status"
|
|
93
|
+
>
|
|
94
|
+
<div className="top-audit-bar__left">
|
|
95
|
+
{mode === "cached" && validCachedAt && ageMs !== null && (
|
|
96
|
+
<>
|
|
97
|
+
<span className="top-audit-bar__label">
|
|
98
|
+
<span className="top-audit-bar__glyph">━━</span> last audit
|
|
99
|
+
</span>
|
|
100
|
+
<span
|
|
101
|
+
className={
|
|
102
|
+
"top-audit-bar__time"
|
|
103
|
+
+ (ageMs < HOUR_MS ? " top-audit-bar__time--fresh" : "")
|
|
104
|
+
}
|
|
105
|
+
>
|
|
106
|
+
<span className="top-audit-bar__dot" aria-hidden="true" />
|
|
107
|
+
audited {relativeTimeAgo(now, cachedAtMs!)}
|
|
108
|
+
</span>
|
|
109
|
+
</>
|
|
110
|
+
)}
|
|
111
|
+
{mode === "expired" && (
|
|
112
|
+
<>
|
|
113
|
+
<span className="top-audit-bar__label top-audit-bar__label--warn">
|
|
114
|
+
<span className="top-audit-bar__glyph">━━</span> audit expired
|
|
115
|
+
</span>
|
|
116
|
+
<span className="top-audit-bar__expired-copy">
|
|
117
|
+
your audit aged past 7d — re-audit to refresh
|
|
118
|
+
</span>
|
|
119
|
+
</>
|
|
120
|
+
)}
|
|
121
|
+
{mode === "empty" && (
|
|
122
|
+
<>
|
|
123
|
+
<span className="top-audit-bar__label">
|
|
124
|
+
<span className="top-audit-bar__glyph">━━</span> no audit yet
|
|
125
|
+
</span>
|
|
126
|
+
<span className="top-audit-bar__time">
|
|
127
|
+
run one to see your archetype
|
|
128
|
+
</span>
|
|
129
|
+
</>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{expiringSoon && remainingMs !== null && (
|
|
134
|
+
<div className="top-audit-bar__center" aria-live="polite">
|
|
135
|
+
<span className="top-audit-bar__amber-dot" aria-hidden="true" />
|
|
136
|
+
expires in {timeUntilExpiry(now, cachedAtMs!)} — re-audit to refresh
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
<div className="top-audit-bar__right">
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
className="share-btn top-audit-bar__btn"
|
|
144
|
+
onClick={onRerun}
|
|
145
|
+
disabled={isRunning}
|
|
146
|
+
data-state={isRunning ? "scanning" : "idle"}
|
|
147
|
+
>
|
|
148
|
+
{buttonLabel}
|
|
149
|
+
</button>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|