From 21e08821e32c18b54ec89ecc8b580ebfbb0d5a33 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:59:02 +0200 Subject: [PATCH] fix: clarify desktop io recovery --- README.md | 16 ++-- components/StorageErrorScreen.tsx | 56 +++++++++++--- locales/ar/common.json | 2 + locales/de/common.json | 2 + locales/el/common.json | 2 + locales/en/common.json | 2 + locales/es/common.json | 2 + locales/eu/common.json | 2 + locales/fa/common.json | 2 + locales/fi/common.json | 2 + locales/fr/common.json | 2 + locales/he/common.json | 2 + locales/hu/common.json | 2 + locales/is/common.json | 2 + locales/it/common.json | 2 + locales/ja/common.json | 2 + locales/ko/common.json | 2 + locales/pt/common.json | 2 + locales/ru/common.json | 2 + locales/sv/common.json | 2 + locales/zh/common.json | 2 + public/locales/ar/bundle.json | 2 + public/locales/de/bundle.json | 2 + public/locales/el/bundle.json | 2 + public/locales/en/bundle.json | 2 + public/locales/es/bundle.json | 2 + public/locales/eu/bundle.json | 2 + public/locales/fa/bundle.json | 2 + public/locales/fi/bundle.json | 2 + public/locales/fr/bundle.json | 2 + public/locales/he/bundle.json | 2 + public/locales/hu/bundle.json | 2 + public/locales/is/bundle.json | 2 + public/locales/it/bundle.json | 2 + public/locales/ja/bundle.json | 2 + public/locales/ko/bundle.json | 2 + public/locales/pt/bundle.json | 2 + public/locales/ru/bundle.json | 2 + public/locales/sv/bundle.json | 2 + public/locales/zh/bundle.json | 2 + services/startupRecovery.tsx | 5 +- services/startupRecoveryPolicy.ts | 22 +++++- tests/unit/StorageErrorScreen.test.tsx | 97 ++++++++++++++++++++---- tests/unit/startupRecovery.test.tsx | 11 +++ tests/unit/startupRecoveryPolicy.test.ts | 15 ++++ 45 files changed, 259 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 51988a5e..0548cc5e 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ Release v1.28.3 IndexedDB v8 PWA v3.0 - i18n 19 locales — 2937 keys - 7317+ tests / 594 files + i18n 19 locales — 2939 keys + 7320+ tests / 594 files Codecov Coverage License MIT CI Status @@ -401,7 +401,7 @@ Infrastructure-level features that keep the app fast and extensible as projects ### 🌐 Full Multi-Language Support -Shipped UI locales with **2937 i18n keys** across all 19 languages — zero hardcoded user-facing strings: +Shipped UI locales with **2939 i18n keys** across all 19 languages — zero hardcoded user-facing strings: - 🇩🇪 **German** (Deutsch) - 🇬🇧 **English** @@ -510,8 +510,8 @@ The Settings → AI panel shows a live GPU status badge with adapter details and | **PDF Export** | jsPDF | Client-side, configurable PDF document generation | | **Document Export** | docx + jszip | Word-compatible `.docx` generation (lazy-loaded) | | **PWA** | Service Worker + Web App Manifest v3 | Offline support, installability, Workbox chunking | -| **i18n** | Custom React Context (`I18nContext.tsx`) | 2937 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence | -| **Testing** | Vitest 4.x (7317+ tests / 594 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | +| **i18n** | Custom React Context (`I18nContext.tsx`) | 2939 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence | +| **Testing** | Vitest 4.x (7320+ tests / 594 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | | **Code Quality** | Biome (lint + format) + TypeScript 7 (tsgo) strict | `--error-on-warnings` in CI; zero `any` policy | | **Visualization** | Force-directed graph | Interactive character relationship network | | **Desktop** | Tauri v2 | Cross-platform installer; auto-updater via `latest.json` | @@ -549,7 +549,7 @@ WorldScript-Studio/ │ ├── sw.js # PWA Service Worker │ └── manifest.json # PWA Web App Manifest v3 ├── tests/ -│ ├── unit/ # Vitest unit tests (7317+ tests, 594 files) — count spans tests/, components/, packages/*/tests/, not just this folder +│ ├── unit/ # Vitest unit tests (7320+ tests, 594 files) — count spans tests/, components/, packages/*/tests/, not just this folder │ │ ├── ai/ # aiSmallModules, aiCoreFallbackPaths │ │ └── settings/ # WebLlmPanel, AiSections │ └── e2e/ # Playwright specs + helpers.ts @@ -711,9 +711,9 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt | `scorecard` | weekly + `main` push | OpenSSF Scorecard — SARIF uploaded to GitHub Code Scanning | **Current test metrics (2026-08-30, source-synchronized; CI remains authoritative for pass/fail):** -- **7317+ unit tests** across **594 test files** — CI is authoritative for pass/fail +- **7320+ unit tests** across **594 test files** — CI is authoritative for pass/fail - Coverage thresholds: lines ≥ 80 · branches ≥ 66 · functions ≥ 72 · statements ≥ 78 — enforced in CI (see Codecov badge for live metrics) -- i18n: **2937 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta) +- i18n: **2939 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta) **CI-cloud-first workflow (recommended):** On constrained hardware run **`pnpm run lint && pnpm run i18n:check && pnpm run typecheck`** locally, then push and let CI handle coverage, E2E, Lighthouse, and Stryker. Authoritative numbers come from CI artifacts (Codecov, JUnit). After CI goes green, update the README badges and `AUDIT.md` quality-gate line from the reported metrics. See **[`docs/CI.md`](docs/CI.md) § Cloud CI-first vs local development** for the full post-merge doc-update checklist. diff --git a/components/StorageErrorScreen.tsx b/components/StorageErrorScreen.tsx index c398c9b8..3e30e324 100644 --- a/components/StorageErrorScreen.tsx +++ b/components/StorageErrorScreen.tsx @@ -2,12 +2,15 @@ import React from 'react'; import { ProjectQuarantineError } from '../services/fs/projectFsStore'; import { getStaticTranslation } from '../services/i18n/staticTranslate'; import { logger } from '../services/logger'; +import type { StartupRecoveryFailureKind } from '../services/startupRecoveryPolicy'; export interface StorageErrorCopy { description: string; storageUnavailable: string; projectUnavailable: string; + projectIoUnavailable: string; reload: string; + retry: string; recover: string; recovering: string; reset: string; @@ -22,7 +25,10 @@ export const STARTUP_COPY_FALLBACKS: StorageErrorCopy = { description: 'The local project or database could not be opened. Reload and try again.', storageUnavailable: 'Local storage could not be opened. Reload and try again.', projectUnavailable: 'A local project could not be opened. Reload and try again.', + projectIoUnavailable: + 'The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.', reload: 'Reload', + retry: 'Retry', recover: 'Quarantine project and reload', recovering: 'Preserving project…', reset: 'Reset database and reload', @@ -47,7 +53,9 @@ export async function loadStorageErrorCopy(): Promise { description, storageUnavailable, projectUnavailable, + projectIoUnavailable, reload, + retry, recover, recovering, reset, @@ -66,7 +74,12 @@ export async function loadStorageErrorCopy(): Promise { 'error.startup.projectUnavailable', STARTUP_COPY_FALLBACKS.projectUnavailable, ), + startupTranslation( + 'error.startup.projectIoUnavailable', + STARTUP_COPY_FALLBACKS.projectIoUnavailable, + ), startupTranslation('error.startup.reload', STARTUP_COPY_FALLBACKS.reload), + startupTranslation('error.startup.retry', STARTUP_COPY_FALLBACKS.retry), startupTranslation('error.startup.recover', STARTUP_COPY_FALLBACKS.recover), startupTranslation('error.startup.recovering', STARTUP_COPY_FALLBACKS.recovering), startupTranslation('error.startup.reset', STARTUP_COPY_FALLBACKS.reset), @@ -83,7 +96,9 @@ export async function loadStorageErrorCopy(): Promise { description, storageUnavailable, projectUnavailable, + projectIoUnavailable, reload, + retry, recover, recovering, reset, @@ -95,23 +110,34 @@ export async function loadStorageErrorCopy(): Promise { }; } -// QNBS-v3: show stable localized recovery actions while keeping technical filesystem diagnostics in the logger. +// QNBS-v3: use explicit failure kinds so I/O retry, quarantine, and database reset cannot be inferred from callbacks. export function StorageErrorScreen({ copy, onReset, onRecover, + onRetry, + failureKind, }: { copy: StorageErrorCopy; + failureKind: StartupRecoveryFailureKind; onReset?: () => void; onRecover?: () => Promise; + onRetry?: () => void; }) { const [recoveryStatus, setRecoveryStatus] = React.useState< 'failed' | 'already-preserved' | 'source-missing' | null >(null); const [isRecovering, setIsRecovering] = React.useState(false); + const isProjectCorrupt = failureKind === 'project-corrupt'; + const isProjectIo = failureKind === 'project-io'; + const failureMessage = isProjectCorrupt + ? copy.projectUnavailable + : isProjectIo + ? copy.projectIoUnavailable + : copy.storageUnavailable; const handleRecover = async () => { - if (!onRecover) return; + if (!onRecover || !isProjectCorrupt) return; setRecoveryStatus(null); setIsRecovering(true); try { @@ -132,6 +158,14 @@ export function StorageErrorScreen({ } }; + const handleRetry = () => { + if (onRetry) { + onRetry(); + return; + } + window.location.reload(); + }; + return (
- {onRecover ? copy.projectUnavailable : copy.storageUnavailable} + {failureMessage}

