M6-16: Visualizations in the desktop app, console and mobile¶
Status: ready | Depends on: M5-03, M6-04, M6-06, M6-12, M6-14, M6-15 | Milestone: M6
Goal¶
After this ticket the six views of bothy_viz (M6-12) run with real data in all three hosts (25 6, 7.1; ADR 0069): in the desktop app under "Live and statistics" (screen D12) with the live stream through sync_engine (stats.subscribe on the gateway, frames delivered as the ephemeral event stats.live), with TV mode (D13); in the mobile app as screen M10 under Me with one tab per view except TV mode, density touch, the live stream only in the foreground (the app is suspended in the background, 15 7.2), tapping a run card opens the thread; and in the console under "Live" with the frame polled from getStatsLive every viz.live_poll_interval and TV mode. Each host implements the two ports of M6-11 (LiveFrameSource, StatsQuerySource), shows "stale" after viz.stale_after without a new frame, shows costs in TV mode only with viz.tv_show_costs, and offers "Open in Grafana" only when telemetry.dashboard_url is delivered (M6-06), opening it in the system browser or a new tab, never in a WebView or iframe. A static check proves that apps/client contains no WebView package and no HtmlElementView with an iframe (VZ-T16). Integration tests with the fake server and fixtures, patrol web tests of the console and patrol tests on iOS and Android for all views of M10 with the table view and detaching in the background are green. Why: the visualizations are a product function of every app (R-PR-13 pillar "statistics"), built once in bothy_viz and fed by the transport each host has (R-CL-13, R-VZ-06).
Read first¶
docs/spec/25-visualization.md: 1, 4 rule 5, 5.1 (subscription; console polling), 6 (views; 6.6 TV mode desktop and console only), 7.1 (ports and who implements them; mobile only in the foreground), 9.1 (no WebView or iframe in apps; "Open in Grafana" withtelemetry.dashboard_urlin the system browser), 12 (VZ-T16), 15 (viz.live_poll_interval,viz.stale_after,viz.tv_rotation_interval,viz.tv_show_costs).docs/spec/01-product.md: 5.1 D12, D13; 5.3 M10; 5.5 (console navigation "Live").docs/spec/15-client-architecture.md: 3.1 rule 6 (LiveFrameSourcefromsync_enginein the apps, REST in the console), 7.1 (building blocks ofsync_engine), 7.2 (Suspended), 21 (R-CL-13).docs/spec/04-sync-protocol.md(core contract, read only): 6.2 (opsstats.subscribe,stats.unsubscribe), 6.3 (resumerestores subscriptions), 12 item 4.docs/spec/19-security.mdSEC-VZ-1, SEC-T19 (the static check part).docs/api/openapi.yamlgetStatsLive,StatsLiveFrame;docs/api/asyncapi.yamlop.stats.subscribe,op.stats.unsubscribe,event.stats.live.docs/DECISIONS.md: D-W-100 and its extension to M5 and M6 (D-W-120); section 10 (format ofD-M6-16-<n>rows) and the rows of M6-06, M6-11, M6-12, M6-15.- Briefs (merged names win, D-W-100):
M6-11-viz-package.md(ports, stale rule, fixtures),M6-12-viz-views.md(view contract,VizHostOptions, TV mode overlay),M6-15-stats-live.md(subscription, errors,resume),M6-14-stats-queries.md,M6-06-stats-views.md(telemetry_dashboard_url; itsAdminStatsSourceserves A7, not the six views), the briefs of M4-13 and M4-17 (gateway session and fake server),M5-02-iphone-layout.mdandM5-03-android-layout.md(phone shell, Me tab, lifecycle),M6-02-console-app.md,M6-03-console-csp.md(console-e2e). - Code on the base branch:
packages/bothy_viz,packages/sync_engine,packages/admin_ui,apps/client,apps/console.
This ticket touches no API operation, no table, no action, no event and no configuration key.
Fixed requirements that apply¶
Owner's fixed build rules (apply verbatim):
- No MVP. New from scratch and directly in the final state; stages are only the order.
- Strictly modular: clear module and service boundaries, dependencies only through defined interfaces, no cross access.
- Documentation in code: every public module, class, function and interface with a docstring/doc comment (purpose, parameters, return value, errors, example where useful); comments explain the why, not the what. API documentation is generated from code (OpenAPI from the routes, dartdoc, Python API docs in MkDocs).
- Documentation in the repo: README per module/service, ADR for every architecture decision, CHANGELOG per release, keep the documentation site current. A change without matching documentation is not done.
- Quality gates (SonarQube, linters, type checking, security scanners, test coverage) block the merge.
Rules for every brief:
- Test first: write the failing test, run it and keep the red output for the report, then make it green, then clean up.
- No en dash and no em dash in any file you write; the
text-rulespre-commit hook rejects them, and the ARB test of M4-06 rejects them in interface texts. Use a hyphen or rephrase. - White label: examples and test data use
example.comand the people Alex, Sam and Robin; no company, host or personal names. Interface texts come frompackages/l10n. - Intervals only through named configuration values (
viz.*fromAppConfig); code comments name the key, never the number. - Specification first: a change to behaviour updates
docs/spec/in the same change. The core contract is not edited by this ticket.
Product requirements implemented here:
- The views of M6-12 in the desktop app (D12, TV mode D13), the mobile app (M10, no TV mode) and the console ("Live", TV mode), each with the table view.
- The desktop and mobile apps take the live stream from
sync_engine(stats.subscribe); the console pollsgetStatsLiveeveryviz.live_poll_interval;bothy_vizstays free of transport code (15 3.1 rule 6). - On mobile the stream runs only in the foreground: the subscription ends when the app is suspended and is restored on
resume(04 6.3) or a newstats.subscribeafteridentify. - "Stale" after
viz.stale_afterwithout a frame; TV mode ends on a key or a click and shows costs only withviz.tv_show_costs. - No WebView, no iframe in
apps/client(VZ-T16, SEC-VZ-1); "Open in Grafana" only withtelemetry.dashboard_urland outside the app. - A tapped run card opens the thread by deep link in the apps; the console shows "Open in the app" (ADR 0028).
Scope¶
Create:
packages/sync_engine/lib/src/stats/: the subscription manager (subscribe(spaceId),unsubscribe(), the subscription carried acrossresume, re-sent afteridentify, ended by the server'serrorwith the code passed on), and the delivery ofstats.liveframes to a stream; pure Dart, no Flutter (Design note 1).apps/client/lib/viz/: the app implementations ofLiveFrameSource(over thesync_enginemanager, with the stale rule of M6-11) and ofStatsQuerySource(the query operations of M6-11 over theapi_clientwith DPoP, M6-11 "Out of scope"); the desktop page "Live" with the view tabs and TV mode, the mobile M10 screen with tabs and without TV mode, foreground handling (Design notes 2 and 3).apps/console/lib/viz/: the consoleLiveFrameSourceby pollinggetStatsLiveeveryviz.live_poll_interval(stopped while the tab is hidden, Page Visibility), the consoleStatsQuerySourceover the cookie client of M6-02, the "Live" route with TV mode (Design note 4).tools/arch-checkor a pytest intools/ci/tests: the static check of VZ-T16 overapps/client(Design note 5).- Tests (Tests).
Change:
packages/admin_uinavigation entry "Live" (desktop administration and console) as 15 3.1 assigns it toadmin_ui.apps/clientdesktop sidebar group "Live and statistics" (D12) and the mobile Me tab entry "Live" (M10).tools/console-e2e: thepatrolweb suite of the console views.apps/client/platform-tests.json: thepatrolM10 suites for iOS and Android.packages/l10nARB files; READMEs ofsync_engine,apps/client,apps/console;docs/spec/15-client-architecture.md7.1 (the stats subscription building block); rootCHANGELOG.mdunder[Unreleased];docs/DECISIONS.mdsection 10 rowsD-M6-16-1toD-M6-16-5.
Out of scope¶
- The optional Grafana embedding in the console (M6-17).
- The Grafana panel plugin (M7).
- Changes to the core contract.
Boundaries¶
- Work in a dedicated git worktree on the branch
feat/M6-16-viz-in-apps; one writer per worktree. - Conventional Commits with the package or component as scope (for example
feat(sync-engine): ...,feat(client): ...,feat(console): ...) and a DCO sign-off on every commit (git commit -s); squash into meaningful commits before the merge (docs/quality/push-discipline.md). - Never push to
main, never tag and never create remote repositories; the only remote steps are the branch push and the pull request ofAGENTS.md. - Do not modify files outside the scope above, except the CHANGELOG, README and
docs/DECISIONS.mdentries named in this brief. - Follow the host rules supplied by the launcher (ports, machine rules); the stack publishes its ports only on
127.0.0.1. - Tests bind only
127.0.0.1with port 0 and use the fake server of M4-17 with the fixtures ofbothy_viz(the fake server servesstats.subscribeandgetStatsLivefrom the fixtures; extend it if it lacks them). Stop only processes and containers you started, by PID or container ID; simulators by UDID, emulators by serial. Temporary directories only viamktemp -d, removed at the end. - Never read or print secrets.
- The device suites need macOS with Xcode and an iOS simulator, and an Android emulator; the console suite Docker and the pinned browsers; where missing,
make apps-platformprints its SKIP line; the ticket must runmake console-e2eonce on the build host or stop withVerdict: BLOCKED <tool> missing; the report names where the device suites ran. - Never lower a threshold, disable a gate, delete a test, raise
repo.max_file_kbor extendquality/licenses/allowed.txt. - No permission logic in the client (15 1 principle 1).
- If the core contract or
docs/api/lacks something this ticket needs, stop withVerdict: BLOCKED <reason>instead of building a deviation (P-11). - This brief was written before milestone M1 closed (D-W-100, extended to M5 and M6 by D-W-120). Where the merged code of an earlier ticket names a package, type, function, file, provider or make target differently from this brief, follow the code and list each difference in the report; where it behaves differently from the specification, stop with
Verdict: BLOCKED <reason>.
Design notes¶
-
Subscription in
sync_engine(04 6.2, 6.3, 12 item 4; 25 5.1; Ruling for docs/DECISIONS.md). The manager keeps the desired subscription (none, instance, or a space ID). When the session is live it sendsstats.subscribeonce; onresumedit expects the server to have restored it (04 6.3) and does not resend; afteridentify(a new session) it resends; onerrorwith codeforbidden,not_found,feature_disabledorrate_limitedrelated tostats.subscribeit clears the desired subscription and emits the error;unsubscribesendsstats.unsubscribe. Frames arrive as ephemeralstats.liveevents on a stream; nothing is written tolocal_store(live frames are ephemeral and must not be persisted). -
App source and views (25 7.1; D12, M10). The app
LiveFrameSourcewraps the manager and applies the stale rule of M6-11 (no frame forviz.stale_after). The desktop page hosts the view tabs and a TV mode button (allowTvMode: true); the mobile M10 screen hosts the tabs without TV mode (allowTvMode: false), densitytouch; run card taps go to/t/{thread_id}. "Open in Grafana" passesonOpenGrafanaonly when M6-06'stelemetry_dashboard_urlis present. -
Foreground only on mobile (25 7.1; 15 7.2). Entering
Suspendedends the session, so the server ends the subscription; returning to the foreground resumes; if the resume window passed andidentifyruns, the manager resubscribes. The view shows "stale" in between and never shows a frame older thanviz.stale_afteras live. -
Console source (25 5.1; R-VZ-06; Ruling for docs/DECISIONS.md). A timer every
viz.live_poll_intervalcallsgetStatsLivewith the scope; hidden tabs (document.visibilityState) stop polling; a403 feature_disabledstops polling and shows the text; errors back off up toviz.stale_afterand the view shows "stale". TV mode in the console uses the same overlay as the desktop. -
Static check (VZ-T16; SEC-VZ-1; Ruling for docs/DECISIONS.md). A check in the gate fails when
apps/client/pubspec.yaml(or its resolved dependencies inpubspec.lock) contains a WebView package (a list:webview_flutter,flutter_inappwebview,webview_windows,desktop_webview_windowand any package whose name containswebview), when any Dart file ofapps/clientusesHtmlElementViewor creates an iframe element, or when a native folder ofapps/clientreferencesWKWebView,android.webkit.WebVieworWebView2. Run it inmake arch(the M4-01 architecture tool or a separate script called there); test it with fixtures. -
Rulings for
docs/DECISIONS.md(D-M6-16-<n>): the subscription manager and its resend rules (note 1); no persistence of frames (note 1); mobile foreground behaviour (note 3); console polling with visibility (note 4); the static WebView check and its list (note 5).
Tests (write them first)¶
packages/sync_engine/test/stats/: subscribe once when live; no resend afterresumed; resend afteridentify; errors clear the subscription and pass the code;unsubscribesends the op; frames never reach the local database (a fake store records no write).- Integration with the fake server (
apps/client/integration_test/viz/): desktop D12 shows frames from the fixtures and "stale" when the fake server stops sending; TV mode starts, rotates, hides costs by default, shows them withviz.tv_show_costs, ends on a key and a click; "Open in Grafana" only with the URL. patrolon iOS and Android (apps/client/integration_test/phone/viz_test.dart): M10 with every view and its table view; the app goes to the background and the fake server sees the subscription end; back in the foreground frames resume; a run card opens the thread.patrolweb inmake console-e2e: "Live" pollsgetStatsLiveagainst the Compose stack, shows the views and the table views, TV mode ends on a key; no CSP violation.- VZ-T16: the static check passes on the real
apps/clientand fails on fixtures with a WebView dependency, anHtmlElementViewwith an iframe and a nativeWKWebView.
Commands that must pass, from the repository root:
cd packages/sync_engine && dart test;cd apps/client && flutter analyze --fatal-infos && flutter test --coverage;cd apps/console && flutter test --coverage- on macOS with Xcode and with an attached Android emulator:
make apps-platform; otherwise the SKIP lines make console-e2eon the build hostmake arch,make lint,make fmt-check,make coveragemake check
Acceptance criteria¶
- VZ-T16 is green: no WebView and no iframe in
apps/client. - Integration tests with the fake server and fixtures pass; the
patrolweb test of the console passes;patrolon iOS and Android shows all views of M10 with the table view and detaches in the background. - TV mode ends with a key or a click and shows costs only with
viz.tv_show_costs; "stale" afterviz.stale_after; "Open in Grafana" only withtelemetry.dashboard_url. - READMEs, 15 7.1, ARB files, CHANGELOG and the rulings are updated.
-
make checkis green.
Definition of done¶
- Code and tests as specified; doc comments on every public Dart symbol; comments explain why.
- READMEs of
sync_engine,apps/clientandapps/consoleupdated in English. - Rulings of Design note 6 recorded in
docs/DECISIONS.mdsection 10. - Root
CHANGELOG.mdentry under 'Unreleased'. - An ADR only if you take an architecture decision beyond this brief (MADR 4 as in
docs/adr/0000-template-madr-4.md, headings and text in English, next free number,make adr-index). make checkgreen.- An independent code review over the diff, then the red team gate (
docs/quality/red-team-gate.md, verdictJAorNEIN); onNEINfix with a test per finding and have the delta checked again. Record both verdicts. - Land the change as
AGENTS.md("How work lands onmain") describes: a pull request from this branch, squash-merged only when review, red team andmake check,ci-okand the review threads are green; thengit worktree remove.
Report¶
Write to {{REPORT_PATH}}:
- First line exactly
Verdict: DONEorVerdict: BLOCKED <reason>. - Commits (SHA and subject), changed files with line counts, test results with counts (the red run before the change and the green run after), the summary of
make checkand ofmake console-e2e. - Review and red team verdicts and the findings fixed.
- Where the device suites ran.
- What is not proven.
- Follow-ups that could not be done in this scope, each with its reason, every difference to the names of this brief (D-W-100), and spec gaps found.
Then write {{DONE_PATH}} with exit=0 sha=<sha> or exit=1 <reason>.