Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ OpenMBEE View Editor brings source editing, named model views and reviewed graph
- **Review graphical changes.** Rename parts, create native views, add typed part usages and connect compatible directed ports through an exact source-change preview.
- **Preserve compatible layouts.** Keep verified symbol positions across supported edits, including inactive views. Unsafe identity mappings are reported and arranged again.
- **Keep projects portable.** Download applied source, unfinished drafts and compatible diagram positions together; reopen the bundle or import multiple SysML files.
- **Recover unfinished work.** Restore or discard one local browser recovery copy, with explicit storage errors and protection against competing tabs.
- **Recover unfinished work.** Inspect the saved copy’s time, files, draft status and source sizes before restoring or discarding local browser recovery. Competing-tab conflicts explain that Retry only inspects the saved copy and keeps the current project open.
- **Arrange your workspace.** Resize or collapse Explorer, Inspector, source/diagram panes and Issues using dividers, keyboard controls or panel options.
- **Choose your appearance.** Use Light, Dark or Follow system, six diagram palettes and custom muted element colors. Theme and panel preferences stay in the browser.

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/source-assistance-recovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ After edits, the workbench debounces an immutable project snapshot for 750 ms. D

A startup record blocks replacement until an explicit restore/discard choice. Restore validates the project through the real workspace engine before replacing it, confirms replacement of dirty current work, and accepts the recovery snapshot synchronously before observing subsequent work. The active view is checked against the restored model. Failed restore retains the stored record and current project.

Writes and deletion use an atomic compare-and-swap transaction against the exact previously read string. A competing tab's change causes a visible conflict; retry reads and protects that copy for an explicit choice. Denied storage and write failures retain the editable project and permit retry. Malformed or future string envelopes remain protected until explicit discard. An unsupported non-string IndexedDB value is retained and reported unavailable; this version cannot discard that value through the app.
Writes and deletion use an atomic compare-and-swap transaction against the exact previously read string. A competing tab's change causes a visible conflict. Its message states that Retry recovery only reads the saved copy without replacing the current project. After Retry, the pending copy shows its project name and saved time, with expandable filenames, unapplied-draft status and applied/draft UTF-8 byte sizes; Restore recovery replaces the open project (with dirty-work confirmation), while Discard recovery deletes the saved copy and keeps the live project. Users are advised to download current work before restoring. The live copy has no invented save timestamp; pending edits are explicitly described as remaining in this tab until a choice permits recovery saving. Denied storage and write failures retain the editable project and permit retry. Malformed or future string envelopes remain protected until explicit discard. An unsupported non-string IndexedDB value is retained and reported unavailable; this version cannot discard that value through the app.

Only one recovery copy is kept, without a time-based expiration. Disabling saving retains the existing record until discarded. Browser storage is local to the origin and may be cleared or unavailable; downloaded project files remain the portable backup mechanism.

Expand Down
4 changes: 4 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,7 @@ Drag helpers wait for stable heading and viewport geometry across animation fram
The 0.12 assembly overview fixture records thirteen source-backed edges (ownership/reference plus typing) rather than the old six type-collapsed edges. Its routing regression still requires every route/label to avoid cards and reveals each exact declaration or type reference by keyboard. Changed example tree expectations retain explicit source targets/gaps and exact endpoint/declaration fixtures; structural boundary connection counts are not relaxed.

`editor-tour.spec.ts` exercises the optional guide with an imported project and an unapplied draft, verifies keyboard dismissal/focus return and checks every popup stays in the viewport at phone width with Explorer collapsed. It also checks that collapsed Explorer and Code-only layouts receive instructions for revealing hidden controls without changing the layout, that each step has an accessible description and a polite live region, and that dismissing by choosing another control preserves focus on that control. The visual project captures the first step at desktop/narrow widths in both themes. Screen-reader announcements still require manual assistive-technology verification.

`recovery.spec.ts` exercises the competing-tab conflict through both explicit Restore and Discard decisions. It checks the Retry explanation, exact stored timestamp, both live drafts and the unchanged foreign IndexedDB record before a decision. Restore additionally cancels dirty-work confirmation before accepting replacement; Discard retains the local project and resumes saving its queued copy. Visual recovery-conflict and pending-copy captures use a controlled foreign IndexedDB record with a fixed timestamp, while exercising the real manager’s compare-and-swap and Retry paths. They cover desktop/narrow widths in both themes, dialog bounds and keyboard focus return; they are presentation evidence, not a substitute for the real two-tab preservation scenarios.

The critical recovery restore scenario now writes distinguishable successive drafts, waits for the latest exact durable project, reloads and inspects the timestamp and expandable file summary before restoring. Applied/draft byte counts are checked against independently computed UTF-8 sizes, including Unicode, and inspecting the summary must leave the durable record unchanged. The restored source and both saved view layouts are compared exactly with that latest snapshot. Pending-copy visual captures expand the file list. These checks establish bounded successful-save behavior, not a reproduction of the report’s earlier condensed recovery copy.
22 changes: 22 additions & 0 deletions docs/verification.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
# Verification record

## Recovery contents preview — issue #15

Recorded 2026-11-11, extending the local issue #23 work on `fix/recovery-conflict-clarity`. [Issue #15](https://github.com/Open-MBEE/sysml_view_editor/issues/15) now has an expandable pending-copy summary in the launcher/banner and recovery settings: source-file count, unapplied-draft count, filenames, per-file draft status and applied/draft UTF-8 byte sizes. The copy’s name and actual saved time remain visible. The summary explicitly excludes changes that failed to save; it does not invent a current-tab save time or provide snapshot history. No persistence format or storage behavior changed.

`npm run test:e2e -- --project=functional recovery.spec.ts --grep '@critical restores exact'` failed against the prior UI because the file summary did not exist (`sandbox/recovery-preview-before.log`; screenshot and trace in `sandbox/recovery-preview-before-artifacts/`). The strengthened scenario saves two distinguishable drafts, waits for the latest exact durable project, reloads and checks its saved timestamp and per-file byte counts (independently computed with Node’s UTF-8 byte counter, including Unicode). Reading the summary must not mutate the stored record. Restoring compares exact applied source, latest drafts and both saved view layouts.

`npm run test:e2e -- --project=functional recovery.spec.ts` passed all ten recovery scenarios after the change (`sandbox/recovery-preview-functional.log`). `npm run check` passed formatting, architecture, TypeScript, 160 Vitest tests and ten Node checks (`sandbox/recovery-preview-check.log`); production builds passed through Playwright. The four expanded pending-copy captures were regenerated with `npm run test:e2e -- --project=visual --grep 'recovery conflict and saved-copy' --update-snapshots` and inspected in light/dark at 1440px/768px (`sandbox/recovery-preview-visual-generate.log`). The first generation attempt was stopped because the visual fixture imports nine compressor files, while its new summary assertion incorrectly expected the two-file Workshop used by the functional test. The expectation now derives the known file count from the imported fixture; the application behavior and comparison tolerances were not changed. That stopped run is recorded in `sandbox/recovery-preview-visual-fixture-initial.log`. Normal comparison with `npm run test:e2e -- --project=visual --grep 'local recovery dialog|recovery conflict and saved-copy'` passed all eight recovery scenarios without updates (`sandbox/recovery-preview-visual-compare.log`). A preceding comparison process exited with signal 143 after five passes and no assertion failure; its partial log is retained as `sandbox/recovery-preview-visual-interrupted.log`. The leftover test server was stopped before the complete rerun.

These bounded successful-save and conflict checks do not reproduce or establish the cause of the report’s original condensed/older copy. Full suites outside recovery, manual screen-reader testing, hosted CI and deployment were not performed. These local checks do not establish a deployed fix.

## Recovery conflict clarity — issue #23

