From 428097f35dba5721f2f6cf2761bcecfdb49f46a4 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Tue, 5 May 2026 00:48:09 +0200 Subject: [PATCH] feat: show mobile vault load retry --- apps/mobile/src/MobileApp.tsx | 28 +++++++++++++++- apps/mobile/src/styles.ts | 2 ++ apps/mobile/src/styles/vaultLoadStyles.ts | 32 +++++++++++++++++++ .../mobile/src/useMobileVaultRuntimeLoader.ts | 31 +++++++++++++++--- docs/MOBILE_PROGRESS.md | 8 ++++- 5 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 apps/mobile/src/styles/vaultLoadStyles.ts diff --git a/apps/mobile/src/MobileApp.tsx b/apps/mobile/src/MobileApp.tsx index c8939ef3..f3144531 100644 --- a/apps/mobile/src/MobileApp.tsx +++ b/apps/mobile/src/MobileApp.tsx @@ -83,7 +83,7 @@ export function MobileApp() { setCompactNavigation((state) => selectLoadedNote(state, notes, selectedNoteId)) }, []) - useMobileVaultRuntimeLoader({ + const runtimeLoader = useMobileVaultRuntimeLoader({ appStateStorage, loadNotes: loadDemoVaultNotes, metadataStorage: vaultMetadataStorage, @@ -127,9 +127,11 @@ export function MobileApp() { createNoteTitle={createFlow.title} isCreatePromptOpen={createFlow.isPromptOpen} isCreatingNote={createFlow.isCreating} + runtimeLoadFailed={runtimeLoader.failed} onCancelCreateNote={createFlow.cancel} onChangeCreateNoteTitle={createFlow.setTitle} onOpenCreateNote={createFlow.open} + onRetryRuntimeLoad={runtimeLoader.retry} onSubmitCreateNote={createFlow.submit} onSelectNote={selectNote} /> @@ -155,9 +157,11 @@ export function MobileApp() { createNoteTitle={createFlow.title} isCreatePromptOpen={createFlow.isPromptOpen} isCreatingNote={createFlow.isCreating} + runtimeLoadFailed={runtimeLoader.failed} onCancelCreateNote={createFlow.cancel} onChangeCreateNoteTitle={createFlow.setTitle} onOpenCreateNote={createFlow.open} + onRetryRuntimeLoad={runtimeLoader.retry} onSubmitCreateNote={createFlow.submit} onSelectNote={selectNote} /> @@ -179,9 +183,11 @@ function CompactShell({ createNoteTitle, isCreatePromptOpen, isCreatingNote, + runtimeLoadFailed, onCancelCreateNote, onChangeCreateNoteTitle, onOpenCreateNote, + onRetryRuntimeLoad, onSubmitCreateNote, onSelectNote, selectedNoteId, @@ -194,12 +200,14 @@ function CompactShell({ createNoteTitle: string isCreatePromptOpen: boolean isCreatingNote: boolean + runtimeLoadFailed: boolean onNavigate: (event: CompactNavigationEvent) => void onDeleteNote?: () => void onDraftChange: (draft: MobileEditorDraft) => void onCancelCreateNote: () => void onChangeCreateNoteTitle: (title: string) => void onOpenCreateNote: () => void + onRetryRuntimeLoad: () => void onSubmitCreateNote: () => void onSelectNote: (note: MobileNote) => void selectedNoteId: string @@ -244,10 +252,12 @@ function CompactShell({ createNoteTitle={createNoteTitle} isCreatePromptOpen={isCreatePromptOpen} isCreatingNote={isCreatingNote} + runtimeLoadFailed={runtimeLoadFailed} onCancelCreateNote={onCancelCreateNote} onChangeCreateNoteTitle={onChangeCreateNoteTitle} onOpenCreateNote={onOpenCreateNote} onOpenSidebar={() => onNavigate({ type: 'openSidebar' })} + onRetryRuntimeLoad={onRetryRuntimeLoad} onSelectNote={onSelectNote} onSubmitCreateNote={onSubmitCreateNote} /> @@ -294,10 +304,12 @@ function NoteListPanel({ createNoteTitle, isCreatePromptOpen, isCreatingNote, + runtimeLoadFailed, onCancelCreateNote, onChangeCreateNoteTitle, onOpenCreateNote, onOpenSidebar, + onRetryRuntimeLoad, onSelectNote, onSubmitCreateNote, selectedNoteId, @@ -307,10 +319,12 @@ function NoteListPanel({ createNoteTitle: string isCreatePromptOpen: boolean isCreatingNote: boolean + runtimeLoadFailed: boolean onCancelCreateNote: () => void onChangeCreateNoteTitle: (title: string) => void onOpenCreateNote: () => void onOpenSidebar?: () => void + onRetryRuntimeLoad: () => void onSelectNote: (note: MobileNote) => void onSubmitCreateNote: () => void selectedNoteId: string @@ -323,6 +337,7 @@ function NoteListPanel({ } /> + {runtimeLoadFailed ? : null} item.id} @@ -377,6 +392,17 @@ function NoteListPanel({ ) } +function VaultLoadErrorNotice({ onRetry }: { onRetry: () => void }) { + return ( + + Could not load vault notes. + [styles.vaultLoadRetry, pressed ? styles.pressed : null]}> + Retry + + + ) +} + function selectLoadedNote( state: ReturnType, loadedNotes: MobileNote[], diff --git a/apps/mobile/src/styles.ts b/apps/mobile/src/styles.ts index 89963889..55bc67cb 100644 --- a/apps/mobile/src/styles.ts +++ b/apps/mobile/src/styles.ts @@ -5,6 +5,7 @@ import { noteCreateStyles } from './styles/noteCreateStyles' import { noteListStyles } from './styles/noteListStyles' import { propertiesStyles } from './styles/propertiesStyles' import { sidebarStyles } from './styles/sidebarStyles' +import { vaultLoadStyles } from './styles/vaultLoadStyles' export const styles = { ...commonStyles, @@ -14,4 +15,5 @@ export const styles = { ...noteListStyles, ...propertiesStyles, ...sidebarStyles, + ...vaultLoadStyles, } diff --git a/apps/mobile/src/styles/vaultLoadStyles.ts b/apps/mobile/src/styles/vaultLoadStyles.ts new file mode 100644 index 00000000..07b2e448 --- /dev/null +++ b/apps/mobile/src/styles/vaultLoadStyles.ts @@ -0,0 +1,32 @@ +import { StyleSheet } from 'react-native' +import { colors, spacing } from '../theme' + +export const vaultLoadStyles = StyleSheet.create({ + vaultLoadError: { + marginHorizontal: spacing.lg, + marginBottom: spacing.sm, + padding: spacing.md, + borderColor: colors.chipRed, + borderWidth: StyleSheet.hairlineWidth, + borderRadius: 8, + backgroundColor: '#fff8f7', + }, + vaultLoadErrorText: { + color: colors.text, + fontSize: 14, + fontWeight: '600', + }, + vaultLoadRetry: { + alignSelf: 'flex-start', + marginTop: spacing.sm, + paddingHorizontal: spacing.md, + paddingVertical: spacing.xs, + borderRadius: 6, + backgroundColor: colors.primarySoft, + }, + vaultLoadRetryText: { + color: colors.primary, + fontSize: 13, + fontWeight: '700', + }, +}) diff --git a/apps/mobile/src/useMobileVaultRuntimeLoader.ts b/apps/mobile/src/useMobileVaultRuntimeLoader.ts index 3fe62f27..6de5927c 100644 --- a/apps/mobile/src/useMobileVaultRuntimeLoader.ts +++ b/apps/mobile/src/useMobileVaultRuntimeLoader.ts @@ -1,4 +1,4 @@ -import { useEffect } from 'react' +import { useCallback, useEffect, useState } from 'react' import type { MobileAppStateStorage } from './mobileAppStateStorage' import type { MobileNote } from './demoData' import type { MobileVaultMetadata } from './mobileVaultMetadata' @@ -20,19 +20,42 @@ export function useMobileVaultRuntimeLoader({ selectedNoteId: string | null }) => void }) { + const [loadState, setLoadState] = useState<'loading' | 'ready' | 'failed'>('loading') + const [reloadKey, setReloadKey] = useState(0) + + const retry = useCallback(() => { + setLoadState('loading') + setReloadKey((key) => key + 1) + }, []) + useEffect(() => { let isActive = true void loadMobileVaultRuntime({ appStateStorage, loadNotes, metadataStorage }) .then((runtime) => { - if (isActive && runtime.notes.length > 0) { + if (!isActive) { + return + } + + if (runtime.notes.length > 0) { onLoaded(runtime) } + setLoadState('ready') + }) + .catch(() => { + if (isActive) { + setLoadState('failed') + } }) - .catch(() => {}) return () => { isActive = false } - }, [appStateStorage, loadNotes, metadataStorage, onLoaded]) + }, [appStateStorage, loadNotes, metadataStorage, onLoaded, reloadKey]) + + return { + failed: loadState === 'failed', + isLoading: loadState === 'loading', + retry, + } } diff --git a/docs/MOBILE_PROGRESS.md b/docs/MOBILE_PROGRESS.md index 6d457f72..8f0d06da 100644 --- a/docs/MOBILE_PROGRESS.md +++ b/docs/MOBILE_PROGRESS.md @@ -72,6 +72,7 @@ This file is the resumable working log for Tolaria mobile. The strategy and road - Replaced the demo vault's hardcoded identity with the shared default vault metadata boundary. - Wired mobile runtime loading through the persisted vault metadata catalog boundary, so app state, note loading, autosave, note creation, and deletion all operate against the active vault metadata. - Extracted runtime loading into a hook plus tested pure loader to keep `MobileApp.tsx` at CodeScene `10.0`. +- Added first-class runtime vault load failure state with a visible retry notice in the note list, replacing the previous silent failure path. ## Next Action @@ -79,7 +80,7 @@ Continue Phase 3 with app-managed vault storage hardening: 1. Add a focused simulator interaction path for create/open/edit/autosave/delete once Expo Go's overlay no longer blocks clean screenshots. 2. Decide whether archive should be modeled as a first-class note state or deferred until the mobile vault schema exists. -3. Add first-class error/retry state for vault runtime load failures instead of silently falling back to fixture notes. +3. Add a small runtime empty-vault state once user-created blank vaults exist. ## Verification Log @@ -248,6 +249,11 @@ Continue Phase 3 with app-managed vault storage hardening: - CodeScene after runtime vault metadata wiring: `apps/mobile/src/MobileApp.tsx`, `apps/mobile/src/mobileDemoVault.ts`, `apps/mobile/src/mobileVaultRuntime.ts`, and `apps/mobile/src/mobileVaultRuntime.test.ts` scored `10`; `apps/mobile/src/useMobileVaultRuntimeLoader.ts` returned no scorable code and no findings. - `pnpm --filter @tolaria/mobile test` passed after runtime vault metadata wiring: 22 files / 70 tests. - `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after runtime vault metadata wiring. +- `pnpm --filter @tolaria/mobile test -- src/mobileVaultRuntime.test.ts` passed after runtime load retry UI: 22 files / 70 tests. +- `pnpm --filter @tolaria/mobile typecheck` passed after runtime load retry UI. +- CodeScene after runtime load retry UI: `apps/mobile/src/MobileApp.tsx`, `apps/mobile/src/styles/noteListStyles.ts`, and `apps/mobile/src/styles/vaultLoadStyles.ts` scored `10`; `apps/mobile/src/useMobileVaultRuntimeLoader.ts` returned no scorable code and no findings. +- `pnpm --filter @tolaria/mobile test` passed after runtime load retry UI: 22 files / 70 tests. +- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after runtime load retry UI. ## Risks / Watch Items