feat(onboarding): shared getting-started state, live re-read and data-change channel (ADR 0022) #402

Merged
maximus merged 4 commits from issue-396-onboarding-shared-state into main 2026-10-09 01:10:05 +00:00

4 commits

Author SHA1 Message Date
le king fu
21c46b6380 test(onboarding): pin the provider hook's React wiring
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m49s
renderToStaticMarkup runs no effect, so the hook's own wiring was guarded by
no test: the route re-read (pathname in the deps), the data-channel
subscription and its cleanup, the card latch stored during render, and the
order "write the preference, then update the state" of hide, show, skip and
resume. React's hooks are stubbed so the hook runs as a plain function on a
prepared state, its effects captured and run by hand.

Resolves #396

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 07:39:00 -04:00
le king fu
68b90f5f16 docs: ADR 0022 and changelog for the data-change channel
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m46s
ADR 0022 records the channel: its scope, the writes that notify and when,
the 150 ms wait on the subscriber side, stale answers ignored, and who may
subscribe. architecture.md gets its row in the ADR table, plus the rows this
change made wrong (onboardingService, useOnboardingChecklist, the new
dataEvents service, the contexts folder). The changelogs record rule D9: a
profile that already had transactions hides the getting-started checklist
the first time it is opened with this version.

Resolves #396

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 07:34:02 -04:00
le king fu
dcf3b013dc feat(onboarding): share the getting-started path through a provider
The checklist state moves from DashboardPage to OnboardingProvider, mounted
in AppShell around the routed page; pages read it through useOnboarding().
The reducer stays in useOnboardingChecklist:

- loads are numbered and a stale answer is ignored;
- HIDE, SHOW, SKIP and RESUME win over the snapshot of a load started
  before them (#384);
- a refresh keeps the counters and the ready status, and a failed re-read
  after a first success keeps the last state; only the start of a failure
  streak is logged;
- re-read at mount, on every route change and 150 ms after a burst of
  data events.

onboardingService: getOnboardingSnapshot replaces getOnboardingCounters and
is a pure read (skipped steps included, one preference per step, filtered on
the six keys and the value "1"); initializeGuidedPath applies rule D9 once
per profile in one transaction (hidden first, then the flag, ROLLBACK on
failure), awaited once by the first load through a promise shared by
StrictMode's double mount; skipStep / resumeStep are idempotent.

computeChecklist counts skipped rows; the new pure onboardingPath module
holds nextStep, CARD_PAGES (empty for now), stepsOfPage, the card latch,
bannerMode and readOnboardingNavState. The dashboard reads the context and
keeps its compact bar for now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 07:33:57 -04:00
le king fu
4febed9eab feat(data): announce counter-moving writes on a data-change channel
Add src/services/dataEvents.ts (ADR 0022): notifyDataChanged() and
subscribeDataChanged(), without React, on the model of logService.subscribe.
A throwing subscriber is logged and does not stop the others.

Notify after every successful write of the tables the getting-started
counters read, plus the import folder: setImportFolder, createSource,
deleteSource, createImportedFile, updateFileStatus, both import deletions,
insertBatch, updateTransactionCategory, applyKeywordWithReassignment (after
COMMIT), the four budget writes and runRestore (after COMMIT, one point for
the three restore modes). executeImport notifies once more when it ends,
succeeded or not. A failed write notifies nothing; transactional writes
notify once withTransaction has returned. applyTemplate notifies once for
the whole template through a private writeBudgetEntry helper.

Each service suite proves the notification after a success and its absence
after a failure; executeImport is driven as a plain function with React's
hooks stubbed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 07:33:47 -04:00