Recorded 2026-11-11 on `fix/recovery-conflict-clarity`, based on develop `76627b0`. This addresses [WP26 Bug 10 / issue #23](https://github.com/Open-MBEE/sysml_view_editor/issues/23) locally; it does not establish a fix for stale exploration messages or silent recovery data loss.

The real two-tab scenario reproduced the ambiguous conflict message. The existing compare-and-swap and Retry behavior already preserved both copies; the explanation was incomplete. Conflict text now states that Retry only checks the saved copy and does not replace the live project. After Retry, banner and settings show the pending copy’s project name and saved timestamp and explain Restore versus Discard. The live project has no invented save time. Storage policy, recovery envelope and project format are unchanged.

`npm run test:e2e -- --project=functional recovery.spec.ts --grep 'second tab'` failed against the original implementation at the new Retry-explanation assertion (`sandbox/recovery-conflict-before.log`; trace and screenshots retained in `sandbox/recovery-conflict-before-artifacts/`). The expanded suite initially passed nine scenarios and failed the restore-choice fixture: its earlier download had marked the project saved, so the dirty-work confirmation under test was not required. The corrected fixture makes a new undownloaded edit and waits for the actual confirmation, then verifies Cancel preserves it before accepting Restore. That initial failure is retained in `sandbox/recovery-conflict-functional-initial.log` and `sandbox/recovery-conflict-initial-artifacts/`.

`npm run test:e2e -- --project=functional recovery.spec.ts` then passed all ten scenarios, including exact source/draft/layout preservation in both tabs, Retry without replacement, cancelled and accepted Restore, explicit Discard with queued local saving, denied storage, malformed records, clean-state clearing and failed real-engine restore (`sandbox/recovery-conflict-functional.log`). `npm run check` passed formatting, architecture, TypeScript, 160 Vitest tests and ten Node checks (`sandbox/recovery-conflict-check-final.log`). Fresh production builds passed through Playwright.

Eight new screenshots were generated with `npm run test:e2e -- --project=visual --grep 'recovery conflict and saved-copy' --update-snapshots` and inspected at 1440px/768px in light/dark (`sandbox/recovery-conflict-visual-generate.log`). Their foreign IndexedDB record and timestamp are controlled presentation fixtures; the manager, compare-and-swap, Retry and dialog interactions are real. Normal comparison with `npm run test:e2e -- --project=visual --grep 'local recovery dialog|recovery conflict and saved-copy'` then passed all eight scenarios without baseline updates (`sandbox/recovery-conflict-visual-compare.log`). The scenarios assert dialog bounds, keyboard focus containment and Escape return. Full functional/visual coverage outside recovery and manual assistive-technology testing were not run. No commit, hosted CI result, deployment or issue closure is implied.

## Editor guide review

Recorded 2026-11-10. The guide now explains how to reveal collapsed Explorer and model representation controls hidden by Code layout. Each step supplies the dialog's accessible description and a polite, atomic live region. Removing the custom close-focus override lets outside clicks keep focus on the chosen control; Escape, Close and Done still return focus to Guide. These changes do not switch layout or modify project data.
Expand Down
59 changes: 58 additions & 1 deletion src/lib/workbench/recovery/RecoveryControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,11 +190,66 @@ function RecoveryErrors({ recovery }: { recovery: LocalRecovery }) {
) : null;
}

function RecoverySnapshot({ state }: { state: RecoveryState }) {
if (!state.pending || !state.entry) return null;
const project = state.entry.project;
const applied = new Map(
project.documents.map((file) => [file.name, file.text]),
);
const encoder = new TextEncoder();
const files = project.drafts.map((file) => {
const text = applied.get(file.name);
return {
name: file.name,
unapplied: file.text !== text,
appliedBytes: text === undefined ? null : encoder.encode(text).length,
draftBytes: encoder.encode(file.text).length,
};
});
const drafts = files.filter((file) => file.unapplied).length;
return (
<div className="text-sm text-muted-foreground break-words">
<p>
Saved copy: {project.name}. Saved at{' '}
<time dateTime={new Date(state.entry.savedAt).toISOString()}>
{new Date(state.entry.savedAt).toLocaleString()}
</time>
.
</p>
<details>
<summary className="cursor-pointer">
{files.length} source {files.length === 1 ? 'file' : 'files'} ·{' '}
{drafts} unapplied {drafts === 1 ? 'draft' : 'drafts'}
</summary>
<ul
aria-label="Recovery source files"
className="mt-2 max-h-40 space-y-2 overflow-y-auto"
>
{files.map((file) => (
<li key={file.name}>
<strong>{file.name}</strong> —{' '}
{file.unapplied ? 'Unapplied draft' : 'Applied source'}
<br />
{file.appliedBytes === null
? 'No applied source'
: `${file.appliedBytes} bytes applied`}{' '}
· {file.draftBytes} bytes draft
</li>
))}
</ul>
<p>
Sizes are UTF-8 bytes. Changes that failed to save are not included.
</p>
</details>
</div>
);
}

function recoveryStatus(state: RecoveryState): string {
if (state.pending && !state.entry && state.error)
return 'The saved recovery cannot be restored. It is retained until you discard it; retry to check it again. New edits are not saved locally until you choose.';
if (state.pending)
return 'Choose whether to restore or discard the saved recovery. New edits are protected in this session but are not saved locally until you choose.';
return 'Restore recovery replaces any open project with the saved copy. Discard recovery deletes the saved copy and keeps your current project. New edits remain in this tab but are not saved locally until you choose. Download your current project before restoring if you need to keep its changes.';
if (!state.enabled)
return 'Local recovery is off. Existing saved recovery is retained until you discard it.';
if (state.status === 'saving') return 'Saving recovery locally…';
Expand Down Expand Up @@ -228,6 +283,7 @@ export function RecoveryBanner(props: RecoveryControlsProps) {
<p className="text-muted-foreground">
{recoveryStatus(recovery.state)}
</p>
<RecoverySnapshot state={recovery.state} />
<RecoveryErrors recovery={recovery} />
</div>
<RecoveryActions {...props} />
Expand Down Expand Up @@ -270,6 +326,7 @@ export function RecoveryButton(props: RecoveryControlsProps) {
<p role="status" className="text-sm">
{recoveryStatus(recovery.state)}
</p>
<RecoverySnapshot state={recovery.state} />
<RecoveryErrors recovery={recovery} />
<RecoveryActions {...props} />
<div className="dialog-actions">
Expand Down
6 changes: 3 additions & 3 deletions src/lib/workbench/recovery/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export function createRecoveryManager(
) {
conflict = true;
throw new Error(
'Another tab changed local recovery. Retry to inspect its saved copy before continuing.',
'Another tab changed local recovery. Your current project remains in this tab. Retry recovery only checks the saved copy; it does not replace your current project. Download your project to keep a separate backup before choosing which copy to keep.',
);
}
expected = null;
Expand All @@ -100,7 +100,7 @@ export function createRecoveryManager(
if (!(await storage.compareAndSwap(expected, raw))) {
conflict = true;
throw new Error(
'Another tab changed local recovery. Retry to inspect its saved copy before continuing.',
'Another tab changed local recovery. Your current project remains in this tab. Retry recovery only checks the saved copy; it does not replace your current project. Download your project to keep a separate backup before choosing which copy to keep.',
);
}
expected = raw;
Expand Down Expand Up @@ -192,7 +192,7 @@ export function createRecoveryManager(
conflict = true;
fail(
new Error(
'Another tab changed local recovery. Retry to inspect its saved copy before discarding.',
'Another tab changed local recovery. Nothing was discarded. Retry recovery only checks the saved copy; it does not replace your current project. Download your project to keep a separate backup before choosing which copy to keep.',
),
);
return;
Expand Down
Loading
Loading