Rewrite reviewed-finding localStorage hooks.

Dashboard count and report toggle still migrate legacy keys and skip the first persist write.
This commit is contained in:
2026-09-20 20:12:20 +02:00
parent 7d82358e19
commit 72b4b20665
3 changed files with 145 additions and 0 deletions
@@ -0,0 +1,33 @@
"use client";
import { useEffect, useState } from "react";
import {
legacyReviewedFindingsKey,
migrateLocalKey,
reviewedFindingsKey,
} from "@/lib/storage-keys";
/** Dashboard-only: length of the reviewed-finding id list in localStorage. */
export function useReviewedCount(projectId: string): number {
const [count, setCount] = useState(0);
useEffect(() => {
try {
const raw = migrateLocalKey(
reviewedFindingsKey(projectId),
legacyReviewedFindingsKey(projectId),
);
if (!raw) {
setCount(0);
return;
}
const parsed: unknown = JSON.parse(raw);
setCount(Array.isArray(parsed) ? parsed.length : 0);
} catch (err) {
console.warn("useReviewedCount: could not read reviewed ids", err);
setCount(0);
}
}, [projectId]);
return count;
}
@@ -0,0 +1,79 @@
"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { Finding } from "@/lib/types";
import { getFindingKey } from "@/lib/utils";
import {
legacyReviewedFindingsKey,
migrateLocalKey,
reviewedFindingsKey,
} from "@/lib/storage-keys";
function readStoredIds(projectId: string): Set<string> {
try {
const raw = migrateLocalKey(
reviewedFindingsKey(projectId),
legacyReviewedFindingsKey(projectId),
);
if (!raw) return new Set();
const parsed: unknown = JSON.parse(raw);
if (!Array.isArray(parsed)) return new Set();
return new Set(parsed.filter((id): id is string => typeof id === "string"));
} catch (err) {
console.warn("useReviewedFindings: could not read reviewed ids", err);
return new Set();
}
}
export function useReviewedFindings(projectId: string, findings: Finding[]) {
const liveKeys = useMemo(() => {
const keys = new Set<string>();
findings.forEach((finding, index) => keys.add(getFindingKey(finding, index)));
return keys;
}, [findings]);
const [reviewedIds, setReviewedIds] = useState<Set<string>>(() => {
if (typeof window === "undefined") return new Set();
return readStoredIds(projectId);
});
const skipFirstWrite = useRef(true);
useEffect(() => {
if (skipFirstWrite.current) {
skipFirstWrite.current = false;
return;
}
localStorage.setItem(reviewedFindingsKey(projectId), JSON.stringify([...reviewedIds]));
}, [projectId, reviewedIds]);
useEffect(() => {
if (findings.length === 0) return;
setReviewedIds((prev) => {
let dropped = false;
const next = new Set<string>();
for (const key of prev) {
if (liveKeys.has(key)) next.add(key);
else dropped = true;
}
return dropped ? next : prev;
});
}, [findings.length, liveKeys]);
const toggleReviewed = useCallback((key: string) => {
setReviewedIds((prev) => {
const next = new Set(prev);
if (next.has(key)) next.delete(key);
else next.add(key);
return next;
});
}, []);
const isReviewed = useCallback((key: string) => reviewedIds.has(key), [reviewedIds]);
return {
reviewedIds,
toggleReviewed,
isReviewed,
reviewedCount: reviewedIds.size,
};
}
@@ -0,0 +1,33 @@
"""Reviewed-finding localStorage hooks live under periscope/src."""
from __future__ import annotations
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "periscope" / "src" / "frontend" / "src"
def test_reviewed_hooks_are_src():
for rel in ("hooks/use-reviewed-count.ts", "hooks/use-reviewed-findings.ts"):
text = (SRC / rel).read_text(encoding="utf-8")
assert "Native Periscope overlay" not in text[:400]
def test_reviewed_count_reads_migrated_key():
text = (SRC / "hooks/use-reviewed-count.ts").read_text(encoding="utf-8")
assert "export function useReviewedCount" in text
assert "reviewedFindingsKey" in text
assert "legacyReviewedFindingsKey" in text
assert "migrateLocalKey" in text
def test_reviewed_findings_return_shape():
text = (SRC / "hooks/use-reviewed-findings.ts").read_text(encoding="utf-8")
assert "export function useReviewedFindings" in text
assert "toggleReviewed" in text
assert "isReviewed" in text
assert "reviewedCount" in text
assert "reviewedIds" in text
assert "getFindingKey" in text
assert "skipFirstWrite" in text or "hydrated" in text