Skip to content

Wallpapers: Add the Pulse Monitor wallpaper and widget - #1006

Merged
AllTerrainDeveloper merged 5 commits into
trunkfrom
add/pulse-monitor
Oct 14, 2026
Merged

AllTerrainDeveloper merged 5 commits into
trunkfrom
add/pulse-monitor

Conversation

@AllTerrainDeveloper

@AllTerrainDeveloper AllTerrainDeveloper commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

What

openstation-pulse-monitor-short-v10.mp4

A heart monitor wired to the site, as a built-in PixiJS wallpaper (wp-pulse) and a widget (desktop-mode/pulse). Every HTTP request and every signal the desk can see is a beat on a red ECG trace over black, labelled with its cause; failed beats are amber and say why; and the rhythm is a health diagnosis.

  • Beats for everything: wp.os.fetch, raw fetch / XHR / sendBeacon in the shell, fetch/XHR inside every window (via os.iframe.network-completed — the Posts app's block editor included, since Windows: Relay embedded pages' error and network reports #1005), window pages without the bridge (resource timing), page loads and in-window form posts, WordPress Heartbeat, assets, window events, clicks/typing, messages, toasts, uploads, JS errors.
  • Server feed (includes/pulse-monitor.php, over Heartbeat): visitors, bots, comments, publishes, orders, logins (failed ones amber), anonymous API calls, cron, other people's admin traffic. Records only while someone has a monitor mounted (a 3-minute lease), skips the listener's own requests, stores no URLs/IPs/UAs, admins only by default (openstation_pulse_capability).
  • Labels: "request", "visit", "comment", "heartbeat ×3"… Amber beats add the reason: request · 404, heartbeat · timeout, error · TypeError: …, PVC: slow responses (p95 3.1 s).
  • Diagnosis: sinus → PVCs → atrial fibrillation → ventricular tachycardia → flatline, from request failures, latency, JS errors, auth loss, Site Health, 5xx share and overdue cron. Capped by kind of trouble: only substantial failure (~16 % of requests) draws VT; slowness and JS errors top out at AF; misconfiguration at PVCs. Latency samples from hidden tabs are ignored.
  • Numbers: req/min, median ms, error %, visits/min — measured values only.
  • Settings (one bag, both faces): sweep/scroll, colours, speed, intensity, line width, glow, line height, echo lines, grid, numbers, labels, beep/flatline alarm, resting heart rate, diagnosis on/off, and a switch per source (19).

How it's built

One bundle (os-pulse-monitor) publishes both faces, so the engine loads once. A reference-counted hub (hub.ts) wires the signal taps, the Heartbeat field and the health monitor once per page however many monitors are mounted; each monitor (monitor.ts) has its own rhythm + Pixi scene and applies its own muting. The widget's gear opens the same form (config.ts) the wallpaper's settings dialog renders, writing through the OS Settings store like that dialog does.

New public surface (all Experimental, documented in docs/pulse-monitor.md, hooks-reference.md, javascript-reference.md):

  • PHP: openstation_pulse_record(); filters openstation_pulse_capability, _request_kind, _is_bot, _event, _health.
  • JS: activity channel pulse/beat ({ source, amplitude, error, detail }), action os.pulse.beat, Heartbeat field openstation_pulse.

Testing

  • Vitest: tests/vitest/pulse-monitor.test.ts (27) — settings, diagnosis + severity caps, rhythm scheduling and morphologies, spike text, the shared hub (two monitors, one set of taps). Neighbouring wallpaper/widget suites pass.
  • PHPUnit: tests/phpunit/tests/pulseMonitor.php (22) — lease gate, classification, recorder, own-request exclusion, Heartbeat relay, health snapshot, script + widget registration. phpcs clean.
  • tsc --noEmit and eslint clean.
  • Live, on a local site in a visible headless Chromium (no stand-ins), all 22 request paths drew a beat: the trace animates on its own; wp.os.fetch, raw fetch, XHR, sendBeacon, admin-ajax, image; a 404 (amber, labelled); Heartbeat; an admin window's load, fetch, XHR and form POST; a bridge-less 404 window's load, fetch and XHR; the block editor's apiFetch; and real visitor, Googlebot, anonymous REST, comment and wp-cron.php requests sent with curl.

🤖 Generated with Claude Code

Open WordPress Playground Preview

AllTerrainDeveloper and others added 4 commits October 13, 2026 12:51
A PixiJS ECG wired to the site, as a canvas wallpaper (`wp-pulse`) and
a widget (`desktop-mode/pulse`) that ship in one bundle and share one
engine, one signal hub and one settings bag.

Every request the shell makes or sees is a labelled beat: wp.os.fetch,
raw fetch/XHR, sendBeacon, fetch/XHR inside admin windows, in-window
navigations and form posts, WordPress Heartbeat, assets, window events,
input, messages, toasts, uploads and errors. A Heartbeat-relayed server
feed (includes/pulse-monitor.php) adds what only the server sees:
visitors, bots, comments, publishes, orders, logins, API calls, cron,
other people's admin traffic, and the listener's own block-editor
requests, which the shell cannot observe.

Failed beats are drawn amber and say why ("request · 404", "heartbeat ·
timeout", "PVC: slow responses"). The rhythm is a health diagnosis —
sinus, PVCs, atrial fibrillation, ventricular tachycardia, flatline —
capped by kind of trouble so that only failing requests reach VT.
Latency samples from hidden tabs are ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The block editor's windows are cross-origin isolated, and the shell's
window bridge does not re-dispatch their `os-iframe-network` reports as
`os.iframe.network-completed` — so the editor's REST, autosave and
Heartbeat traffic never reached the trace in real time. Read those
reports straight off the message (same-origin, sent by one of the
desk's own iframes) and drop the server-side workaround that relayed
the listener's own editor requests one Heartbeat tick late.

The diagnosis now requires substantial failure (~16 % of requests)
before drawing ventricular tachycardia, and failure or slowness before
fibrillation; abnormal beats print the reason that justified the
rhythm. A slow, misconfigured site with a few 404s used to read as VT
"caused by" slowness.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A window page that never reached `admin_footer` — a 404, a fatal error —
has no chromeless bridge, so its fetch/XHR were never reported. Count
those from the frame's resource timing instead, and keep skipping it
for bridged pages, which report their own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since #1005 the shell relays the chromeless bridge's network reports
for admin pages embedded in native windows too, so the block editor's
traffic reaches the hook. Subscribe to it again instead of reading the
bridge's messages directly.

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

Copy link
Copy Markdown
Collaborator Author

@claude can you review?

@claude

claude Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Claude finished @AllTerrainDeveloper's task in 28s —— View job


Review of PR #1006: Pulse Monitor

This is a partial review. I read includes/pulse-monitor.php in full and grepped the docs for version annotations. I did not read the TypeScript under src/plugins/pulse-monitor/, the tests, or the other changed files. I also didn't run phpcs, lint, typecheck or the test suites. Treat the verdict below as covering the PHP feed only.

Must fix

  • Version annotations are banned (AGENTS.md, "No version-history annotations"). includes/pulse-monitor.php has five @since 1.2.0 tags, on the filters openstation_pulse_is_bot, _request_kind, _event, _capability and _health. Remove them. Keep the status label (Experimental) in the docs. docs/pulse-monitor.md has no version stamps, so only the PHP needs changing.

Should fix

  • Formatting. In openstation_pulse_heartbeat_received() (around lines 469–471) the = signs of $now, $cutoff and $windowed are misaligned. $now and $cutoff are padded for a name shorter than $windowed. phpcs may flag this, so run npm run lint:php:fix and skim the diff.
  • Duplicated log reading. openstation_pulse_append() and openstation_pulse_heartbeat_received() each parse the log option and filter entries by the time window with near-identical code. Extract a small openstation_pulse_read_log() helper that returns seq and the windowed entries.

Worth considering (not blockers)

  • Lost updates. openstation_pulse_append() and openstation_pulse_touch_listener() read an option, modify it and write it back, with no lock. Under concurrent traffic, which is the case this feature is meant for, some beats and lease writes will be dropped. The log is best-effort and the lease has a half-life, so this is acceptable. I'd say so in the file docblock or in docs/pulse-monitor.md.
  • Cursor reset. If the log option is deleted or reset, seq restarts at 0. A client holding a larger since then sees no events until seq passes that value. When $since > $seq, reset the client's cursor to $seq.
  • Cost of the lease check. With no listener, each request pays one autoloaded option read plus a loop over the leases. That matches the stated cost model.
  • Hook coverage. wp_insert_comment fires for imports and programmatic inserts as well as visitor comments. That is fine if intended, but the docs say "comments" without that caveat.
  • Privacy. The privacy claim holds for the PHP side: no URLs, IPs or user agents are stored. The user field stays in the option, but the relay strips it. Good.
  • Access. The feed is gated at manage_options through a filter, and current_user_can is checked before the lease is touched. The opt-in key, the lease and the own-request exclusion all look correct.

Not covered

The following still need review: the TS engine and hub (hub.ts, signals.ts, health.ts), the settings and widget wiring, the wallpaper registration in includes/wallpapers.php, vite.config.js, and the tests. Ask if you want a second pass on those.
· branch add/pulse-monitor

@epeicher

Copy link
Copy Markdown
Collaborator

Second-pass review: TypeScript engine, hub and widget, plus PHP gaps

This comment only lists findings that the earlier review did not cover. It skips the @since tags and the lost updates, which that review already raised. npm run test:js -- pulse-monitor passes (27/27). On the TS side, signals.ts only listens: it does not wrap fetch, XHR or sendBeacon, so it cannot conflict with other patches.

Should fix

  • Every window page tapped stays in memory (signals.ts ~355-375). scanFrames() adds a PerformanceObserver and input listeners to each new same-origin iframe document. Their cleanup closures keep the old document alive. The disposers list is only emptied when the hub's refcount drops to 0, which does not happen while the wallpaper is on. So every in-window navigation, block-editor load and closed window stays in memory for the whole session. Release a document's taps when it unloads (pagehide on the old document) or when its iframe is removed.
  • An aborted request inside a window is drawn as a network failure (signals.ts ~265-273). The chromeless bridge reports a rejected fetch (an AbortError included) with status 0 and failed: true, and XHR loadend also fires on abort. The shell path skips aborted (~251), but the os.iframe.network-completed path does not. A search-as-you-type field in a window that aborts its previous request therefore produces amber "network error" beats. Enough of them push the diagnosis towards VT, and five in a row count as a 'down' flatline with the alarm. Either have the bridge send an aborted flag, or ignore status-0 reports from aborts.
  • One long request makes the site look slow (health.ts ~237-251). Every fetch/XHR duration counts towards latency, including calls that are long by design: /ai/search, uploads, streamed responses. With a minimum of only 4 samples, p95 is close to the maximum. A single 20 s AI call is enough to show "PVC: slow responses (p95 20.0 s)" for a minute. Cap the duration counted per request, exclude silent or streaming requests, or require more samples before reporting p95.
  • A Web Audio context opens even with sound off (readout.ts ~137-154). Beeper creates and resumes an AudioContext on the first pointer or key press, whatever the setting, and sound defaults to off. The context keeps the audio device open for the rest of the session. Create it only when sound is enabled.
  • Labels sit in the wrong place in RTL locales (scene.ts 280, with the translate( -50%, … ) at 112/119). The label's x is measured from the canvas's left edge but written to insetInlineStart. Under dir="rtl" that resolves to right, so labels appear mirrored away from their spikes. Use left.
  • Paused monitors keep queueing beats (rhythm.ts ~217-227, monitor.ts ~116). rhythm.push() still runs while the scene is paused (hidden tab, suspended wallpaper), but the queues are only pruned in step(), which needs the ticker. With reduced motion on, the ticker never runs, so events/pending grow for the whole session. After a long pause, the backlog plays as a burst. Drop pushes while the scene is not animating.
  • The widget's gear is a raw <button> with hardcoded colours (widget.ts ~47-66, 149-156). AGENTS.md asks for the os-* components: <os-button variant="ghost"> with an aria-label covers an icon-only button and picks up the theme tokens.
  • A database write on every request while someone listens (includes/pulse-monitor.php, openstation_pulse_on_shutdown() / openstation_pulse_append()). The earlier review covered the cost when nobody listens. While a monitor is mounted, every request rewrites a non-autoloaded option of up to 200 serialized rows, including every front-end page view by every visitor. On a busy site, an admin who leaves the wallpaper on means one wp_options UPDATE per page view, contending for the same row. Consider throttling, sampling, or counters in the object cache. At minimum, say this in the cost-model section of docs/pulse-monitor.md.
  • docs/data-model.md has no rows for openstation_pulse_log or openstation_pulse_listeners. AGENTS.md requires every stored name to be listed there.

Nits

  • WooCommerce order notes beat as comments. wp_insert_comment fires for order_note comments, so each order draws its orders beat plus one "comment" beat per note. The earlier review raised imports and other programmatic inserts as a docs caveat; this one shows up on every order. Filtering on comment_type fixes it.
  • The health snapshot's load field is never read. sys_getloadavg() is computed and sent on every tick, but nothing in src/plugins/pulse-monitor/ reads load. Use it or drop it, and update the docs and filter description to match.
  • WP-CLI commands are drawn as bot beats. shutdown fires under WP-CLI, the request falls through to the front-end branch, and an empty user agent counts as a bot. Skip the recorder when WP_CLI is defined.
  • A follow-up scan can outlive the hub (signals.ts ~402). The window.setTimeout( scanFrames, 50 ) call is never cancelled. If the hub is released within that 50 ms, the scan adds taps to the already-emptied disposers list, and they keep beating into the next session.
  • The wallpaper readout ignores the work area (readout.ts ~38-41). It is placed 28px from the layer's bottom-right corner rather than reading --os-work-area-inset-*, so a static dock or the widget column can cover the numbers.
  • The flatline alarm keeps beeping (with sound on) while the tab is hidden or the wallpaper is suspended (monitor.ts ~136-139). The refresh interval is not paused.
  • The gear's settings modal stays open if the widget is removed while it is showing (widget.ts ~104-129).

…ettings

Review fixes (both passes on #1006):
- PHP feed: drop the @SInCE tags; one openstation_pulse_read_log() for
  the writer and the Heartbeat relay; relay everything after a log reset;
  only comments, reviews, pingbacks and trackbacks beat (not order notes);
  skip WP-CLI; drop the unused `load` field; document the write cost and
  the best-effort (lock-free) log in the docblock and docs.
- Signals: release each window document's taps on pagehide or when its
  frame goes away; cancel the follow-up scan on teardown; ignore requests
  the page cancelled itself (the chromeless bridge now reports
  `aborted`, relayed on os.iframe.network-completed).
- Diagnosis: cap a request at 10 s and need 20 samples for p95, so one
  long call cannot make the site look slow.
- Audio context only while sound is on; paused or reduced-motion monitors
  drop beats instead of queueing them; the alarm pauses with the trace.
- Labels positioned with physical left/top (RTL); readout clear of the
  docked pill; the widget gear is an <os-button variant="ghost">, and its
  dialog closes with the widget.
- docs/data-model.md lists openstation_pulse_log and _listeners.

New:
- Work area reservations (wp.os.workArea.reserve / reservedRectOf /
  placementRectOf, Experimental): a band nothing may use — maximize,
  snap, new windows, icons, widgets and notes stay out, and dragging,
  resizing and pinned positions stop at it.
- Wallpaper layout setting: whole wallpaper, or a reserved top band
  (56–300 px, 88 by default; off on phones).
- Issues window (click the numbers in the band or widget, a settings
  button, or the command palette): the diagnosis itemised with Site
  Health links, and every failure with URL, method, status and window.
- The widget keeps its own settings; the trace survives browser
  resizes; smaller numbers; a clock icon on the response time; the
  settings dialog's headings use the dialog's colour tokens.

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

Copy link
Copy Markdown
Collaborator Author

@claude has everything been addressed?

@claude

claude Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Claude finished @AllTerrainDeveloper's task in 38s —— View job


Has everything been addressed?

Mostly yes. I checked each earlier finding against the code on add/pulse-monitor (head 0d0c1db) by grepping, not by reading every file in full. I did not run lint, typecheck or the test suites.

Addressed

  • @since tags: none left in includes/pulse-monitor.php.
  • Duplicated log reading: now openstation_pulse_read_log(), used by both the append path and the Heartbeat handler.
  • Cursor reset: since > seq now resets since to 0 (pulse-monitor.php:486).
  • Lost updates: documented as "best effort" in docs/pulse-monitor.md:128.
  • Order notes and other non-comment types: openstation_pulse_on_comment() filters on comment_type.
  • WP-CLI: the recorder skips when WP_CLI is defined (:315).
  • docs/data-model.md: has rows for openstation_pulse_log and openstation_pulse_listeners.
  • Aborted requests inside windows: the bridge now sends aborted, and signals.ts:283 ignores those reports.
  • Leaked window taps: they are released on pagehide (signals.ts:408).
  • Follow-up scan outliving the hub: the rescan timer is cleared on release (signals.ts:448, :482).
  • Long requests skewing latency: durations are capped at 10 s (LATENCY_CAP_MS) and p95 needs at least 20 samples (health.ts:132).
  • Gear button and modal buttons: they now use <os-button>.
  • Readout position: it now reads the work-area insets (readout.ts:16).
  • Hidden or suspended wallpaper: the flatline alarm interval is cleared by setAnimating(false) (monitor.ts:177).
  • AudioContext: the comment at readout.ts:223 suggests it is now deferred. I didn't confirm that it is only created when sound is on.

Not confirmed

  • RTL labels: I found no insetInlineStart in scene.ts. It looks fixed, but I didn't read the surrounding code.
  • Paused monitors still queueing beats: setAnimating exists in scene.ts, but I didn't check whether rhythm.push() now drops beats while paused.
  • Health snapshot load field: I found no load field in the PHP, so it appears removed. The docs and filter description may still mention it.
  • Widget modal left open if the widget is removed.
  • Database write on every request while a monitor is mounted: I found no throttling in the PHP. I also didn't confirm that the cost-model section of the docs now states the write-per-request cost.

If you want, I can read those last five items properly and report back. Say so and I'll do that.
· branch add/pulse-monitor

@AllTerrainDeveloper
AllTerrainDeveloper merged commit 77e369b into trunk Oct 14, 2026
7 checks passed
@AllTerrainDeveloper
AllTerrainDeveloper deleted the add/pulse-monitor branch October 14, 2026 11:24
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.

2 participants