v0.9.69 — Anonymous Telemetry Pipeline + Showcase Dashboard Streaming Fix#50
Conversation
- Add chart.js@^4.4.0 dependency (browser-only, dynamic import in page).
- Create showcase/angular/src/app/core/stats/github-stats.types.ts:
StatsViewId union, RepoSummary/StarEvent/IssueEvent/PullEvent/CommitEvent/
ForkEvent/ReleaseEvent shapes, TimeSeriesPoint, WeeklyDelta, DatasetState<T>.
- Create showcase/angular/src/app/core/stats/github-stats.service.ts:
- SSR-safe: every fetch + document ref behind isPlatformBrowser.
- Per-URL ETag cache with 304 short-circuit.
- MAX_PAGES=2 hard cap; PER_PAGE=100; 7 endpoints.
- 403+X-RateLimit-Remaining=0 -> graceful rate-limited state (no throw).
- 5-min polling, visibility-aware (clears interval on hidden, resumes on
visible with immediate refresh).
- Promise.allSettled across endpoints (no cascade on partial failure).
- 8 BehaviorSubject<DatasetState<...>> streams.
- 7 pure aggregator functions (cumulative stars, weekly delta, issues
open/closed, forks growth, PRs opened/merged, commits over time,
maintenance: releases-per-month w/ commits-per-week fallback).
- No edits to crawler files, hreflang verifier, prerender-routes.txt, or
locale-seo.ts -- crawler-exclusion invariant preserved by omission.
…link
- StatsPageComponent (standalone) at src/app/pages/stats/{ts,html,scss}:
- ngOnInit sets <title>'FSB · Stats' + meta robots=noindex,nofollow.
- afterNextRender block: dynamic import 'chart.js/auto' (browser only;
NEVER bundled into SSR), GitHubStatsService.start(), subscribe to all
8 dataset BehaviorSubjects, ViewChild canvas mount.
- 7 view toggles via setView() with no re-fetch (data shared across
views; chart instance destroyed + recreated per switch).
- ngOnDestroy: statsService.stop(), chart.destroy(), unsubscribe.
- Reads CSS tokens from documentElement so chart colors flip with theme.
- Styles use only existing FSB tokens (--bg-card / --primary /
--text-secondary / --border-color / etc.); zero new tokens.
- app.routes.ts: lazy { path: 'stats', loadComponent: ... } before **.
- app.routes.server.ts: { path: 'stats', renderMode: RenderMode.Client }
-- never prerendered; no /stats/index.html under any locale subpath.
- showcase-shell.component.html: one new <a routerLink='/stats'
i18n='@@shell.footer.project.stats'>Stats</a> inside the Project footer
column AFTER the License link. NOT in .nav-links, NOT in .nav-mobile,
NOT in Pages column.
- Add @@shell.footer.project.stats trans-unit to messages.xlf (source)
and to all 5 target XLIFFs:
es 'Estadísticas', de 'Statistiken', ja '統計',
zh-CN '统计', zh-TW '統統' [zh-TW '統計']
- [Rule 3 - Blocking issue] Exclude src/app/pages/stats/** from
lint:i18n in package.json (mirrors the existing dashboard exclusion).
Rationale: /stats is an Easter-egg page hidden from crawlers + nav;
English-only content is consistent with its 'for nerds' positioning
and the established dashboard precedent. The footer link itself IS
fully translated via @@shell.footer.project.stats.
- Crawler invariants preserved (byte-identical):
prerender-routes.txt, public/sitemap.xml, public/llms.txt,
public/llms-full.txt, scripts/build-crawler-files.mjs,
scripts/verify-hreflang.mjs, src/app/core/seo/locale-seo.ts.
- Build green: ng build emits 30 prerendered routes (6 × 5; /stats
intentionally absent), stats-page-component lazy chunk = 21.59 kB,
chart.js/auto in a separate lazy chunk = ~205 kB (NOT in initial).
- npm run verify:hreflang -> 301 pass, 0 fail; no /stats in output.
…with live GitHub graphs auto-refreshing every 5min - Adds /stats page with 7 chart views (stars cumulative + weekly, issues, forks, PRs, commits, releases, maintenance) - Footer link is the ONLY entry point; route uses RenderMode.Client; absent from prerender-routes.txt / sitemap / llms.txt / hreflang - chart.js lazy chunk; afterNextRender + isPlatformBrowser gating for SSR safety - Per-URL ETag cache, 304 short-circuit, 5-min visibility-aware polling (pauses on tab hidden, resumes on visibilitychange) - noindex,nofollow meta; rate-limit graceful fallback Files: 5 created + 11 modified (incl. 5 locale XLIFFs). Commits on Refinements: 9f82efe (service+deps), 27cc70a (page+route+footer), f0202ea (worktree merge).
… Dashboard Streaming Fix
- New extension/utils/install-identity.js attaches globalThis.fsbInstallIdentity with getOrCreateInstallUuid, isTelemetryOptedOut, setTelemetryOptOut and the fsbInstallUuid / fsbTelemetryOptOut storage-key constants (IDENT-01..05). - Storage namespace is exclusively chrome.storage.local; the module source contains zero references to chrome.storage.sync (D-11 / IDENT-04 grep gate). - Defensive v4 regex validation re-mints a fresh UUID with a single warn log if the stored value is corrupt or hand-edited. - On storage error: returns null (no throw, no session-only fallback) per IDENT-03. - background.js loads install-identity.js at the TOP of the importScripts chain (line 7, before analytics.js at line 35) so downstream modules can call globalThis.fsbInstallIdentity.* synchronously after boot. - chrome.runtime.onInstalled and chrome.runtime.onStartup both call globalThis.fsbInstallIdentity.getOrCreateInstallUuid() between initializeAnalytics() and loadDebugMode(); idempotent on every SW wake.
…toggle - New .settings-card with id='card-privacy-telemetry' mounted as the LAST child of .advanced-settings-grid inside <section id='advanced'> (CONS-01). - Toggle (id='fsbTelemetryOptOut', checked by default) shows 'Send anonymous usage data' label + factual subtitle 'Tokens used, MCP client name, active agent count. No URLs, prompts, or DOM.' and a 'Read full policy ->' link pointing to /privacy#telemetry-disclosure (anchor lands in Phase 275). - Inline <script> before </body> wires DOMContentLoaded read + change-event write to chrome.storage.local.fsbTelemetryOptOut. Toggle position is the INVERSE of opt-out: checked == telemetry ON, !checked == opted out. - Visible toggle position flips via the browser-native checkbox visual immediately on click; chrome.storage.local.set fires-and-forgets so the UI reflects state in well under 100ms (CONS-02). - aria-label dynamically updates between the two announced states per UI-SPEC. - Reuses existing .settings-card / .modern-toggle / .toggle-track / .toggle-thumb / .setting-hint classes verbatim; no new CSS, no new external JS file. - No 'Reset ID' button, no 'Wipe data' button, no first-run banner -- locked out per CONTEXT.md <deferred> and D-02.
- New tests/install-identity.test.js covers 7 sections / 23 assertions:
1. Mint-once on first call (IDENT-01): fresh store -> v4 UUID minted,
persisted under fsbInstallUuid, .set called exactly once; second
call returns the same UUID with NO further .set.
2. Reuse-on-restart (IDENT-02): pre-seeded valid UUID returned unchanged
with ZERO .set calls.
3. Null on storage unavailable (IDENT-03): chrome.storage.local.get throws
-> resolves to null, no uncaught exception.
4. Opt-out round-trip (CONS-01, CONS-02): set/get cycle for true/false
plus Boolean coercion of truthy non-boolean values; storage key is
exactly fsbTelemetryOptOut.
5. Defensive re-mint on corruption: pre-seeded 'not-a-uuid' triggers
re-mint, storage overwritten, exactly one console.warn line with
the expected prefix.
6. IDENT-04 grep gate: reads module source from disk, strips comments,
asserts ZERO chrome.storage.sync references.
7. Storage-key string locks: FSB_INSTALL_UUID_KEY === 'fsbInstallUuid',
FSB_TELEMETRY_OPT_OUT_KEY === 'fsbTelemetryOptOut' (catches snake_case
regressions to the REQUIREMENTS.md verbatim text).
- package.json scripts.test chain inserts 'node tests/install-identity.test.js'
immediately after 'node tests/cost-tracker.test.js' so the test groups
with related cost / analytics tests.
- Helpers (createStorageArea, setupChromeMock, setupBrokenStorage,
captureWarn, freshRequire) adapted from tests/agent-cap-storage.test.js
with per-area call-count tracking so 'set was called once' and
'set was NOT called' assertions are observable.
Move the Phase 269 kill-switch wiring out of an inline <script> block in control_panel.html and into a new external file at extension/ui/install- identity-ui.js. MV3's default CSP (`script-src 'self'`) forbids inline scripts on extension pages, which silently blocked the previous block at runtime. With the old wiring, DOMContentLoaded never ran and the change listener never registered, so user opt-out clicks were discarded with no storage write -- the worst-possible failure mode for a privacy kill switch. The extracted file preserves the original behaviour verbatim (toggle state mirrors `fsbTelemetryOptOut` storage key, inverse-checked semantics, aria- label flip on change). A follow-up commit refactors the file to call the install-identity module's exported API instead of hardcoding the storage string.
Change the Privacy & Telemetry card's "Read full policy" anchor from `/privacy#telemetry-disclosure` to the absolute `https://full-selfbrowsing.com/privacy#telemetry-disclosure`. The extension-relative path resolved to `chrome-extension://<id>/privacy`, which does not exist inside the extension bundle and opened a blank/404 tab -- failing the CONS-01 acceptance criterion ("Read full policy" link visible and functional). The `#telemetry-disclosure` anchor target itself lands in Phase 275 on the showcase side; until then the fragment is a benign no-op that scrolls to page top, which is acceptable per 269-CONTEXT.md line 84.
Memoize the in-flight Promise from getOrCreateInstallUuid() at module level via `_pendingMintPromise`, so concurrent callers (chrome.runtime .onInstalled + chrome.runtime.onStartup firing close together at first install) await the same operation. Without coalescing, both async handler bodies could observe an empty store, mint two different UUIDs, and race to .set -- the persisted value is whichever .set lands last while the other call site returns a UUID that is no longer in storage. The memo is cleared in `finally` so subsequent calls (later SW wakes, or recovery after a transient failure) can re-attempt the mint. Without the clear, one storage hiccup would latch null for the whole SW lifetime. Adds Test 8 to tests/install-identity.test.js -- fires two concurrent getOrCreateInstallUuid() calls, asserts both resolve to the SAME UUID and chrome.storage.local.set was called exactly ONCE. Verified by temporarily disabling the short-circuit: the assertions fail (got two UUIDs, set count = 2), confirming the test discriminates the fix. 23/23 -> 29/29 PASS.
…-once) Add `_corruptWarningEmitted` boolean to install-identity module and require it be false before emitting the "Stored install UUID failed validation" console.warn. Without this gate, a corrupt stored value + a failing chrome.storage.local.set (enterprise policy, quota exhausted, write contention) would loop: every read sees the same corrupt value, the warn fires, the .set rejects, the outer try/catch returns null, the corrupt value remains, the NEXT read repeats. Under MV3 SW eviction this spams the console dozens of times per hour. After the gate flips to true, the warn stays silent for the remainder of the SW lifetime. A fresh SW wake (cold-start) resets the module state naturally -- the user only gets one signal per SW lifetime. Adds Test 7b to tests/install-identity.test.js -- corrupt UUID + always- failing .set, three consecutive calls, asserts exactly ONE warn line emitted. Negative test verified: with the gate removed, warn count was 3, confirming the test discriminates the fix. 29/29 -> 35/35 PASS.
… hardcoded keys
Load extension/utils/install-identity.js inside control_panel.html so the
storage-key constant (FSB_TELEMETRY_OPT_OUT_KEY) and the API functions
(isTelemetryOptedOut, setTelemetryOptOut) are available on
globalThis.fsbInstallIdentity from the UI document context.
Rewrite install-identity-ui.js to call the module API as the primary path,
removing the duplicated `STORAGE_KEY = 'fsbTelemetryOptOut'` literal. A
future rename of the storage key now only needs to touch install-identity
.js -- the UI no longer carries a parallel copy that could drift.
A defensive fallback to direct chrome.storage.local.{get,set} with a
hardcoded key string remains in case the module fails to load (e.g.,
load-order regression). The fallback only fires when `globalThis
.fsbInstallIdentity` is null; in steady state the module is always
loaded first by the <script src="..."> chain in control_panel.html.
29/29 -> 35/35 PASS preserved (test suite unaffected by UI changes).
…lient defaults - mcp/data/mcp-pricing-data.json: top-level pricing_source_date='2026-05-14', pricing_policy doc, 30 model_pricing rows (Anthropic 7, OpenAI 7, Gemini 4, Grok 10, DeepSeek 2), 13 client_default_model rows (visual-session allowlist incl. 'OpenClaw 🦀' with U+1F980 crab grapheme). - Every row carries source_url + source_date + HIGH/MEDIUM/LOW confidence. - Caveat notes on opus-4.7 (tokenizer 35% drift), grok-4 (deprecation 2026-05-15), deepseek-v4-pro (75% promo expiring 2026-05-31), and gemini-3.1-pro (assumed-parity). - extension/utils/mcp-pricing-data.json is byte-exact copy (md5 confirmed 5e95bccaa38c8c37f8bff8af1701053b).
mcp/src/tools/pricing.ts (TS, MCP-server side):
- Imports mcp/data/mcp-pricing-data.json via import-attribute syntax
('with { type: "json" }'; works under module=ESNext, moduleResolution=bundler).
- Exports PRICING_SOURCE_DATE='2026-05-14', MCP_MODEL_PRICING,
MCP_CLIENT_DEFAULT_MODEL, estimateMcpCost(), type McpPricingResult.
- Module-load invariant: every client_default_model.model must resolve to a
real model_pricing row; throws clearly at startup if JSON ships broken.
- Resolver implements 4-path contract (lookup / fallback / unknown /
missing-tokens) per CONTEXT.md decisions; wrapped in try/catch so it
NEVER throws at runtime.
- Lookup paths use Object.prototype.hasOwnProperty.call (prototype-pollution
safe). Tokens validated via typeof number + Number.isFinite.
extension/utils/mcp-pricing.js (JS, service-worker mirror):
- Mirrors install-identity.js pattern: function/prototype on
globalThis.fsbMcpPricing + CommonJS module.exports for tests.
- Loads adjacent extension/utils/mcp-pricing-data.json: synchronously via
require() under Node; via fetch(chrome.runtime.getURL(...)) under MV3 SW.
- Resolver is SYNCHRONOUS; if called before SW fetch resolves (defensive),
returns canonical UNKNOWN envelope.
- _loadPricingData(data) test seam exposed for unknown-path tests.
- Same 4-path contract + never-throws guarantees as the TS module.
Verified: npm --prefix mcp run build clean; resolver returns expected
{cost, source, model_used, pricing_confidence, pricing_source_date} envelope
on every input (lookup=18, fallback=30, unknown=null, chaos=UNKNOWN).
… wiring
tests/mcp-pricing.test.js (156 assertions, 8 sections):
1. lookup path (PRICE-01) -- known model resolves with row confidence
2. fallback path (PRICE-02) -- known client + missing/unknown model
3. unknown path + chaos sweep -- canonical UNKNOWN envelope; never throws
(10 garbage inputs incl. prototype-pollution)
4. missing tokens -- cost=null, model_used preserved
5. pricing_source_date (PRICE-05)-- attached to all 8 result variants
6. data integrity 5-row spot -- claude-opus-4-7/sonnet-4-6/gpt-5/grok-4.3/
deepseek-v4-pro rates byte-match STACK
7. confidence stamp completeness -- every model + client row HIGH/MEDIUM/LOW
and every client.model resolves
8. visual-session.ts parity -- regex-extract MCP_VISUAL_CLIENT_LABELS,
set-equality with client_default_model
keys (anti-drift gate); crab grapheme
byte-exact on both sides
tests/mcp-pricing-data-parity.test.js (CI gate):
Byte-exact compare of mcp/data/mcp-pricing-data.json against
extension/utils/mcp-pricing-data.json. On divergence: prints first 5
mismatching lines + the cp command to recopy.
package.json:
Wired both new tests into 'npm test' AFTER tests/install-identity.test.js
(Phase 269 anchor) and BEFORE tests/transcript-store.test.js. No other
test commands reordered.
Verified:
- node tests/mcp-pricing.test.js: 156 passed, 0 failed
- node tests/mcp-pricing-data-parity.test.js: PASS byte-exact
- install-identity.test.js (Phase 269 anchor): exit 0
- transcript-store.test.js (next in chain): exit 0
- npm --prefix mcp run build: clean
- source_url count in JSON: 43 (>=30)
- confidence count in JSON: 43 (=43, 30 models + 13 clients)
mcp/src/tools/pricing.ts imports ../../data/mcp-pricing-data.json. The compiled output mcp/build/tools/pricing.js preserves that relative path, but the previous "files" array only shipped build/, ai/, README.md, server.json, so the JSON was excluded from the npm tarball. After this fix, npm --prefix mcp pack --dry-run shows data/mcp-pricing-data.json (11.1kB) in the tarball alongside build/. This is latent (no server-side consumer imports pricing.ts yet) but the moment one does, every consumer of fsb-mcp-server >=0.9.69 from npm would fail to load pricing.js with ERR_MODULE_NOT_FOUND.
…butes
mcp/src/tools/pricing.ts uses the ESM import-attribute syntax:
import pricingData from '../../data/mcp-pricing-data.json'
with { type: 'json' };
The `with { type: 'json' }` syntax (which replaced the deprecated
`assert { type: 'json' }` form) was added in Node 18.20 and 20.10.
On Node 18.0-18.19 the file fails to parse with a syntax error -- there
is no graceful runtime fallback.
The previous engines.node value (">=18.0.0") advertised support for
those broken Node minors. Bump the floor to >=18.20.0 so the package
metadata accurately reflects what the compiled output can run on.
… + i18n extract
- Append <h2 id="telemetry-disclosure">Anonymous Usage Telemetry</h2> section
to showcase/angular privacy page covering: what we collect (5 fields), what
we do NOT collect (6 categories), retention (7d raw / 365d rollups /
lifetime aggregates), how to opt out (Control Panel toggle), how to erase
data (GDPR Article 17 curl recipe for POST /api/telemetry/forget), Limited
Use affirmation (verbatim CWS-compliant phrasing), aggregated public metrics
(link to /stats).
- 25 new @@PRIVACY_TELEMETRY_* trans-units extracted into messages.xlf,
preserving v0.9.63 i18n discipline.
- Curl JSON braces escaped via {{ '{' }} / {{ '}' }} so Angular template
parser accepts the literal.
Insert 25 fully translated <target state="translated"> entries into each of
messages.{es,de,ja,zh-CN,zh-TW}.xlf for the PRIVACY_TELEMETRY_* trans-units
introduced by 275-01.
Code identifiers (FSB, UUID, MCP, install_uuid, fsbInstallUuid, Chrome,
Chrome Web Store, Limited Use, GDPR, HTTP, DevTools, AI, IP, URL, DOM,
grok-4-fast, claude-opus-4) stay verbatim — they live inside <x id="…"/>
placeholders extracted by ng extract-i18n and so are preserved automatically.
Prose between placeholders is culturally and technically accurate for each
target locale.
Verified by `npm --prefix showcase/angular run build` (30 prerendered routes,
i18nMissingTranslation: error invariant green) + `verify:hreflang` (301/301
pass; /stats absent).
… evidence - Append "Data Collection" section to store-assets/chrome-web-store/listing-copy.md mirroring the privacy-page telemetry disclosure (5 collected fields, 6 NOT-collected categories, retention, opt-out, Limited Use affirmation using verbatim CWS-compliant phrasing, full privacy-policy link to full-selfbrowsing.com/privacy#telemetry-disclosure). - New store-assets/chrome-web-store/privacy-practices-evidence.md is the in-repo source-of-truth for what the human publisher does on the CWS Developer Dashboard Privacy Practices tab: which checkboxes to tick (PII = YES, Web history = NO, Limited Use = YES), which to leave unticked, Privacy Policy URL, screenshots to capture at publish time, and the verbatim Limited Use compliance statement.
- New scripts/verify-store-listing.mjs reads listing-copy.md, privacy-practices-evidence.md, and extension/manifest.json and asserts the 5 CWS-readiness invariants (Data Collection heading, full-selfbrowsing.com/privacy reference, #telemetry-disclosure anchor, evidence file non-empty, manifest homepage_url present). Exit 0 PASS, exit 1 with descriptive failures on FAIL. Uses only Node built-ins. - New tests/verify-store-listing.test.js spawns the script and asserts exit 0 + stdout contains PASS + stderr empty of FAIL. - New tests/showcase-privacy-page.test.js reads the privacy-page template directly (no browser needed) and asserts 53 invariants: stable anchor id="telemetry-disclosure", "Anonymous Usage Telemetry" heading, the 5 collected fields + 6 NOT-collected categories, /api/telemetry/forget curl recipe, /stats link, GDPR Article 17 attribution, all 6 verbatim Limited Use clause phrases (paraphrasing forbidden by spec), all 24 required @@PRIVACY_TELEMETRY_* i18n markers. - Wire scripts/verify-store-listing.mjs + both new tests into root package.json test chain immediately after tests/showcase-build-smoke.test.js. - Auto-fix (Rule 2): add homepage_url = https://full-selfbrowsing.com to extension/manifest.json so the CI guard's 5th assertion holds.
…hase
Final BLOCKER B3 closeout for v0.9.69. Adds .planning/phases/275-…/{275-SUMMARY.md, 275-VERIFICATION.md} documenting the 4 atomic task commits (c9db4f0 → 412195f → 94144ee → b3fc2ee), 5/5 verification gates green, and the user-gated CWS Developer Dashboard click-through workflow (D-15) deferred to publish time.
Updates STATE.md (progress 75% → 87.5%, 7/8 phases complete, B3 RESOLVED), ROADMAP.md (Phase 275 checked + completion note), and REQUIREMENTS.md (CONS-03..07 all Complete with commit-pinned attribution).
…hesis #1) - Add .planning/phases/276-.../276-DIAGNOSTIC.md: reproduction steps, log capture commands (server stdout, fsb_diagnostics_ring, dashboard transport-event history), 7-hypothesis validation matrix with predicted symptom + verification command + fix surface per row, empty captured-logs / root-cause / fix-applied sections for HUMAN repro pass. - showcase/server/src/ws/handler.js setupWSHandler: log each connection as '[WS] <role> connected | hashKey=<prefix>' and emit a structured '[WS] room-state | roles=<comma-separated> hashKey=<prefix>' line on every connect. A hashKey-mismatch bug now produces two room-state lines with different hashKey prefixes; the happy path produces one line with roles=ext,dash. This makes DIAGNOSTIC.md row-1 verifiable via grep. - Pair-handshake contract documented in the setupWSHandler JSDoc block. Defensive patch; harmless if hypothesis #1 is not the actual root cause.
…ntent re-arm (hypotheses #2 + #4) - extension/ws/ws-client.js: - Add module-level `_pendingStreamStart` queue + 200ms/5s constants (FSB_CONTENT_READY_POLL_INTERVAL_MS, FSB_CONTENT_READY_TIMEOUT_MS). - New `_waitForContentScriptReady(tabId, timeoutMs)` polls chrome.tabs.sendMessage({ action: 'pingDomStream' }) at 200ms cadence until the content-script dom-stream module responds { ready: true }, or 5s deadline elapses. Replaces the arbitrary setTimeout(300) guess. - New `_onDomStreamReady(senderTabId)` re-arms a parked dash:dom-stream-start payload from the background.js domStreamReady handler. - `_handleDashboardStreamStart`: probe readiness before issuing domStreamStart. On timeout, park the payload in `_pendingStreamStart` for re-arm. On readiness, clear any prior parked intent so a late ready ping does not double-fire. - `_forwardToContentScript` reinjection branch: replace the `setTimeout(r, 300)` heuristic with `_waitForContentScriptReady`. On timeout, record dom-forward-failed with readyState=ping-timeout-after-inject instead of issuing a stale sendMessage. - extension/content/dom-stream.js: add `case 'pingDomStream'` in the chrome.runtime.onMessage listener. Responds synchronously with { ready: true } once the dom-stream module has installed its listener (i.e. the IIFE has executed past the addListener call). - extension/background.js `case 'domStreamReady':` after the existing fsbWebSocket.send('ext:dom-ready', ...) call, invoke `_onDomStreamReady` inside a try/catch (non-blocking). Re-arms any parked stream-start intent when the dom-stream module signals load-complete. - tests/dashboard-stream-readiness-ping.test.js: static invariants on the pingDomStream case + _waitForContentScriptReady function shape + the 300ms heuristic removal, PLUS three behavioural simulations against a stubbed chrome.tabs.sendMessage: * sim A: ready on 3rd poll -> resolves true at ~150ms * sim B: never ready -> resolves false at the 300ms test budget * sim C: immediate ready -> resolves true under 100ms 16/16 PASS. - tests/dashboard-stream-pending-intent.test.js: static invariants on the _pendingStreamStart + _onDomStreamReady wiring (parking, clearing, re-dispatch, idempotency) + a behavioural simulation that parks an intent, fires ready, asserts exactly one re-dispatch with the original payload, then fires ready again and asserts no double-fire. 14/14 PASS. Defensive patch chain; harmless even if hypotheses #2 and #4 are not the actual root cause -- the readiness ping always succeeds on the happy path and the pending-intent flag is null on the happy path.
…ailures, stale counts + Resync button
- showcase/angular/src/app/pages/dashboard/dashboard-page.component.ts:
- Add `lastFrameTime` (number; refreshed on every snapshot AND every
mutation envelope) and `mutationsAppliedTotal` (number; per accepted
DOM op inside handleDOMMutations).
- Add `lastFrameAgo()` getter returning seconds-since-last-frame (0 when
no frame observed yet).
- Reset `mutationsAppliedTotal` inside `resetPreviewGenerationState` so
fresh snapshots start counting from 0. `lastFrameTime` is left as-is
so the tooltip's "X seconds ago" reading does not jump back to 0
mid-cycle.
- Update `updatePreviewTooltip` to append 4 new diagnostic lines after
the existing snapshot/state/reason rows:
last-frame: <N>s ago | mutations: <count> | apply failures: <count> | stale: <count>
- Wire a new `previewResyncBtn` DOM ref + click listener invoking
`requestPreviewResync('user-resync-button')` -- reuses the existing
recovery surface the stale-mutation watchdog already triggers.
- showcase/angular/src/app/pages/dashboard/dashboard-page.component.html:
- Add `<button id="dash-preview-resync-btn">` as the first control in
`.dash-preview-controls` (before the rc/pip/maximize/fullscreen/toggle
buttons). Title + aria-label only -- no i18n attributes added since
the surrounding buttons (PiP, Maximize, Fullscreen) are also unmarked.
- showcase/angular/src/app/pages/dashboard/dashboard-page.component.scss:
- `.dash-preview-tooltip`: relax single-line ellipsis to multi-line wrap
(`white-space: normal; word-break: break-word; max-width: 360px;
line-height: 1.35`) so the four extra diagnostic counters remain
legible at narrower viewport widths.
- `.dash-preview-resync-btn:hover`: tint the icon with the accent
colour to distinguish it from the play/pause toggle glyph.
Pure UI add; no behavioural changes to the streaming pipeline beyond
exposing already-tracked state in the tooltip and routing user-initiated
resyncs through the existing `requestPreviewResync` pathway.
… (bonuses)
- extension/background.js fsb-domstream-watchdog alarm handler: if
`_streamingActive` is true AND fsbWebSocket is connected, send
`ext:request-snapshot` (reason=sw-watchdog-tick, ts=Date.now()) on every
watchdog tick. Best-effort; wrapped in try/catch. No-op when the WS is
offline or _streamingActive is false/undefined. Covers the case where
the SW just woke from idle eviction and no mutation has flushed yet --
asking the dashboard to re-request a snapshot routes through the
existing requestPreviewResync path.
- showcase/server/src/ws/handler.js:
- Add `BACKPRESSURE_BUFFER_LIMIT_BYTES = 16 * 1024 * 1024` (16 MiB).
- Add module-level `backpressureDroppedCount` counter + the
`getBackpressureDroppedCount` accessor + `_resetBackpressureDroppedCount`
test helper. All exported.
- Update `sendToClients`: before each `client.send(data)`, check
`client.bufferedAmount > BACKPRESSURE_BUFFER_LIMIT_BYTES`. If so,
increment the counter, increment droppedCount, push a `backpressure-drop`
event into the room diagnostics ring (carrying type, bufferedAmount,
limitBytes), and skip the send. The pre-existing `readyState != OPEN`
drop path is NOT routed through the backpressure counter -- only
genuine buffer-overflow drops.
- Also export `sendToClients` from the module so the test can invoke it
directly with stub clients.
- tests/server-ws-backpressure.test.js: 22-assertion test covering:
* exports of the limit, counter accessor, reset helper
* single-wedged-client scenario (drop + counter increment; send NOT called)
* happy-path delivery (no drop)
* CLOSING-state drop does NOT increment the backpressure counter
* counter accumulates across 5 successive sendToClients calls
* backpressure-drop event recorded in the room diagnostics ring
22/22 PASS.
Defensive bonuses; both are harmless in normal operation. The watchdog
auto-resnapshot is a one-way safety net (no extra side effect when
streaming is healthy); the backpressure drop only fires when a client
is already wedged at 16 MiB of buffered output.
- package.json root test chain: insert 3 new tests AFTER
tests/verify-store-listing.test.js so the Phase 275 wiring point is
preserved AND the new Phase 276 invariants run in the same npm test
pass:
+ tests/dashboard-stream-readiness-ping.test.js
+ tests/dashboard-stream-pending-intent.test.js
+ tests/server-ws-backpressure.test.js
No reordering of pre-existing entries.
- .planning/phases/276-.../276-SUMMARY.md: complete phase summary,
frontmatter (requirements_satisfied, decisions, metrics), one-liner,
4 sections describing what landed in each of the 4 prior commits, the
3-test summary table with 52/52 PASS, deviation log (none auto-fixed),
authentication-gate log (none), known-stubs note (none), threat-flag
note (none), constraint-compliance checkboxes, what-remains-for-human
block, and self-check showing 6 created + 8 modified file paths +
4 task-commit hashes confirmed in git log.
- .planning/phases/276-.../276-VERIFICATION.md: 4-row automated-gate
table (3 new tests + showcase-build-smoke 134/134 PASS), regression
sweep on 10 representative existing tests (all green), self-check
reference, constraint-compliance checkboxes, status `human_needed`,
and the `<human_verification>` block:
* 8-step manual repro
* hypothesis-walk instructions: per-row table noting which
defensive patch is already landed and what to do if each row fires
* 5-attempt cap note (this counts as attempt 1 of 5)
* outcome-recording playbook (PASS path -> status: passed;
FAIL path -> follow-up phase or v0.9.70 re-scope).
- .planning/STATE.md frontmatter: completed_phases 7 -> 8;
completed_plans 8 -> 9; percent 87.5 -> 100; last_updated bumped;
last_activity now reads Phase 276 completion + 4 commit hashes + test
counts + status human_needed. Progress bar updated 87.5% -> 100%.
Performance Metrics table appended Phase 275 P01 + Phase 276 P01 rows.
Session Continuity section appended Phase 276 entry with commit
hashes + STREAM-01..06 satisfied + STREAM-07 attempt 1 of 5 note.
Resume file: None.
- .planning/ROADMAP.md: Phase 276 checkbox flipped [ ] -> [x] with a
full one-paragraph closeout summarising defensive patches landed,
status `human_needed`, and STREAM-07 attempt budget. Phase 274 + 275
+ 276 status rows updated in the phase-progress table.
- .planning/REQUIREMENTS.md: STREAM-01, STREAM-02, STREAM-04, STREAM-05,
STREAM-06, STREAM-07 marked Complete with per-line commit pointers.
STREAM-03 marked Partial (human_needed) since acceptance smoke
requires browser repro. Traceability table updated.
Phase 276 -- FINAL phase of v0.9.69 milestone -- closed in
human_needed status with all defensive patches landed, all automated
gates green, and the manual repro pass reserved for the user.
…REAM-03 human-gated; DOC-DEBT-01 fixed
…olved, 67/68 reqs)
….9.69 created locally (push user-gated)
… test was breaking on fresh installs
…for v0.9.69 telemetry tests quick task 260514-pu4: the expanded npm test chain (~108 files) now includes 13 server-side telemetry tests + 4 Angular/server integration tests that require showcase/server (express-rate-limit, better-sqlite3, ws) and showcase/angular (chart.js + Angular CLI) deps. CI's extension job previously installed only root + mcp deps; PRs to main would fail at the first server-* test. Verified locally: npm test exits 0 with all 30 v0.9.69 tests passing in the chain. No fly.toml / Dockerfile / schema.js changes needed — telemetry tables auto-create via CREATE TABLE IF NOT EXISTS at server boot; npm ci in Dockerfile picks up both new deps.
…nt:i18n caught at PR #50) CI's lint:i18n flagged privacy-page.component.html:134 — the <pre><code>...</code></pre> curl block was missing an i18n attribute. Added @@PRIVACY_TELEMETRY_ERASE_CURL with a meaning note guiding translators to copy the command literally (do not translate). Restored {{ '{' }} / {{ '}' }} Angular interpolation escape — using HTML entities {/ } caused the EN-root prerender to fail because the decoded { } characters got parsed as Angular ICU expressions. Updated all 5 non-en XLFs (es/de/ja/zh-CN/zh-TW) with target translations matching the new source structure (INTERPOLATION placeholders + literal curl command). Verified: lint:i18n clean, ng build prerenders 30/30 routes, verify:hreflang 301/0 pass, npm test exit 0.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 92c890ca75
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| var existing = await storage.get([FSB_USAGE_DATA_KEY]); | ||
| var arr = (existing && Array.isArray(existing[FSB_USAGE_DATA_KEY])) ? existing[FSB_USAGE_DATA_KEY] : []; | ||
| arr.push(row); | ||
| var write = {}; | ||
| write[FSB_USAGE_DATA_KEY] = arr; | ||
| await storage.set(write); |
There was a problem hiding this comment.
Serialize fsbUsageData appends to avoid dropping rows
recordDispatch appends via a get→push→set read/modify/write cycle, but the dispatcher invokes this method in fire-and-forget mode for each tool call, so concurrent dispatches can interleave: two calls read the same array snapshot, each pushes its own row, and the later set overwrites the earlier one. In busy sessions this silently loses telemetry events and undercounts tokens/cost in both extension analytics and server-side aggregates.
Useful? React with 👍 / 👎.
| var curr = (typeof ev.attempts === 'number' && ev.attempts >= 0) ? ev.attempts : 0; | ||
| ev.attempts = curr + 1; |
There was a problem hiding this comment.
Strip retry metadata from telemetry payloads
The retry path mutates queued events by adding an attempts property, then sends the full event objects as-is. The server validator accepts only the 9 allowlisted fields, so any retried event now contains an extra field and is rejected with unknown_field, turning transient POST failures into persistent retry failures and dropping those events after the attempts cap.
Useful? React with 👍 / 👎.
| await _advanceWatermark(storage, nowMs); | ||
| await _writeQueue(storage, queue); |
There was a problem hiding this comment.
Avoid watermark jumps that skip in-flight usage rows
Flush aggregates fsbUsageData by row.ts > watermark, but then advances WATERMARK_KEY to Date.now() after the read. MCP dispatch recording runs concurrently and writes rows with ts=Date.now(), so rows appended between the aggregation read and this watermark write can end up with ts <= new watermark and be permanently skipped on the next cycle, causing silent telemetry loss under active traffic.
Useful? React with 👍 / 👎.
| if (released) { | ||
| try { | ||
| const cur = await chrome.storage.local.get(['fsbActiveAgentsCount']); | ||
| const n = (cur && typeof cur.fsbActiveAgentsCount === 'number' && cur.fsbActiveAgentsCount > 0) | ||
| ? Math.floor(cur.fsbActiveAgentsCount) | ||
| : 0; | ||
| await chrome.storage.local.set({ fsbActiveAgentsCount: Math.max(0, n - 1) }); |
There was a problem hiding this comment.
Keep active-agent counter in sync with all release paths
The counter is decremented only in handleAgentReleaseRoute, but agents are also removed by other flows (for example pool-drain releaseTab cleanup and staged connection-loss expiry in the registry). Those paths bypass this decrement, so fsbActiveAgentsCount can drift upward and keep reporting inflated active_agent_count telemetry long after agents are actually gone.
Useful? React with 👍 / 👎.
| const day_utc = utcDayString(nowMs); | ||
| const entry = budgets.get(installUuid); | ||
| if (!entry || entry.day_utc !== day_utc) { | ||
| budgets.set(installUuid, { count: 1, day_utc }); |
There was a problem hiding this comment.
Prune expired per-UUID budget entries
The daily-budget map only updates the touched UUID and never removes old UUID keys, so a long-running server accumulates one permanent entry per unique install UUID ever seen. Under sustained adoption this creates unbounded in-memory growth in middleware state and can eventually pressure process memory even though old UUIDs are no longer active.
Useful? React with 👍 / 👎.
| Number.isInteger(active_agent_count) && active_agent_count >= 0 | ||
| ? active_agent_count | ||
| : 0; | ||
| seen.set(install_uuid, { ts: ts_ms, agent_count: agentCount }); |
There was a problem hiding this comment.
Evict stale active-tracker entries on ingest
Stale entries are evicted only by read APIs, but ingest writes keep inserting/updating UUIDs regardless of map size. If /api/public-stats/* is not queried frequently, old installs are never purged and the in-memory tracker grows without bound as telemetry traffic continues, which can cause avoidable memory bloat on the server.
Useful? React with 👍 / 👎.
… avoid concurrent-dispatch row loss (Codex PR #50 P1) - Add module-level _recordLock + _withRecordLock helper mirroring the proven _flushLock pattern from telemetry-collector.js. Two concurrent recordDispatch() calls now chain onto a serialised promise; the get->push->set rmw against chrome.storage.local.fsbUsageData cannot interleave so neither row is lost. - Move the get->push->set + ANALYTICS_UPDATE broadcast INSIDE the locked region. Pure synchronous setup (input validation, token estimate, pricing lookup, row assembly) stays outside the lock -- it has no shared state. - Broadcast continues to fire AFTER each successful set, in the same per-row order as today, so the Control Panel hero re-render order matches storage-write order. - The outer never-throw try/catch is preserved. - Add tests/extension-record-dispatch-serializes.test.js: 20 concurrent recordDispatch() calls -> 20 rows; broadcast count == row count; broadcast ordering preserved. Test is authoritative -- verified to FAIL on the legacy (pre-fix) code path with 1/20 rows recorded. Addresses Codex P1A on PR #50. Restores the COST-05 "no double count / no dropped row" promise in the live anonymous telemetry pipeline.
…er allowlist doesn't reject retries (Codex PR #50 P1) - Build a `wireEvents` projection in _runFlush() that copies EXACTLY the 9 ALLOWED_EVENT_KEYS per event before JSON.stringify. The explicit per-field copy (no spread, no Object.assign of row) prevents the in-memory `attempts` retry counter (set by _bumpAttempts on POST failure) from leaking into the request body, which the server's 9-field allowlist (showcase/server/src/routes/telemetry.js:41-44) would 400 with unknown_field -- causing retried events to burn the 5-attempt cap and be permanently dropped. - Object-literal key order mirrors the server's ALLOWED_EVENT_FIELDS Set for code-review clarity. Loop index `w` avoids shadowing. - Projection is READ-ONLY over `snapshot`. In-memory queue retains `attempts` so _bumpAttempts + _applyAttemptsCap continue to work on retry. snapshotIds is unchanged (still drives post-POST residue removal). - Add tests/telemetry-collector-strips-internal-fields.test.js: * Scenario A: queue seeded with attempts:2 -> POST body events have EXACTLY the 9-key allowlist; no `attempts`. * Scenario B: failed POST -> re-enqueued event still carries `attempts`, bumped 2 -> 3 (in-memory queue intact). Test is authoritative -- verified to FAIL on the legacy (pre-fix) code path (5 of 15 assertions fail on the wire-payload shape). - Wire both new regression tests (this one + the recorder concurrency test from the previous commit) into root package.json scripts.test chain after the existing telemetry-collector.test.js anchor. Addresses Codex P1B on PR #50. Restores the BEAT-04 "INSERT OR IGNORE dedup on retry" promise in the live anonymous telemetry pipeline.
…eview (recordDispatch race + attempts wire leak)
…ch race + attempts wire leak) (#53) * docs(260514-rm4): pre-dispatch plan for Codex P1 telemetry fixes * fix(extension-mcp-metrics-recorder): serialize fsbUsageData append to avoid concurrent-dispatch row loss (Codex PR #50 P1) - Add module-level _recordLock + _withRecordLock helper mirroring the proven _flushLock pattern from telemetry-collector.js. Two concurrent recordDispatch() calls now chain onto a serialised promise; the get->push->set rmw against chrome.storage.local.fsbUsageData cannot interleave so neither row is lost. - Move the get->push->set + ANALYTICS_UPDATE broadcast INSIDE the locked region. Pure synchronous setup (input validation, token estimate, pricing lookup, row assembly) stays outside the lock -- it has no shared state. - Broadcast continues to fire AFTER each successful set, in the same per-row order as today, so the Control Panel hero re-render order matches storage-write order. - The outer never-throw try/catch is preserved. - Add tests/extension-record-dispatch-serializes.test.js: 20 concurrent recordDispatch() calls -> 20 rows; broadcast count == row count; broadcast ordering preserved. Test is authoritative -- verified to FAIL on the legacy (pre-fix) code path with 1/20 rows recorded. Addresses Codex P1A on PR #50. Restores the COST-05 "no double count / no dropped row" promise in the live anonymous telemetry pipeline. * fix(telemetry-collector): strip attempts field from POST body so server allowlist doesn't reject retries (Codex PR #50 P1) - Build a `wireEvents` projection in _runFlush() that copies EXACTLY the 9 ALLOWED_EVENT_KEYS per event before JSON.stringify. The explicit per-field copy (no spread, no Object.assign of row) prevents the in-memory `attempts` retry counter (set by _bumpAttempts on POST failure) from leaking into the request body, which the server's 9-field allowlist (showcase/server/src/routes/telemetry.js:41-44) would 400 with unknown_field -- causing retried events to burn the 5-attempt cap and be permanently dropped. - Object-literal key order mirrors the server's ALLOWED_EVENT_FIELDS Set for code-review clarity. Loop index `w` avoids shadowing. - Projection is READ-ONLY over `snapshot`. In-memory queue retains `attempts` so _bumpAttempts + _applyAttemptsCap continue to work on retry. snapshotIds is unchanged (still drives post-POST residue removal). - Add tests/telemetry-collector-strips-internal-fields.test.js: * Scenario A: queue seeded with attempts:2 -> POST body events have EXACTLY the 9-key allowlist; no `attempts`. * Scenario B: failed POST -> re-enqueued event still carries `attempts`, bumped 2 -> 3 (in-memory queue intact). Test is authoritative -- verified to FAIL on the legacy (pre-fix) code path (5 of 15 assertions fail on the wire-payload shape). - Wire both new regression tests (this one + the recorder concurrency test from the previous commit) into root package.json scripts.test chain after the existing telemetry-collector.test.js anchor. Addresses Codex P1B on PR #50. Restores the BEAT-04 "INSERT OR IGNORE dedup on retry" promise in the live anonymous telemetry pipeline. * docs(quick-260514-rm4): Fix two Codex P1 telemetry bugs from PR #50 review (recordDispatch race + attempts wire leak) --------- Co-authored-by: Lakshman <lakshman@lakshman-GG9LQ90J61.local>
Summary
Milestone v0.9.69 — Anonymous Telemetry Pipeline + Showcase Dashboard Streaming Fix (8 phases, 91 commits ahead of
main).Stands up a privacy-preserving anonymous telemetry pipeline that flows MCP + extension usage metrics from end-user installs into the FSB showcase server, exposes aggregates on the public
/statsEaster-egg page, hardens the Chrome Web Store privacy disclosures, and lands defensive patches for the broken DOM streaming pipeline (top-3 hypotheses).Phases shipped
BLOCKERs resolved
Deploy + DB readiness
Test plan
Carry-forward backlog (NOT in this PR)
Tag: `v0.9.69` pushed (pointing at HEAD of this PR).