Skip to content

feat(planning): safe side-by-side planning in several tabs (save conflicts, cross-tab sync, logout, db upgrades) - #569

Open
jplacht wants to merge 1 commit into
mainfrom
feat/multi-tab-safety
Open

jplacht wants to merge 1 commit into
mainfrom
feat/multi-tab-safety

Conversation

@jplacht

@jplacht jplacht commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

What and why

People plan with several tabs side by side, but each tab kept its own copy of the persisted stores: a save in one tab silently overwrote another tab's, preference changes reset other tabs' keys, logout didn't stick, lists went stale, and after a deploy a new tab could hang on the IndexedDB upgrade until every old tab was closed. Now several tabs work side by side without silently losing data, see each other's saved changes, share one login state and never block each other after a deploy.

Changes

  • Plans, empires and CXs send the version they were loaded with (base_modified_at); a save over another tab's newer one opens a dialog listing both sides' changes (Save as new plan / Overwrite / Reload), a deleted one offers Save as new plan or a notice
  • Planning mutations broadcast on a BroadcastChannel; other tabs refetch their lists and reload open editors, or keep unsaved edits with a "Saved in another tab" notice
  • One login state across tabs through the storage event; an account switch stops the old tab's persisted stores from writing, so it can't put the old login back
  • Preference PATCH sends only changed keys and per-uuid overrides (null removes one); preferences are fetched on every app start; the frontend override cleanup is gone (the backend drops a deleted plan's)
  • IndexedDB blocking/blocked handlers: old tabs release the DB and show the update notification
  • Analytics: plan:save_conflict, empire:save_conflict, exchange:save_conflict, app:remote_change, app:session_change, app:db_blocked

How it was verified

  • QA: PASS after four rounds (round 1 full, rounds 2–4 scoped). pnpm test && pnpm test:components, pnpm tsc, pnpm lint, pnpm knip all pass.
  • All 16 acceptance criteria walked in the running app with two real tabs (e2e stack, backend from main), incl. every dialog option, deleted objects, live refresh with and without unsaved edits, preferences from both tabs, logout/account switch (with a forced old-token refresh in the abandoned tab), and a DB upgrade at version+1.
  • UI audit: plan, empire, exchanges, management, profile and others at all widths: no regressions versus baseline.
  • Spec with the full QA report: specs/2026-10-03-multitab-02-frontend.md (workspace repo).

Screenshots

Two tabs staged with Playwright (two pages in one browser context).

Desktop (1440)

Plan saved in another tab: both sides' changes, FRM changed in both Unsaved plan edits kept, "Saved in another tab" notice Saving a plan deleted in another tab
Plan saved in another tab: both sides' changes, FRM changed in both Unsaved plan edits kept, "Saved in another tab" notice Saving a plan deleted in another tab
Empire configuration conflict Empire form keeps unsaved edits, inline notice CX conflict on the Exchanges page
Empire configuration conflict Empire form keeps unsaved edits, inline notice CX conflict on the Exchanges page
Old tab after a newer version opened the database Tab kept open after another tab logged in as someone else Profile preferences, shortened description
Old tab after a newer version opened the database Tab kept open after another tab logged in as someone else Profile preferences, shortened description

Mobile (375)

Plan saved in another tab: both sides' changes, FRM changed in both Unsaved plan edits kept, "Saved in another tab" notice Saving a plan deleted in another tab
Plan saved in another tab: both sides' changes, FRM changed in both Unsaved plan edits kept, "Saved in another tab" notice Saving a plan deleted in another tab
Empire configuration conflict Empire form keeps unsaved edits, inline notice CX conflict on the Exchanges page
Empire configuration conflict Empire form keeps unsaved edits, inline notice CX conflict on the Exchanges page
Old tab after a newer version opened the database Tab kept open after another tab logged in as someone else Profile preferences, shortened description
Old tab after a newer version opened the database Tab kept open after another tab logged in as someone else Profile preferences, shortened description

Deploy order

Needs PRUNplanner/backend#77 (merged) deployed first; it is backward compatible. This PR can deploy any time after it. The first release with the blocking handler can still hit one blocked upgrade from tabs of the previous release.

🤖 Generated with Claude Code

…licts, cross-tab sync, logout, db upgrades)

Lost edits, lost preferences, logout that doesn't stick, stale lists and
the IndexedDB upgrade hang when several tabs are open.

- Plans, empires and CXs send the version they were loaded with
  (base_modified_at); a save over another tab's newer one opens a dialog
  listing both sides' changes (save as new plan, overwrite, reload),
  a deleted one offers save as new or a notice
- Planning mutations broadcast on a BroadcastChannel; other tabs refetch
  lists and reload open editors, or keep unsaved edits with a notice
- One login state across tabs through the storage event; an account
  switch stops the old tab's persisted stores from writing
- Preference PATCH sends only changed keys and per-uuid overrides;
  preferences are fetched on every start, the frontend override cleanup
  is gone
- IndexedDB blocking/blocked handlers: old tabs release the DB and show
  the update notification
- Analytics: save_conflict, app:remote_change, app:session_change,
  app:db_blocked

Spec: specs/2026-10-03-multitab-02-frontend.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@netlify

netlify Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for prunplanner-preview ready!

Name Link
🔨 Latest commit c48a89d
🔍 Latest deploy log https://app.netlify.com/projects/prunplanner-preview/deploys/6ac0fb35f928bf000861db40
😎 Deploy Preview https://deploy-preview-569--prunplanner-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

jplacht added a commit that referenced this pull request Oct 3, 2026
@codacy-production

codacy-production Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 309 complexity · 1 duplication

Metric Results
Complexity 309
Duplication 1

View in Codacy

🟢 Coverage 97.95% diff coverage · +0.03% coverage variation

Metric Results
Coverage variation ✅ +0.03% coverage variation (-1.00%)
Diff coverage ✅ 97.95% diff coverage

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (c165a93) 3715 3628 97.66%
Head commit (c48a89d) 4020 (+305) 3927 (+299) 97.69% (+0.03%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#569) 341 334 97.95%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant