Skip to content

feat: keep every display in step with one revision counter per kind of data (#325) - #339

Merged
jherforth merged 1 commit into
mainfrom
feat/household-sync
Oct 26, 2026
Merged

jherforth merged 1 commit into
mainfrom
feat/household-sync

Conversation

@jherforth

Copy link
Copy Markdown
Owner

Implements #325, with the decisions from the issue:

  • Counters: one per kind of data (coarse).
  • Check interval: a fixed 15 seconds.
  • Phones: they check only when they become visible.

What it does

A change on one display (a chore ticked, a prize approved, vacation turned on, a calendar event added) shows on every other display:

  • within 15 seconds on any install;
  • within a second where the event stream reaches the browser.

The nightly job and calendar sync reach every display the same way.

Server: utils/householdChanges.js

  • The counters: one revision counter each for chores, users, prizes, settings and calendar.
  • Writes through a request: an onResponse hook moves the counters for every successful write under the household routes, so no route can forget to.
    • Clam totals: a chore or prize write also moves users, because it changes clam totals.
    • Excluded: reads, failed writes, device-scoped routes and plugin storage move nothing, and neither do the two POSTs that only read (/api/settings/search, a calendar source's test).
  • Writes without a request move them explicitly:
    • the nightly job;
    • calendar sync, only when a source's cached events actually changed;
    • calendar routines, only when a sync added, retimed or removed days;
    • the demo reset.
  • Clock-only changes need no counter: a snooze ending, an expiry and midnight are worked out by each display.
  • GET /api/changes: { chores, users, prizes, settings, calendar } with an ETag, and 304 when nothing moved. The counters start from a random base, so after a restart every display reloads everything once.
  • The event stream: each burst of changes goes out as household.changed { domains, revisions }, coalesced over 100 ms. It's a new catalog event that plugins can declare.

Client: utils/householdChanges.js

  • A wall display or desktop checks every 15 seconds while visible, and at once when it becomes visible again.
  • A phone (the mobile layout) checks only when it becomes visible.
  • The stream's household.changed feeds the same notification, so a change is reported once, whichever saw it first.
  • What subscribes:
    • the Chores widget: chores, users, prizes or settings. It runs one batched reload, and the reload guard from perf(client): instant chore toggles and fewer widget re-renders #324 keeps a tap in progress from flickering.
    • the calendar and the screensaver agenda: calendar.
    • household settings (appearance, vacation, control limits): settings. The 5-minute poll stays as a safety net.
    • plugins that declare household.changed: they're told which kinds changed.

Left out on purpose

  • The shared data store from the issue's phase 2: one per-page cache that widgets read instead of fetching their own copy. It would cut duplicate fetches between widgets and on tab switches, but every display already stays in step without it. It's a larger refactor of each widget, better done on its own.
  • Admin Panel forms don't reload when another display changes something, because that would overwrite edits in progress.
  • The tapping display reloads once more on its next check, after its own write moved the counter. It's harmless, and suppressing it would mean tracking which writes were whose.

Worth knowing: every display now keeps the event stream open, not only those with event-subscribing plugins. Where the stream doesn't reach the browser, nothing breaks; the 15-second checks carry it.

Testing

  • Edge, two displays on one server, demo data. 8 of 8 checks pass:

    • without the event stream (blocked in both browsers): a chore ticked in the kitchen shows in the hallway in 12.7 s, and a household appearance change in 14.0 s;
    • the checks: the hallway checks every 15.0 s and gets 304 while nothing changes;
    • with the event stream: the tap shows in 0.1 s;
    • a phone (390 px) left open made no checks in 20 s, and caught up 0.1 s after becoming visible again;
    • page errors: none.
  • Server tests:

    • householdChanges.test.js: the route mapping, reads and failures, coalescing, the random base;
    • householdChangesRoutes.test.js, on a real server: 304 with the ETag, each write moving its kinds (a completion moves chores and users), reads and failures moving nothing, the nightly job, and the stream event;
    • a calendar sync calls back only when the cache changed.

    Server npm test: 656 pass.

  • Client tests: householdChanges.test.js: notifications only for the kinds asked for, the baseline, If-None-Match and 304, a restart, the 15-second cadence and visibility, the phone mode, stopping with the last subscriber, and recovery after a failed check. Client vitest (795), check:i18n and build pass.

  • Dev setup: the dev server is cross-origin, so If-None-Match is allowed and ETag exposed for it. Production is same-origin.

Closes #325.

🤖 Generated with Claude Code

…f data (#325)

A change on one display now shows on the others within 15 seconds on any
install, and within a second where the event stream reaches the browser.

Server (utils/householdChanges.js):
- A revision counter for each kind of household data: chores, users,
  prizes, settings, calendar. An onResponse hook moves them for every
  successful write under the household routes, so no route can forget;
  a chore or prize write also moves users, for clam totals. The nightly
  job, calendar sync (only when the cache changed) and calendar routines
  move them too. Counters start from a random base, so a restart reads as
  everything changed.
- GET /api/changes returns them with an ETag, and 304 when nothing moved.
- Each burst of changes goes out on the event stream as household.changed
  { domains, revisions }, a new catalog event plugins can declare.

Client (utils/householdChanges.js):
- A display checks GET /api/changes every 15 seconds while visible and at
  once when it becomes visible again; a phone checks only when it becomes
  visible. The stream's household.changed is the fast path; both feed one
  deduplicated notification.
- Coarse by design: subscribers learn which kinds moved and reload them.
  The Chores widget reloads on chores, users, prizes or settings; the
  calendar and the screensaver agenda on calendar; household settings
  (appearance, vacation, control limits) on settings; plugins that declare
  household.changed are told.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@jherforth jherforth self-assigned this Oct 26, 2026
@jherforth jherforth added the enhancement New feature or request label Oct 26, 2026
@jherforth jherforth added this to the 1.11 milestone Oct 26, 2026
@jherforth
jherforth merged commit 300dc52 into main Oct 26, 2026
6 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in HomeGlow Kanban Oct 26, 2026
adamecker pushed a commit to adamecker/HomeGlow that referenced this pull request Oct 26, 2026
… revision counter jherforth#339, dimming jherforth#334, Save layout jherforth#336, chore perf jherforth#324, admin jherforth#330/jherforth#331/jherforth#332, PIN hardening jherforth#329

Conflicts resolved taking upstream's rewrite, preserving fork(prod) features:
- displayDim.js: upstream jherforth#334 base plus fork-only kiosk URL dim
  overrides and LCD pixel shift (both absent upstream)
- ChoreWidget/app.jsx/ChoreSchedulesTab: upstream jherforth#324/jherforth#336 rework
@jherforth
jherforth deleted the feat/household-sync branch October 28, 2026 00:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Displays stay in sync: the server says what changed (a change counter per domain, a cheap check, one client data layer)

1 participant