- {onRecover && ( + {isProjectCorrupt && onRecover && ( )} - {onReset && ( + {failureKind === 'storage' && onReset && ( )}
- {onRecover && ( + {isProjectCorrupt && onRecover && (

{copy.quarantineNotice}

)} - {onRecover && ( + {isProjectCorrupt && onRecover && (

)} - {onReset &&

{copy.resetWarning}

} + {failureKind === 'storage' && onReset && ( +

{copy.resetWarning}

+ )}
); } diff --git a/locales/ar/common.json b/locales/ar/common.json index 3658decd..af538d74 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -699,7 +699,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/locales/de/common.json b/locales/de/common.json index 3331dbbf..8e3963cc 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -699,7 +699,9 @@ "error.startup.description": "Das lokale Projekt oder die lokale Datenbank konnte nicht geöffnet werden. Laden Sie die Anwendung neu und versuchen Sie es erneut.", "error.startup.storageUnavailable": "Der lokale Speicher konnte nicht geöffnet werden. Laden Sie die Anwendung neu und versuchen Sie es erneut.", "error.startup.projectUnavailable": "Ein lokales Projekt konnte nicht geöffnet werden. Laden Sie die Anwendung neu und versuchen Sie es erneut.", + "error.startup.projectIoUnavailable": "Das Projekt konnte derzeit aufgrund eines Speicher- oder Dateizugriffsproblems nicht gelesen werden. Es wurde nicht als beschädigt eingestuft und nicht verändert. Prüfen Sie die Verfügbarkeit des Speichers und die Dateiberechtigungen und versuchen Sie es erneut.", "error.startup.reload": "Neu laden", + "error.startup.retry": "Erneut versuchen", "error.startup.recover": "Projekt unter Quarantäne stellen und neu laden", "error.startup.recovering": "Projekt wird gesichert …", "error.startup.reset": "Datenbank zurücksetzen und neu laden", diff --git a/locales/el/common.json b/locales/el/common.json index e5dbcb63..9691e6d4 100644 --- a/locales/el/common.json +++ b/locales/el/common.json @@ -699,7 +699,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/locales/en/common.json b/locales/en/common.json index 9494cd40..c2237d54 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -699,7 +699,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/locales/es/common.json b/locales/es/common.json index 41117663..dd154c60 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -699,7 +699,9 @@ "error.startup.description": "No se pudo abrir el proyecto local o la base de datos. Recarga la aplicación e inténtalo de nuevo.", "error.startup.storageUnavailable": "No se pudo abrir el almacenamiento local. Recarga la aplicación e inténtalo de nuevo.", "error.startup.projectUnavailable": "No se pudo abrir un proyecto local. Recarga la aplicación e inténtalo de nuevo.", + "error.startup.projectIoUnavailable": "No se pudo leer el proyecto en este momento debido a un problema de almacenamiento o acceso a los archivos. No se ha clasificado como dañado ni se ha modificado. Comprueba la disponibilidad del dispositivo de almacenamiento y los permisos de los archivos, y vuelve a intentarlo.", "error.startup.reload": "Recargar", + "error.startup.retry": "Reintentar", "error.startup.recover": "Poner el proyecto en cuarentena y recargar", "error.startup.recovering": "Preservando el proyecto…", "error.startup.reset": "Restablecer la base de datos y recargar", diff --git a/locales/eu/common.json b/locales/eu/common.json index 921244f1..040c48d7 100644 --- a/locales/eu/common.json +++ b/locales/eu/common.json @@ -699,7 +699,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/locales/fa/common.json b/locales/fa/common.json index 775617ef..3206f093 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -699,7 +699,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/locales/fi/common.json b/locales/fi/common.json index bdd59edc..7c89c142 100644 --- a/locales/fi/common.json +++ b/locales/fi/common.json @@ -699,7 +699,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/locales/fr/common.json b/locales/fr/common.json index 1c0fc7cd..4ddc8f7d 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -699,7 +699,9 @@ "error.startup.description": "Le projet local ou la base de données n’a pas pu être ouvert. Rechargez l’application et réessayez.", "error.startup.storageUnavailable": "Le stockage local n’a pas pu être ouvert. Rechargez l’application et réessayez.", "error.startup.projectUnavailable": "Un projet local n’a pas pu être ouvert. Rechargez l’application et réessayez.", + "error.startup.projectIoUnavailable": "Le projet n’a pas pu être lu pour le moment en raison d’un problème de stockage ou d’accès aux fichiers. Il n’a pas été considéré comme corrompu et n’a pas été modifié. Vérifiez la disponibilité du support de stockage et les autorisations des fichiers, puis réessayez.", "error.startup.reload": "Recharger", + "error.startup.retry": "Réessayer", "error.startup.recover": "Mettre le projet en quarantaine et recharger", "error.startup.recovering": "Préservation du projet…", "error.startup.reset": "Réinitialiser la base de données et recharger", diff --git a/locales/he/common.json b/locales/he/common.json index c0fce188..4194ac1d 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -699,7 +699,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/locales/hu/common.json b/locales/hu/common.json index 3519d768..1a456ddb 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -699,7 +699,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/locales/is/common.json b/locales/is/common.json index c8925c40..070eebc5 100644 --- a/locales/is/common.json +++ b/locales/is/common.json @@ -699,7 +699,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/locales/it/common.json b/locales/it/common.json index a4197f98..1c1536fa 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -699,7 +699,9 @@ "error.startup.description": "Non è stato possibile aprire il progetto locale o il database. Ricarica l’applicazione e riprova.", "error.startup.storageUnavailable": "Non è stato possibile aprire l’archiviazione locale. Ricarica l’applicazione e riprova.", "error.startup.projectUnavailable": "Non è stato possibile aprire un progetto locale. Ricarica l’applicazione e riprova.", + "error.startup.projectIoUnavailable": "Non è stato possibile leggere il progetto in questo momento a causa di un problema di archiviazione o di accesso ai file. Il progetto non è stato classificato come danneggiato né modificato. Verifica la disponibilità del dispositivo di archiviazione e i permessi dei file, quindi riprova.", "error.startup.reload": "Ricarica", + "error.startup.retry": "Riprova", "error.startup.recover": "Metti il progetto in quarantena e ricarica", "error.startup.recovering": "Conservazione del progetto…", "error.startup.reset": "Reimposta il database e ricarica", diff --git a/locales/ja/common.json b/locales/ja/common.json index 3a39a6f9..991fcd8f 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -699,7 +699,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/locales/ko/common.json b/locales/ko/common.json index 3ae366b8..905afeed 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -699,7 +699,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/locales/pt/common.json b/locales/pt/common.json index 9e5a0d51..2552ac3e 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -699,7 +699,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/locales/ru/common.json b/locales/ru/common.json index 25ea3864..e2eefebd 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -699,7 +699,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/locales/sv/common.json b/locales/sv/common.json index fa5a3ddf..21ddf5e7 100644 --- a/locales/sv/common.json +++ b/locales/sv/common.json @@ -699,7 +699,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/locales/zh/common.json b/locales/zh/common.json index cca1db16..5aaf609e 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -699,7 +699,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/public/locales/ar/bundle.json b/public/locales/ar/bundle.json index faf17d4b..00c1a780 100644 --- a/public/locales/ar/bundle.json +++ b/public/locales/ar/bundle.json @@ -815,7 +815,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/public/locales/de/bundle.json b/public/locales/de/bundle.json index f8c2e723..18de8adf 100644 --- a/public/locales/de/bundle.json +++ b/public/locales/de/bundle.json @@ -815,7 +815,9 @@ "error.startup.description": "Das lokale Projekt oder die lokale Datenbank konnte nicht geöffnet werden. Laden Sie die Anwendung neu und versuchen Sie es erneut.", "error.startup.storageUnavailable": "Der lokale Speicher konnte nicht geöffnet werden. Laden Sie die Anwendung neu und versuchen Sie es erneut.", "error.startup.projectUnavailable": "Ein lokales Projekt konnte nicht geöffnet werden. Laden Sie die Anwendung neu und versuchen Sie es erneut.", + "error.startup.projectIoUnavailable": "Das Projekt konnte derzeit aufgrund eines Speicher- oder Dateizugriffsproblems nicht gelesen werden. Es wurde nicht als beschädigt eingestuft und nicht verändert. Prüfen Sie die Verfügbarkeit des Speichers und die Dateiberechtigungen und versuchen Sie es erneut.", "error.startup.reload": "Neu laden", + "error.startup.retry": "Erneut versuchen", "error.startup.recover": "Projekt unter Quarantäne stellen und neu laden", "error.startup.recovering": "Projekt wird gesichert …", "error.startup.reset": "Datenbank zurücksetzen und neu laden", diff --git a/public/locales/el/bundle.json b/public/locales/el/bundle.json index 9fbad20e..e8589b08 100644 --- a/public/locales/el/bundle.json +++ b/public/locales/el/bundle.json @@ -815,7 +815,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/public/locales/en/bundle.json b/public/locales/en/bundle.json index abb0be73..3f353c88 100644 --- a/public/locales/en/bundle.json +++ b/public/locales/en/bundle.json @@ -815,7 +815,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/public/locales/es/bundle.json b/public/locales/es/bundle.json index d7f3bba7..954f72f4 100644 --- a/public/locales/es/bundle.json +++ b/public/locales/es/bundle.json @@ -815,7 +815,9 @@ "error.startup.description": "No se pudo abrir el proyecto local o la base de datos. Recarga la aplicación e inténtalo de nuevo.", "error.startup.storageUnavailable": "No se pudo abrir el almacenamiento local. Recarga la aplicación e inténtalo de nuevo.", "error.startup.projectUnavailable": "No se pudo abrir un proyecto local. Recarga la aplicación e inténtalo de nuevo.", + "error.startup.projectIoUnavailable": "No se pudo leer el proyecto en este momento debido a un problema de almacenamiento o acceso a los archivos. No se ha clasificado como dañado ni se ha modificado. Comprueba la disponibilidad del dispositivo de almacenamiento y los permisos de los archivos, y vuelve a intentarlo.", "error.startup.reload": "Recargar", + "error.startup.retry": "Reintentar", "error.startup.recover": "Poner el proyecto en cuarentena y recargar", "error.startup.recovering": "Preservando el proyecto…", "error.startup.reset": "Restablecer la base de datos y recargar", diff --git a/public/locales/eu/bundle.json b/public/locales/eu/bundle.json index 87d4676e..3e996a9b 100644 --- a/public/locales/eu/bundle.json +++ b/public/locales/eu/bundle.json @@ -815,7 +815,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/public/locales/fa/bundle.json b/public/locales/fa/bundle.json index b5f8d0a1..0d086672 100644 --- a/public/locales/fa/bundle.json +++ b/public/locales/fa/bundle.json @@ -815,7 +815,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/public/locales/fi/bundle.json b/public/locales/fi/bundle.json index de65cd7a..ab2ad074 100644 --- a/public/locales/fi/bundle.json +++ b/public/locales/fi/bundle.json @@ -815,7 +815,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/public/locales/fr/bundle.json b/public/locales/fr/bundle.json index d6f01743..113c63fc 100644 --- a/public/locales/fr/bundle.json +++ b/public/locales/fr/bundle.json @@ -815,7 +815,9 @@ "error.startup.description": "Le projet local ou la base de données n’a pas pu être ouvert. Rechargez l’application et réessayez.", "error.startup.storageUnavailable": "Le stockage local n’a pas pu être ouvert. Rechargez l’application et réessayez.", "error.startup.projectUnavailable": "Un projet local n’a pas pu être ouvert. Rechargez l’application et réessayez.", + "error.startup.projectIoUnavailable": "Le projet n’a pas pu être lu pour le moment en raison d’un problème de stockage ou d’accès aux fichiers. Il n’a pas été considéré comme corrompu et n’a pas été modifié. Vérifiez la disponibilité du support de stockage et les autorisations des fichiers, puis réessayez.", "error.startup.reload": "Recharger", + "error.startup.retry": "Réessayer", "error.startup.recover": "Mettre le projet en quarantaine et recharger", "error.startup.recovering": "Préservation du projet…", "error.startup.reset": "Réinitialiser la base de données et recharger", diff --git a/public/locales/he/bundle.json b/public/locales/he/bundle.json index 7c484db7..1d13ff19 100644 --- a/public/locales/he/bundle.json +++ b/public/locales/he/bundle.json @@ -815,7 +815,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/public/locales/hu/bundle.json b/public/locales/hu/bundle.json index 7b6e2731..6b920642 100644 --- a/public/locales/hu/bundle.json +++ b/public/locales/hu/bundle.json @@ -815,7 +815,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/public/locales/is/bundle.json b/public/locales/is/bundle.json index d42fc65a..bc73a001 100644 --- a/public/locales/is/bundle.json +++ b/public/locales/is/bundle.json @@ -815,7 +815,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/public/locales/it/bundle.json b/public/locales/it/bundle.json index ec6d6fb4..1f41252b 100644 --- a/public/locales/it/bundle.json +++ b/public/locales/it/bundle.json @@ -815,7 +815,9 @@ "error.startup.description": "Non è stato possibile aprire il progetto locale o il database. Ricarica l’applicazione e riprova.", "error.startup.storageUnavailable": "Non è stato possibile aprire l’archiviazione locale. Ricarica l’applicazione e riprova.", "error.startup.projectUnavailable": "Non è stato possibile aprire un progetto locale. Ricarica l’applicazione e riprova.", + "error.startup.projectIoUnavailable": "Non è stato possibile leggere il progetto in questo momento a causa di un problema di archiviazione o di accesso ai file. Il progetto non è stato classificato come danneggiato né modificato. Verifica la disponibilità del dispositivo di archiviazione e i permessi dei file, quindi riprova.", "error.startup.reload": "Ricarica", + "error.startup.retry": "Riprova", "error.startup.recover": "Metti il progetto in quarantena e ricarica", "error.startup.recovering": "Conservazione del progetto…", "error.startup.reset": "Reimposta il database e ricarica", diff --git a/public/locales/ja/bundle.json b/public/locales/ja/bundle.json index 8c78269a..7c4256e8 100644 --- a/public/locales/ja/bundle.json +++ b/public/locales/ja/bundle.json @@ -815,7 +815,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/public/locales/ko/bundle.json b/public/locales/ko/bundle.json index 4e4ae470..6694df43 100644 --- a/public/locales/ko/bundle.json +++ b/public/locales/ko/bundle.json @@ -815,7 +815,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/public/locales/pt/bundle.json b/public/locales/pt/bundle.json index 6e84c85b..4bc52769 100644 --- a/public/locales/pt/bundle.json +++ b/public/locales/pt/bundle.json @@ -815,7 +815,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/public/locales/ru/bundle.json b/public/locales/ru/bundle.json index 9b9d0ad7..c8e01f92 100644 --- a/public/locales/ru/bundle.json +++ b/public/locales/ru/bundle.json @@ -815,7 +815,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/public/locales/sv/bundle.json b/public/locales/sv/bundle.json index aed1cfcf..869f028f 100644 --- a/public/locales/sv/bundle.json +++ b/public/locales/sv/bundle.json @@ -815,7 +815,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/public/locales/zh/bundle.json b/public/locales/zh/bundle.json index 43abb3b5..8ae26f8e 100644 --- a/public/locales/zh/bundle.json +++ b/public/locales/zh/bundle.json @@ -815,7 +815,9 @@ "error.startup.description": "The local project or database could not be opened. Reload and try again.", "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.", "error.startup.reload": "Reload", + "error.startup.retry": "Retry", "error.startup.recover": "Quarantine project and reload", "error.startup.recovering": "Preserving project…", "error.startup.reset": "Reset database and reload", diff --git a/services/startupRecovery.tsx b/services/startupRecovery.tsx index d0283a3d..ef6ce9d2 100644 --- a/services/startupRecovery.tsx +++ b/services/startupRecovery.tsx @@ -14,6 +14,7 @@ export async function renderStorageInitializationFailure(root: Root): Promise { await resetAllDatabases(); window.location.reload(); @@ -32,13 +33,14 @@ export async function renderProjectInitializationFailure( const projectLoadError = error instanceof ProjectLoadError ? error : null; const backendKind = await storageService.getStorageBackendKind(); const copy = await loadStorageErrorCopy(); - const { canQuarantine, canReset } = getStartupRecoveryActions(error, backendKind); + const { canQuarantine, canReset, failureKind } = getStartupRecoveryActions(error, backendKind); const corruptProjectId = projectLoadError?.reason === 'corrupt' ? projectLoadError.projectId : null; root.render( { @@ -49,6 +51,7 @@ export async function renderProjectInitializationFailure( }, } : {})} + {...(failureKind === 'project-io' ? { onRetry: () => window.location.reload() } : {})} {...(canReset ? { onReset: async () => { diff --git a/services/startupRecoveryPolicy.ts b/services/startupRecoveryPolicy.ts index 4fede0c9..cfb4b013 100644 --- a/services/startupRecoveryPolicy.ts +++ b/services/startupRecoveryPolicy.ts @@ -1,15 +1,29 @@ import { ProjectLoadError } from './fs/projectFsStore'; export type StartupStorageBackend = 'indexeddb' | 'filesystem'; +export type StartupRecoveryFailureKind = 'storage' | 'project-corrupt' | 'project-io'; -// QNBS-v3: bind destructive reset to IndexedDB provenance so desktop filesystem failures stay preserve-first. +export interface StartupRecoveryActions { + failureKind: StartupRecoveryFailureKind; + canQuarantine: boolean; + canReset: boolean; +} + +// QNBS-v3: separate project I/O retry UX from corruption so non-destructive failures never gain quarantine authority. export function getStartupRecoveryActions( error: unknown, backend: StartupStorageBackend, -): { canQuarantine: boolean; canReset: boolean } { +): StartupRecoveryActions { const projectLoadError = error instanceof ProjectLoadError ? error : null; + const failureKind: StartupRecoveryFailureKind = + projectLoadError?.reason === 'corrupt' + ? 'project-corrupt' + : projectLoadError || backend === 'filesystem' + ? 'project-io' + : 'storage'; return { - canQuarantine: projectLoadError?.reason === 'corrupt' && backend === 'filesystem', - canReset: !projectLoadError && backend === 'indexeddb', + failureKind, + canQuarantine: failureKind === 'project-corrupt' && backend === 'filesystem', + canReset: failureKind === 'storage' && backend === 'indexeddb', }; } diff --git a/tests/unit/StorageErrorScreen.test.tsx b/tests/unit/StorageErrorScreen.test.tsx index b32627f1..10812e99 100644 --- a/tests/unit/StorageErrorScreen.test.tsx +++ b/tests/unit/StorageErrorScreen.test.tsx @@ -46,11 +46,12 @@ describe('StorageErrorScreen', () => { ...STARTUP_COPY_FALLBACKS, description: 'Localized description', projectUnavailable: 'Localized project failure', + projectIoUnavailable: 'Localized project I/O failure', recover: 'Localized preserve', recoveryFailed: 'Localized safe failure', }; - render(); + render(); expect(screen.getByText('Localized description')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Localized preserve' })).toBeInTheDocument(); @@ -64,7 +65,7 @@ describe('StorageErrorScreen', () => { ); }); - it('announces recovery and blocks destructive reset while preservation is pending', async () => { + it('announces recovery without offering a destructive reset while preservation is pending', async () => { const user = userEvent.setup(); let resolveRecovery: () => void = () => undefined; const onRecover = vi.fn( @@ -73,18 +74,20 @@ describe('StorageErrorScreen', () => { resolveRecovery = resolve; }), ); - const onReset = vi.fn(); - render( - , + , ); await user.click(screen.getByRole('button', { name: STARTUP_COPY_FALLBACKS.recover })); expect(screen.getByRole('status')).toHaveTextContent(STARTUP_COPY_FALLBACKS.recovering); - expect(screen.getByRole('button', { name: STARTUP_COPY_FALLBACKS.reset })).toBeDisabled(); - await user.click(screen.getByRole('button', { name: STARTUP_COPY_FALLBACKS.reset })); - expect(onReset).not.toHaveBeenCalled(); + expect( + screen.queryByRole('button', { name: STARTUP_COPY_FALLBACKS.reset }), + ).not.toBeInTheDocument(); resolveRecovery(); await waitFor(() => expect(screen.getByRole('status')).toHaveTextContent('')); @@ -94,7 +97,13 @@ describe('StorageErrorScreen', () => { const user = userEvent.setup(); const onRecover = vi.fn().mockRejectedValue(new ProjectQuarantineError('source-missing')); - render(); + render( + , + ); await user.click(screen.getByRole('button', { name: STARTUP_COPY_FALLBACKS.recover })); @@ -102,12 +111,15 @@ describe('StorageErrorScreen', () => { expect(screen.queryByText(STARTUP_COPY_FALLBACKS.recoveryFailed)).not.toBeInTheDocument(); }); - it('reports the typed already-preserved outcome and supports successful recovery and reset actions', async () => { + it('reports the typed already-preserved outcome and supports successful recovery', async () => { const user = userEvent.setup(); const onRecover = vi.fn().mockRejectedValue(new ProjectQuarantineError('already-preserved')); - const onReset = vi.fn(); const firstRender = render( - , + , ); await user.click(screen.getByRole('button', { name: STARTUP_COPY_FALLBACKS.recover })); @@ -115,12 +127,19 @@ describe('StorageErrorScreen', () => { await screen.findByText(STARTUP_COPY_FALLBACKS.recoveryAlreadyPreserved), ).toBeInTheDocument(); - await user.click(screen.getByRole('button', { name: STARTUP_COPY_FALLBACKS.reset })); - expect(onReset).toHaveBeenCalledOnce(); + expect( + screen.queryByRole('button', { name: STARTUP_COPY_FALLBACKS.reset }), + ).not.toBeInTheDocument(); firstRender.unmount(); const successfulRecover = vi.fn().mockResolvedValue(undefined); - render(); + render( + , + ); await user.click(screen.getByRole('button', { name: STARTUP_COPY_FALLBACKS.recover })); await waitFor(() => expect(successfulRecover).toHaveBeenCalledOnce()); expect( @@ -129,7 +148,7 @@ describe('StorageErrorScreen', () => { }); it('omits unsupported recovery actions when no handlers are provided', () => { - render(); + render(); expect( screen.queryByRole('button', { name: STARTUP_COPY_FALLBACKS.recover }), @@ -139,4 +158,50 @@ describe('StorageErrorScreen', () => { ).not.toBeInTheDocument(); expect(screen.getByText(STARTUP_COPY_FALLBACKS.storageUnavailable)).toBeInTheDocument(); }); + + it('offers database reset only for generic storage failures', async () => { + const user = userEvent.setup(); + const onReset = vi.fn(); + + render( + , + ); + + await user.click(screen.getByRole('button', { name: STARTUP_COPY_FALLBACKS.reset })); + + expect(onReset).toHaveBeenCalledOnce(); + }); + + it('shows a non-destructive retry for project I/O failures without corruption or reset actions', async () => { + const user = userEvent.setup(); + const onRetry = vi.fn(); + const onRecover = vi.fn(); + const onReset = vi.fn(); + + render( + , + ); + + expect(screen.getByText(STARTUP_COPY_FALLBACKS.projectIoUnavailable)).toBeInTheDocument(); + expect(screen.queryByText(STARTUP_COPY_FALLBACKS.projectUnavailable)).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: STARTUP_COPY_FALLBACKS.retry })).toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: STARTUP_COPY_FALLBACKS.recover }), + ).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: STARTUP_COPY_FALLBACKS.reset }), + ).not.toBeInTheDocument(); + + await user.click(screen.getByRole('button', { name: STARTUP_COPY_FALLBACKS.retry })); + + expect(onRetry).toHaveBeenCalledOnce(); + expect(onRecover).not.toHaveBeenCalled(); + expect(onReset).not.toHaveBeenCalled(); + }); }); diff --git a/tests/unit/startupRecovery.test.tsx b/tests/unit/startupRecovery.test.tsx index 19f65792..340aef92 100644 --- a/tests/unit/startupRecovery.test.tsx +++ b/tests/unit/startupRecovery.test.tsx @@ -11,7 +11,9 @@ const { mockRoot, mockReset, mockBackendKind, mockQuarantine, mockCopy, loggerEr description: 'description', storageUnavailable: 'storage unavailable', projectUnavailable: 'project unavailable', + projectIoUnavailable: 'project io unavailable', reload: 'reload', + retry: 'retry', recover: 'recover', recovering: 'recovering', reset: 'reset', @@ -70,8 +72,10 @@ import { type RecoveryScreenProps = { copy: typeof mockCopy; + failureKind: 'storage' | 'project-corrupt' | 'project-io'; onReset?: () => Promise; onRecover?: () => Promise; + onRetry?: () => void; }; function renderedScreenProps(): RecoveryScreenProps { @@ -92,6 +96,7 @@ describe('startup recovery rendering', () => { expect(mockRoot.render).toHaveBeenCalledOnce(); expect(renderedScreenProps().copy).toBe(mockCopy); + expect(renderedScreenProps().failureKind).toBe('storage'); expect(renderedScreenProps().onReset).toEqual(expect.any(Function)); }); @@ -104,6 +109,7 @@ describe('startup recovery rendering', () => { ); const props = renderedScreenProps(); + expect(props.failureKind).toBe('project-corrupt'); expect(props.onRecover).toEqual(expect.any(Function)); expect(props.onReset).toBeUndefined(); await (props.onRecover as () => Promise)(); @@ -114,13 +120,17 @@ describe('startup recovery rendering', () => { mockBackendKind.mockResolvedValue('filesystem'); await renderProjectInitializationFailure(mockRoot as never, new Error('EACCES /projects/p1')); + expect(renderedScreenProps().failureKind).toBe('project-io'); expect(renderedScreenProps().onReset).toBeUndefined(); await renderProjectInitializationFailure( mockRoot as never, new ProjectLoadError('io-error', 'io', 'p1'), ); + expect(renderedScreenProps().failureKind).toBe('project-io'); expect(renderedScreenProps().onReset).toBeUndefined(); + expect(renderedScreenProps().onRecover).toBeUndefined(); + expect(renderedScreenProps().onRetry).toEqual(expect.any(Function)); expect(loggerError).toHaveBeenCalled(); }); @@ -129,6 +139,7 @@ describe('startup recovery rendering', () => { await renderProjectInitializationFailure(mockRoot as never, new Error('QuotaExceededError')); expect(renderedScreenProps().onRecover).toBeUndefined(); + expect(renderedScreenProps().failureKind).toBe('storage'); expect(renderedScreenProps().onReset).toEqual(expect.any(Function)); }); }); diff --git a/tests/unit/startupRecoveryPolicy.test.ts b/tests/unit/startupRecoveryPolicy.test.ts index a264d83c..e16517dc 100644 --- a/tests/unit/startupRecoveryPolicy.test.ts +++ b/tests/unit/startupRecoveryPolicy.test.ts @@ -8,10 +8,12 @@ describe('startup recovery action policy', () => { const error = new ProjectLoadError('corrupt', 'corrupt', 'project-1'); expect(getStartupRecoveryActions(error, 'filesystem')).toEqual({ + failureKind: 'project-corrupt', canQuarantine: true, canReset: false, }); expect(getStartupRecoveryActions(error, 'indexeddb')).toEqual({ + failureKind: 'project-corrupt', canQuarantine: false, canReset: false, }); @@ -19,12 +21,14 @@ describe('startup recovery action policy', () => { it('never offers destructive reset for filesystem-origin failures, including raw errors', () => { expect(getStartupRecoveryActions(new Error('EACCES /projects/p1'), 'filesystem')).toEqual({ + failureKind: 'project-io', canQuarantine: false, canReset: false, }); expect( getStartupRecoveryActions(new ProjectLoadError('io-error', 'io', 'project-1'), 'filesystem'), ).toEqual({ + failureKind: 'project-io', canQuarantine: false, canReset: false, }); @@ -32,8 +36,19 @@ describe('startup recovery action policy', () => { it('retains database reset for non-project failures from IndexedDB', () => { expect(getStartupRecoveryActions(new Error('QuotaExceededError'), 'indexeddb')).toEqual({ + failureKind: 'storage', canQuarantine: false, canReset: true, }); }); + + it('does not offer database reset for project-specific IndexedDB failures', () => { + expect( + getStartupRecoveryActions(new ProjectLoadError('io-error', 'io', 'project-1'), 'indexeddb'), + ).toEqual({ + failureKind: 'project-io', + canQuarantine: false, + canReset: false, + }); + }); });