From c491af5d02f8e165499aa4b7a422298424f8d4a6 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 21 May 2026 16:47:42 +0200 Subject: [PATCH] =?UTF-8?q?feat(audit):=20P47=20localStorage-Quota=20?= =?UTF-8?q?=E2=80=94=20safeSetItem=20mit=20Auto-Prune?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit storageHelpers.ts: safeSetItem faengt QuotaExceededError, prunet alte doc-check-result-*-Eintraege (oldest first, MAX_KEEP=10) und retried. Bei zweitem Fail aggressiver pruefen. DocCheckTab.tsx nutzt safeSetItem statt setItem fuer doc-check-results, result-Keys und history. Verhindert silent-data-loss + Crash wenn ~5MB localStorage-Limit erreicht. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../app/sdk/agent/_components/DocCheckTab.tsx | 7 +- .../sdk/agent/_components/storageHelpers.ts | 71 +++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 admin-compliance/app/sdk/agent/_components/storageHelpers.ts diff --git a/admin-compliance/app/sdk/agent/_components/DocCheckTab.tsx b/admin-compliance/app/sdk/agent/_components/DocCheckTab.tsx index f08bf588..4df9a9f7 100644 --- a/admin-compliance/app/sdk/agent/_components/DocCheckTab.tsx +++ b/admin-compliance/app/sdk/agent/_components/DocCheckTab.tsx @@ -3,6 +3,7 @@ import React, { useState } from 'react' import { ChecklistView } from './ChecklistView' import { PreScanWizard, useScanContext, isContextComplete } from './PreScanWizard' +import { safeSetItem } from './storageHelpers' interface DocEntry { id: string @@ -118,13 +119,13 @@ export function DocCheckTab() { if (pollData.status === 'completed' && pollData.result) { setResults(pollData.result) setProgress('') - localStorage.setItem('doc-check-results', JSON.stringify(pollData.result)) + safeSetItem('doc-check-results', JSON.stringify(pollData.result)) const resultKey = `doc-check-result-${Date.now()}` - try { localStorage.setItem(resultKey, JSON.stringify(pollData.result)) } catch { /* quota */ } + safeSetItem(resultKey, JSON.stringify(pollData.result)) const entry = { date: new Date().toISOString(), urls: validEntries.length, findings: pollData.result.total_findings || 0, resultKey } const updated = [entry, ...history].slice(0, 30) setHistory(updated) - localStorage.setItem('doc-check-history', JSON.stringify(updated)) + safeSetItem('doc-check-history', JSON.stringify(updated)) break } if (pollData.status === 'failed') { diff --git a/admin-compliance/app/sdk/agent/_components/storageHelpers.ts b/admin-compliance/app/sdk/agent/_components/storageHelpers.ts new file mode 100644 index 00000000..f4e83098 --- /dev/null +++ b/admin-compliance/app/sdk/agent/_components/storageHelpers.ts @@ -0,0 +1,71 @@ +/** + * P47 — localStorage-Quota-Management. + * + * Wenn alte Compliance-Check-Ergebnisse den Browser-Storage fuellen, + * versucht das setItem mit QuotaExceededError zu fangen, prunet + * alte doc-check-result-*-Eintraege (oldest first) und retried. + * + * Wird von DocCheckTab/BannerCheckTab/etc beim Persistieren der + * Result-Bloebs benutzt. + */ + +const RESULT_KEY_PREFIX = 'doc-check-result-' +const MAX_KEEP = 10 // Maximal 10 alte Result-Bloebs behalten. + +export function safeSetItem(key: string, value: string): boolean { + try { + localStorage.setItem(key, value) + return true + } catch (err: any) { + if (err?.name !== 'QuotaExceededError' + && err?.code !== 22 && err?.code !== 1014) { + console.warn('localStorage setItem failed:', err) + return false + } + pruneOldResults() + try { + localStorage.setItem(key, value) + return true + } catch { + // Pruning hat nicht gereicht — aggressiver pruefen + pruneOldResults(0) + try { + localStorage.setItem(key, value) + return true + } catch { + console.warn('localStorage immer noch voll, wert wird verworfen') + return false + } + } + } +} + +function pruneOldResults(keep: number = MAX_KEEP): void { + try { + const keys: { key: string; ts: number }[] = [] + for (let i = 0; i < localStorage.length; i++) { + const k = localStorage.key(i) + if (!k || !k.startsWith(RESULT_KEY_PREFIX)) continue + const ts = Number(k.slice(RESULT_KEY_PREFIX.length)) || 0 + keys.push({ key: k, ts }) + } + keys.sort((a, b) => a.ts - b.ts) // oldest first + const toRemove = keys.slice(0, Math.max(0, keys.length - keep)) + for (const k of toRemove) { + try { localStorage.removeItem(k.key) } catch {} + } + } catch {} +} + +export function getStorageUsageMB(): number { + let bytes = 0 + try { + for (let i = 0; i < localStorage.length; i++) { + const k = localStorage.key(i) + if (!k) continue + const v = localStorage.getItem(k) || '' + bytes += k.length + v.length + } + } catch {} + return bytes / (1024 * 1024) +}