MAESTRO: integrate browser cache UI

This commit is contained in:
Mariusz Banach
2026-02-18 03:54:48 +01:00
parent c039a8a432
commit 7d4a0d4abe
2 changed files with 121 additions and 16 deletions

View File

@@ -1,15 +1,18 @@
"use client";
import { useCallback, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrash } from "@fortawesome/free-solid-svg-icons";
import AnalyseButton from "../components/AnalyseButton";
import AnalysisResults from "../components/AnalysisResults";
import FileDropZone from "../components/FileDropZone";
import HeaderInput from "../components/HeaderInput";
import ProgressIndicator from "../components/ProgressIndicator";
import ReportContainer from "../components/report/ReportContainer";
import useAnalysis from "../hooks/useAnalysis";
import useAnalysisCache from "../hooks/useAnalysisCache";
import { MAX_HEADER_INPUT_BYTES } from "../lib/header-validation";
import type { AnalysisConfig } from "../types/analysis";
import type { AnalysisConfig, AnalysisReport } from "../types/analysis";
const defaultConfig: AnalysisConfig = {
testIds: [],
@@ -18,22 +21,78 @@ const defaultConfig: AnalysisConfig = {
};
export default function Home() {
const [headerInput, setHeaderInput] = useState("");
const { status, progress, result, submit } = useAnalysis();
const { status, progress, result, submit, cancel } = useAnalysis();
const { save, load, clear, isNearLimit } = useAnalysisCache();
const initialCache = useMemo(() => load(), [load]);
const [headerInput, setHeaderInput] = useState(() => initialCache?.headers ?? "");
const [analysisConfig, setAnalysisConfig] = useState<AnalysisConfig>(
() => initialCache?.config ?? defaultConfig,
);
const [cachedReport, setCachedReport] = useState<AnalysisReport | null>(
() => initialCache?.result ?? null,
);
const [cachedTimestamp, setCachedTimestamp] = useState<number | null>(
() => initialCache?.timestamp ?? null,
);
const [isViewCleared, setIsViewCleared] = useState(false);
const lastSubmissionRef = useRef<{ headers: string; config: AnalysisConfig } | null>(null);
useEffect(() => {
if (!result) {
return;
}
const payload = lastSubmissionRef.current ?? {
headers: headerInput,
config: analysisConfig,
};
save({ headers: payload.headers, config: payload.config, result });
}, [analysisConfig, headerInput, result, save]);
const hasHeaderInput = headerInput.trim().length > 0;
const isOversized = headerInput.length > MAX_HEADER_INPUT_BYTES;
const canAnalyse = hasHeaderInput && !isOversized;
const isLoading = status === "submitting" || status === "analysing";
const showProgress = status === "analysing" || status === "timeout";
const incompleteTests = result?.metadata.incompleteTests ?? [];
const allowCachedFallback =
status === "idle" || status === "complete" || status === "error" || status === "timeout";
const report = isViewCleared ? null : (result ?? (allowCachedFallback ? cachedReport : null));
const isCachedView = Boolean(!isViewCleared && !result && allowCachedFallback && cachedReport);
const hasCache = Boolean(result || cachedReport);
const cacheTimestampLabel = useMemo(() => {
if (!cachedTimestamp) {
return null;
}
return new Date(cachedTimestamp).toLocaleString("en-US", {
dateStyle: "medium",
timeStyle: "short",
});
}, [cachedTimestamp]);
const handleAnalyse = useCallback(() => {
if (!canAnalyse) {
return;
}
void submit({ headers: headerInput, config: defaultConfig });
}, [canAnalyse, headerInput, submit]);
const payload = { headers: headerInput, config: analysisConfig };
lastSubmissionRef.current = payload;
setIsViewCleared(false);
void submit(payload);
}, [analysisConfig, canAnalyse, headerInput, submit]);
const handleClearCache = useCallback(() => {
clear();
cancel();
setHeaderInput("");
setAnalysisConfig(defaultConfig);
setCachedReport(null);
setCachedTimestamp(null);
setIsViewCleared(true);
lastSubmissionRef.current = null;
}, [cancel, clear]);
return (
<main className="min-h-screen bg-background text-text">
@@ -90,10 +149,53 @@ export default function Home() {
incompleteTests={incompleteTests}
/>
) : null}
<div className="rounded-2xl border border-info/10 bg-surface p-5">
<div className="flex flex-wrap items-center justify-between gap-3">
<p className="text-xs uppercase tracking-[0.2em] text-info/80">Browser Cache</p>
<button
type="button"
className="inline-flex items-center gap-2 rounded-full border border-info/20 bg-background/40 px-4 py-2 text-[11px] font-semibold uppercase tracking-[0.2em] text-text/70 transition hover:border-info/40 hover:text-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-info disabled:cursor-not-allowed disabled:opacity-50"
onClick={handleClearCache}
disabled={!hasCache}
>
<FontAwesomeIcon icon={faTrash} className="text-xs" />
Clear Cache
</button>
</div>
<p className="mt-2 text-xs text-text/60">
{result
? "Latest analysis cached for this session."
: hasCache
? `Cached analysis saved ${cacheTimestampLabel ?? "recently"}.`
: "No cached analysis yet. Run an analysis to save this session."}
</p>
{isNearLimit ? (
<p className="mt-2 text-xs text-suspicious">
Local storage is nearly full. Consider clearing cached data.
</p>
) : null}
</div>
</div>
</section>
{result ? <AnalysisResults report={result} /> : null}
{report ? (
<section className="flex flex-col gap-3">
{isCachedView ? (
<div className="flex flex-wrap items-center gap-2 text-[11px] text-text/60">
<span className="rounded-full border border-info/20 bg-background/40 px-3 py-1 uppercase tracking-[0.2em] text-info/70">
Cached Result
</span>
{cacheTimestampLabel ? (
<span>Saved {cacheTimestampLabel}</span>
) : (
<span>Saved recently</span>
)}
</div>
) : null}
<ReportContainer report={report} />
</section>
) : null}
</div>
</div>
</main>