| D-M6-16-1 |
2026-10-25 |
StatsSubscription of sync_engine (GatewayConnection.stats, SyncEngine.stats) keeps the wanted subscription (none, the instance or one space) and the one the server holds as far as the client knows. subscribe and unsubscribe send at once on the identified connection, else after the next session start; after ready (a new gateway session after identify) the server holds none, so the wanted one is sent again; after resumed the server restored its own (04 6.3), so only a change made while disconnected is sent. An error with op stats.subscribe (a refused subscription, a refused restore on resume, or the loss of stats.read, 25 5.1 item 3) clears the wanted one, sends stats.unsubscribe (at once, or after the next resumed when the connection is gone) and emits StatsLiveEnded with the code; it is not sent again until the app subscribes anew. The unsubscribe is needed because the server keeps an earlier subscription when it refuses a replacing one and the error does not name the request it answers; an unsubscribe without a subscription is answered with nothing (red team of M6-16). The unsubscribe counts against rl.read and may itself be refused (error {op: stats.unsubscribe}, the class is part of the core contract): then the next resumed sends it again, and every stats.live that arrives while the app wants none sends stats.unsubscribe again, at most one per frame (delta red team of M6-16). Not covered: a stats.subscribe written to a socket that dies before the server reads it, followed by resume, leaves the old scope restored while the client holds the new one; it heals on the next scope change or ready. A frame whose scope is not the wanted one (still on the way after a change) is dropped. |
Brief Design note 1; the server stores the subscription in the gateway session and checks it again on resume. |
| D-M6-16-2 |
2026-10-25 |
A dispatch of stats.live is taken out in the connection before the session listener, so a live frame never reaches the dispatcher, the cursor or the local database; it goes decoded to StatsSubscription.updates and is replaced by the next one. |
Brief Design note 1; ephemeral events have no seq and are lost on reconnect by design (04 2.1). |
| D-M6-16-3 |
2026-10-25 |
Mobile foreground only: the app does nothing of its own; the connection closes with 1000 in the background (15 7.2 Suspended), so no frame reaches the device (the gateway session keeps the subscription for resume), and the views turn stale after viz.stale_after. Back in the foreground resume restores it, or the manager subscribes again after identify. The app source EngineLiveFrameSource subscribes on start and unsubscribes on stop. |
Brief Design note 3; 25 7.1. |
| D-M6-16-4 |
2026-10-25 |
The console polls getStatsLive with the scope at once and every viz.live_poll_interval (PollingLiveFrameSource). A hidden tab (document.visibilityState, the new port PageVisibility) does not poll and polls at once when shown. feature_disabled, forbidden and not_found end polling and pass the code to the view (a retry gives the same answer); every other failure (rate_limited, network, server) doubles the wait from the interval up to viz.stale_after and keeps the view's stale rule; a frame brings the interval back. |
Brief Design note 4; R-VZ-06; the console has no gateway (ADR 0028). |
| D-M6-16-5 |
2026-10-25 |
VZ-T16 is tools/arch-check/no_webview.py, run by make arch after the dependency rules. It fails on a WebView package in the manifest of apps/client (dependencies, dev dependencies, overrides) or in the workspace lock (webview_flutter, flutter_inappwebview, webview_windows, desktop_webview_window and any name containing webview), on HtmlElementView, HTMLIFrameElement, IFrameElement or createElement('iframe') in a Dart file of the app, and on WKWebView, android.webkit.WebView or WebView2 in its native folders; comment lines and build output are not read. The lock is the workspace's one lock, so a WebView package anywhere in the workspace fails it. |
Brief Design note 5; SEC-VZ-1; one resolution for every member (R-CL-02). |
| D-M6-16-6 |
2026-10-25 |
Differences to the brief (D-W-100, the merged code wins): the hosts share one page LiveScreen and one ApiStatsQuerySource in admin_ui (each host passes its client and its live source), as M6-06 shared ApiAdminStatsSource; admin_ui gets no AdminSection "Live", because Live is not administration: the desktop reaches it from the sidebar group "Live and statistics" (D12) and the console from its navigation entry "Live" of M6-02; the device suites are Flutter integration tests in integration_test/phone_suite_test.dart (the phone tests of M5-02 and M5-03 use no patrol); the console suite is a patrol web test of the admin bundle (integration_test/admin/live_test.dart), which ends TV mode with a click because a simulated key press throws in the browser's test binding (the widget tests and the desktop integration test end it with a key); TV mode is an opaque route on the root navigator, and the tab view is taken down while it runs, because the views share one live source; the views query the last 24 hours up to the last full minute before the opening (getStatsTimeline accepts only full minutes and at most stats.timeline_max_range, a day by default; the fake server checks both like the server, found by make console-e2e); the fake gateway sends stats.live only when a test asks (sendStatsLive), so "no frame" is a test that sends none. |
One page and one REST source keep the three hosts equal; the rest follows the merged code. |