feat(unfold): export insight events to Faro or OTLP #249

Open
agent-builder wants to merge 2 commits from agent/vik-1897 into development
Member

Problem

Once Unfold's product events exist, the owner can only read them with SQL: the homelab already runs Alloy's faro.receiver and Grafana, but Unfold exports nothing there, so the Needs-you tables of RFC-0001 are unavailable. The server had no insight export settings, no event store and no collector sink.

Solution

  • Added the UNFOLD_INSIGHT_EXPORT (off/faro/otlp), _URL and _LEVEL (aggregate/events) settings and documented them in apps/unfold/docs/operations/live.md.
  • Added apps/unfold/src/insight.ts: the RFC-0001 event catalogue, a per-tenant pseudonymous actor hash, the product_event/product_event_daily store in apps/unfold/src/store.ts, and server-side Faro/OTLP forwarding.
  • Added POST /api/insight/events in apps/unfold/src/http.ts; it runs only on the server, so the browser never contacts the collector and the CSP is unchanged, and an unreachable collector is logged once an hour without blocking the route.
  • Added apps/unfold/ops/grafana/unfold-insight.json, the RFC-0001 stat panels and tables that suppress groups under five people.
  • Added tests in apps/unfold/test/insight.test.ts (service, fake collector), apps/unfold/test/api-insight.test.ts (route) and apps/unfold/test/config.test.ts.

Checks left to CI

This sandbox has Node 24 and Go, but no npm registry egress and no node_modules, so ws (a production dependency) is absent and tests that import api-support.ts cannot run locally. The Ploeg verify script passed (gofmt -l apps/ploeg clean). Locally run: node scripts/check.mjs (passes) and node --test test/insight.test.ts test/config.test.ts (13 tests pass, including the fake-collector Faro/OTLP integration tests and the unreachable-collector rate limit). test/api-insight.test.ts exercises the live route through api-support.ts and is left to CI, along with mise exec -- npm run typecheck, the full npm test, and mise run docs-check.

Risk notes

  • The route is authenticated and same-origin (mutation guard); viewers get 403. The browser never contacts the collector, so the CSP in src/http.ts is unchanged.
  • The collector send is fire-and-forget with a 10 s timeout; a failure is logged at most once an hour and never blocks or slows the event route.
  • The actor id is a 16-character base32 HMAC-SHA256 with a per-install key held in the encrypted store; aggregate export never carries it.
  • The Grafana queries are proposed with RFC-0001 and may need adjusting to the homelab's VictoriaLogs field mapping. Production desired state (env vars, dashboard provisioning) is out of scope and stays in webgrip/homelab-cluster.

VIK-1897

This pull request was created by an AI agent (OpenHands) on behalf of the owner.

Follow-up after review

Review found the Median time to resolve panel (unfold-insight.json, panel id: 1) was missing the filter actors:>=5 stage its description and the dashboard README promise, so Work Items touched by fewer than five distinct people leaked into the median — the opposite of the RFC-0001 suppression rule, and the panel most exposed on a single-user-first instance. Fixed in 046e012 by inserting the filter before the final stats median(...), matching panels 2–4. Re-ran: node scripts/check.mjs passes (43 JSON files), node --test test/insight.test.ts test/config.test.ts 13/13 pass, and the Ploeg verify script passes. test/api-insight.test.ts remains left to CI for the reason above.

## Problem Once Unfold's product events exist, the owner can only read them with SQL: the homelab already runs Alloy's faro.receiver and Grafana, but Unfold exports nothing there, so the Needs-you tables of RFC-0001 are unavailable. The server had no insight export settings, no event store and no collector sink. ## Solution - Added the `UNFOLD_INSIGHT_EXPORT` (`off`/`faro`/`otlp`), `_URL` and `_LEVEL` (`aggregate`/`events`) settings and documented them in `apps/unfold/docs/operations/live.md`. - Added `apps/unfold/src/insight.ts`: the RFC-0001 event catalogue, a per-tenant pseudonymous actor hash, the `product_event`/`product_event_daily` store in `apps/unfold/src/store.ts`, and server-side Faro/OTLP forwarding. - Added `POST /api/insight/events` in `apps/unfold/src/http.ts`; it runs only on the server, so the browser never contacts the collector and the CSP is unchanged, and an unreachable collector is logged once an hour without blocking the route. - Added `apps/unfold/ops/grafana/unfold-insight.json`, the RFC-0001 stat panels and tables that suppress groups under five people. - Added tests in `apps/unfold/test/insight.test.ts` (service, fake collector), `apps/unfold/test/api-insight.test.ts` (route) and `apps/unfold/test/config.test.ts`. ## Checks left to CI This sandbox has Node 24 and Go, but no npm registry egress and no `node_modules`, so `ws` (a production dependency) is absent and tests that import `api-support.ts` cannot run locally. The Ploeg verify script passed (`gofmt -l apps/ploeg` clean). Locally run: `node scripts/check.mjs` (passes) and `node --test test/insight.test.ts test/config.test.ts` (13 tests pass, including the fake-collector Faro/OTLP integration tests and the unreachable-collector rate limit). `test/api-insight.test.ts` exercises the live route through `api-support.ts` and is left to CI, along with `mise exec -- npm run typecheck`, the full `npm test`, and `mise run docs-check`. ## Risk notes - The route is authenticated and same-origin (mutation guard); viewers get 403. The browser never contacts the collector, so the CSP in `src/http.ts` is unchanged. - The collector send is fire-and-forget with a 10 s timeout; a failure is logged at most once an hour and never blocks or slows the event route. - The actor id is a 16-character base32 HMAC-SHA256 with a per-install key held in the encrypted store; `aggregate` export never carries it. - The Grafana queries are proposed with RFC-0001 and may need adjusting to the homelab's VictoriaLogs field mapping. Production desired state (env vars, dashboard provisioning) is out of scope and stays in `webgrip/homelab-cluster`. VIK-1897 _This pull request was created by an AI agent (OpenHands) on behalf of the owner._ ## Follow-up after review Review found the **Median time to resolve** panel (`unfold-insight.json`, panel `id: 1`) was missing the `filter actors:>=5` stage its description and the dashboard README promise, so Work Items touched by fewer than five distinct people leaked into the median — the opposite of the RFC-0001 suppression rule, and the panel most exposed on a single-user-first instance. Fixed in `046e012` by inserting the filter before the final `stats median(...)`, matching panels 2–4. Re-ran: `node scripts/check.mjs` passes (43 JSON files), `node --test test/insight.test.ts test/config.test.ts` 13/13 pass, and the Ploeg verify script passes. `test/api-insight.test.ts` remains left to CI for the reason above.
feat(unfold): export insight events to Faro or OTLP
Some checks failed
[Workflow] On Pull Request / ploeg-pin (pull_request) Successful in 33s
[Workflow] On Pull Request / release-policy (pull_request) Failing after 18s
[Workflow] On Pull Request / checks (pull_request) Failing after 2m25s
[Workflow] On Pull Request / warnings (pull_request) Successful in 0s
f62fbfdd67
Add the server side of RFC-0001's product-event export. A new
POST /api/insight/events route validates a batch against the event
catalogue, stores it in product_event with a per-tenant pseudonymous
actor hash, folds a daily rollup and prunes past 25 months. When an
operator names a collector, the server forwards Faro events or OTLP
logs; the browser never contacts the collector and the CSP is unchanged.
An unreachable collector is logged once an hour and never blocks the
route. The Grafana dashboard for the owner's instance ships under
ops/grafana and suppresses groups under five people.

VIK-1897
Agent-Trace-Id: ploeg-469aece9647c
Author
Member

builder — round 1

acp run opened a PR for unfold: export insight events to Faro for the owner's Grafana [Ploeg verification passed]

Ploeg verification

Ploeg ran the configured checks on a fresh checkout of commit f62fbfdd678d, the pushed head of the pull request, after the agent finished.

Check Result
if [ -f apps/ploeg/go.mod ]; then test -z "$(gofmt -l apps/ploeg)"; fi passed

Posted by Ploeg for the writing Run that pushed this branch.

### builder — round 1 _acp run opened a PR for unfold: export insight events to Faro for the owner's Grafana [Ploeg verification passed]_ ### Ploeg verification Ploeg ran the configured checks on a fresh checkout of commit `f62fbfdd678d`, the pushed head of the pull request, after the agent finished. | Check | Result | | --- | --- | | `if [ -f apps/ploeg/go.mod ]; then test -z "$(gofmt -l apps/ploeg)"; fi` | passed | <sub>Posted by Ploeg for the writing Run that pushed this branch.</sub>
Author
Member

Ploeg usage report

Settled.

Run Round Wrote Outcome Verdict Models billed Prompt / completion tokens Cost Duration Account
builder 1 yes pr_opened — fireworks_ai/accounts/fireworks/models/deepseek-v4p1-flash 14.612.145 / 141.817 US$ 0,32 55m56s reconciled
reviewer 2 no failed — — 0 / 0 US$ 0,00 43s reconciled
reviewer 2 no no_change_needed request changes fireworks_ai/accounts/fireworks/models/glm-5p3-flash 4.793.535 / 28.646 US$ 0,18 19m28s reconciled
builder 3 yes stuck — fireworks_ai/accounts/fireworks/models/deepseek-v4p1-flash 764.436 / 9.142 US$ 0,03 3m34s reconciled

Shift totals

  • Authorized: US$ 8,00
  • Spent: US$ 0,53
  • Reserved: US$ 0,00
  • Remaining: US$ 7,47
  • Rounds used: 3
  • Close reason: run stuck: builder round 3
  • Trace alias: ploeg-8e547212c56b

Evidence

Verification: not recorded — Ploeg observed no check result for the writing Run.

Posted by Ploeg and updated in place.

<!-- ploeg:usage-report --> ### Ploeg usage report _Settled._ | Run | Round | Wrote | Outcome | Verdict | Models billed | Prompt / completion tokens | Cost | Duration | Account | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | builder | 1 | yes | pr_opened | — | fireworks_ai/accounts/fireworks/models/deepseek-v4p1-flash | 14.612.145 / 141.817 | US$ 0,32 | 55m56s | reconciled | | reviewer | 2 | no | failed | — | — | 0 / 0 | US$ 0,00 | 43s | reconciled | | reviewer | 2 | no | no_change_needed | request changes | fireworks_ai/accounts/fireworks/models/glm-5p3-flash | 4.793.535 / 28.646 | US$ 0,18 | 19m28s | reconciled | | builder | 3 | yes | stuck | — | fireworks_ai/accounts/fireworks/models/deepseek-v4p1-flash | 764.436 / 9.142 | US$ 0,03 | 3m34s | reconciled | **Shift totals** - Authorized: US$ 8,00 - Spent: US$ 0,53 - Reserved: US$ 0,00 - Remaining: US$ 7,47 - Rounds used: 3 - Close reason: run stuck: builder round 3 - Trace alias: `ploeg-8e547212c56b` **Evidence** Verification: not recorded — Ploeg observed no check result for the writing Run. <sub>Posted by Ploeg and updated in place.</sub>
Author
Member

reviewer — round 2

Summary: Reviewed VIK-1897; verdict request_changes; outcome file written Finish with message: Review of VIK-1897 complete. Verdict: request_changes. ## What I reviewed Read the full diff (development...agent/vik-1897, 13 files, +789/−2) against the Work Item, RFC-0001, ADR-0023, and the base-branch AGENTS.md files, plus the PR's own description context. ## Verdict rationale Met: AC1 (…

Scope: the change does what the Work Item asks

The diff (git diff development...agent/vik-1897, 13 files, +789/−2) adds the server-side insight export and nothing else. No changes under apps/ploeg/ (submodule untouched, gofmt check passed), no helm/chart or homelab desired state, no changes to public/, extensions/ or any agent-configuration file (AGENTS.md, CLAUDE.md, .claude/, .agents/, .openhands/, .mcp.json, .cursorrules are byte-identical to development).

Per acceptance criterion:

  1. Env vars documented — met. apps/unfold/docs/operations/live.md (the application's configuration reference for operators, where every other UNFOLD_*/LITELLM_* variable is documented) gains the three rows with defaults and allowed values: UNFOLD_INSIGHT_EXPORT (off default, faro, otlp), _URL (required for faro/otlp) and _LEVEL (aggregate default, events). The route contract is added to apps/unfold/docs/contracts/api.md. test/config.test.ts covers defaults, the missing-URL rejection, and both invalid-value rejections.
  2. Server-side only, CSP unchanged — met. The CSP header string in apps/unfold/src/http.ts:144 is untouched. The collector fetch lives in apps/unfold/src/insight.ts (send(), lines 224–234), inside the server process; nothing under public/ or the extension contacts the collector. The new route sits behind /api/ (authenticated), behind the same-origin X-Unfold-Request mutation guard, and viewers get 403 (http.ts:255), consistent with the other viewer checks at 285/326/345/364.
  3. Error path — met. send() (insight.ts:224) wraps the fetch in try/catch with a 10 s AbortSignal.timeout, logs at most once per hour (lastErrorAt gate, insight.ts:231), and ingest() never awaits delivery (void this.deliver(), insight.ts:196), so the event route cannot block. test/insight.test.ts proves both with a real unreachable collector: ingest timing < 1 s and exactly one insight.export_failed line for two failures.
  4. Dashboard JSON — one gap, see blocking finding. apps/unfold/ops/grafana/unfold-insight.json (uid unfold-insight) carries the stat panels and tables of docs/design/img/insight-tables.png (median time to resolve, tabs per decision, undo rate, regret rate, per-path table, confusion signals per screen) plus an import README, and it correctly keeps provisioning out of this repo. But one of the four stat panels does not suppress groups under 5 people.

Blocking finding (must change)

F1. Stat panel "Median time to resolve" does not suppress groups under 5 people — AC4 and the panel's own description are not satisfied.
apps/unfold/ops/grafana/unfold-insight.json, panel id: 1, target expression:

event.name:"needs_you.verdict_shown" OR event.name:"needs_you.command_sent" | stats by (work_item.id) count_uniq(actor) as actors, min(_time) as shown, max(_time) as decided | math decided - shown as seconds | stats median(seconds) as median_seconds

There is no filter actors:>=5 stage, so Work Items touched by fewer than 5 people are included in the median. The panel description claims "Cells with fewer than 5 distinct people are suppressed", and apps/unfold/ops/grafana/README.md claims "the stat queries with a filter actors:>=5 stage" — both are untrue for this panel. Panels 2, 3 and 4 all have the filter; the two tables suppress via the filterByValue transformation. Consequence: on the owner's single-user-first instance this panel is exactly the one most exposed to small groups (initially one person), and it would quietly report over a suppressed population, contrary to the RFC-0001 suppression rule the dashboard exists to implement. Fix: insert | filter actors:>=5 before the final stats stage, e.g. ... | math decided - shown as seconds | filter actors:>=5 | stats median(seconds) as median_seconds, and keep the description as is. (If the intent was that a median over per-item values is already aggregate, then the description and README must say so instead — but AC4's wording "suppressing groups under 5 people" reads as suppression everywhere.)

Checks run, and what is left to CI

  • $PLOEG_VERIFY_SCRIPT (gofmt on apps/ploeg): passed on this checkout.
  • node --test test/insight.test.ts: 6/6 pass. node --test test/config.test.ts: 7/7 pass (both include the new tests; run directly with node 24, which these modules allow since they import only node: built-ins).
  • node scripts/check.mjs (the check gate): passed (362 source modules, 43 JSON files).
  • The full mise run verify set (typecheck, test, check, design:check, icons:check, license:check, extension, helm, site, mise run docs-check) could not run in this sandbox: npm install fails — the sandbox has no registry egress (npm error network) and node_modules is absent — and docs-check additionally aborts because the apps/ploeg submodule is not checked out. test/api-insight.test.ts therefore could not execute here; it fails to load with Cannot find package 'ws', which reproduces identically on the untouched test/api-oidc.test.ts, i.e. the failure is environmental, not in the new code. Gates left to CI: npm test (especially test/api-insight.test.ts), npm run typecheck, and mise run docs-check after the docs changes.

I read the integration test against the code: it exercises the real route (no mocks around the code under test — the "fake collector" is a fixture node:http server, which is exactly what the Work Item's verification asks for), asserts the Faro payload shape (meta.sdk.name, event.name, tenant.id, the actor hash), that an unlisted property (secret) never reaches the store or the collector, that a >32 KB batch gets 413 and a viewer 403. The accessors match createApplication's real return shape ({ server, store, engine, agentHost, close }, main.ts:51).

Repository rules

  • Conventional commit (feat(unfold): ...) describing exactly this change. ✓
  • No execution feature added; this is workbench-only measurement. ✓
  • Source comments are API documentation on exported symbols (insight.ts, store.ts, config.ts), no reasoning comments. ✓
  • Demo safety: the sink defaults to off (loadConfig sets insight: insightSettings() which is undefined with no env vars), the demo config never sets it, and the deterministic-demo promise (no invented model calls or spend) is untouched. ✓
  • Production desired state stays in webgrip/homelab-cluster; the README says so explicitly. ✓

Non-blocking observations (a reviewer or follow-up may want these)

  • O1. The Work Item says "Blocked by the product-events ticket", but development has no product-event route, table or catalogue — this PR builds the RFC-0001 route/store/rollup as the foundation its integration test needs. That is the right call for this ticket's outcome and matches RFC-0001's specified contract (50 events/32 KB/202, catalogue drop semantics, 16-char base32 HMAC actor hash, 25-month retention), but the blocked-by ticket, when it lands its client side, should reuse this route rather than ship a second one.
  • O2. ADR-0023 specifies the actor key "rotated every 13 months"; InsightService stores insight:actorKey once and never rotates it. Not in this ticket's acceptance criteria — fine to defer, but it should be a tracked follow-up.
  • O3. An events-level sink drops rows from the pending queue after one failed send attempt (insight.ts:214 splices before send); they remain in product_event, and aggregate level is self-healing because maintain() re-sends the whole rollup hourly, but events-level events that fail once are never re-exported. Acceptable under AC3's letter ("never blocks"), worth a line in the docs when the homelab follow-up lands.
  • O4. maintain() runs on an hourly setInterval via void this.maintain() (insight.ts:184); a synchronous SQLite throw inside it would surface as an unhandled rejection on a bare void. This matches the codebase's existing timer convention (AgentHost.startPolling, host.ts:155), so I am not blocking on it, but a .catch() here would be cheap insurance.

What a human should re-check

The LogsQL queries are labelled proposed-with-RFC-0001 and cannot be validated against the homelab's VictoriaLogs field mapping from here; they need a look once real data flows, as the dashboard's own About panel says. And CI must come back green on the PR (especially typecheck and the full npm test, which this sandbox could not run) before merge.

Posted by Ploeg on behalf of the reviewing agent. It could not push to this branch.

### reviewer — round 2 _Summary: Reviewed VIK-1897; verdict request_changes; outcome file written Finish with message: Review of VIK-1897 complete. Verdict: **request_changes**. ## What I reviewed Read the full diff (`development...agent/vik-1897`, 13 files, +789/−2) against the Work Item, RFC-0001, ADR-0023, and the base-branch AGENTS.md files, plus the PR's own description context. ## Verdict rationale **Met:** AC1 (…_ ## Scope: the change does what the Work Item asks The diff (`git diff development...agent/vik-1897`, 13 files, +789/−2) adds the server-side insight export and nothing else. No changes under `apps/ploeg/` (submodule untouched, gofmt check passed), no helm/chart or homelab desired state, no changes to `public/`, `extensions/` or any agent-configuration file (`AGENTS.md`, `CLAUDE.md`, `.claude/`, `.agents/`, `.openhands/`, `.mcp.json`, `.cursorrules` are byte-identical to `development`). Per acceptance criterion: 1. **Env vars documented — met.** `apps/unfold/docs/operations/live.md` (the application's configuration reference for operators, where every other `UNFOLD_*`/`LITELLM_*` variable is documented) gains the three rows with defaults and allowed values: `UNFOLD_INSIGHT_EXPORT` (`off` default, `faro`, `otlp`), `_URL` (required for `faro`/`otlp`) and `_LEVEL` (`aggregate` default, `events`). The route contract is added to `apps/unfold/docs/contracts/api.md`. `test/config.test.ts` covers defaults, the missing-URL rejection, and both invalid-value rejections. 2. **Server-side only, CSP unchanged — met.** The CSP header string in `apps/unfold/src/http.ts:144` is untouched. The collector `fetch` lives in `apps/unfold/src/insight.ts` (`send()`, lines 224–234), inside the server process; nothing under `public/` or the extension contacts the collector. The new route sits behind `/api/` (authenticated), behind the same-origin `X-Unfold-Request` mutation guard, and viewers get 403 (http.ts:255), consistent with the other viewer checks at 285/326/345/364. 3. **Error path — met.** `send()` (insight.ts:224) wraps the fetch in try/catch with a 10 s `AbortSignal.timeout`, logs at most once per hour (`lastErrorAt` gate, insight.ts:231), and `ingest()` never awaits delivery (`void this.deliver()`, insight.ts:196), so the event route cannot block. `test/insight.test.ts` proves both with a real unreachable collector: ingest timing < 1 s and exactly one `insight.export_failed` line for two failures. 4. **Dashboard JSON — one gap, see blocking finding.** `apps/unfold/ops/grafana/unfold-insight.json` (uid `unfold-insight`) carries the stat panels and tables of `docs/design/img/insight-tables.png` (median time to resolve, tabs per decision, undo rate, regret rate, per-path table, confusion signals per screen) plus an import README, and it correctly keeps provisioning out of this repo. But one of the four stat panels does not suppress groups under 5 people. ## Blocking finding (must change) **F1. Stat panel "Median time to resolve" does not suppress groups under 5 people — AC4 and the panel's own description are not satisfied.** `apps/unfold/ops/grafana/unfold-insight.json`, panel `id: 1`, target expression: ``` event.name:"needs_you.verdict_shown" OR event.name:"needs_you.command_sent" | stats by (work_item.id) count_uniq(actor) as actors, min(_time) as shown, max(_time) as decided | math decided - shown as seconds | stats median(seconds) as median_seconds ``` There is no `filter actors:>=5` stage, so Work Items touched by fewer than 5 people are included in the median. The panel description claims "Cells with fewer than 5 distinct people are suppressed", and `apps/unfold/ops/grafana/README.md` claims "the stat queries with a `filter actors:>=5` stage" — both are untrue for this panel. Panels 2, 3 and 4 all have the filter; the two tables suppress via the `filterByValue` transformation. Consequence: on the owner's single-user-first instance this panel is exactly the one most exposed to small groups (initially one person), and it would quietly report over a suppressed population, contrary to the RFC-0001 suppression rule the dashboard exists to implement. Fix: insert `| filter actors:>=5` before the final stats stage, e.g. `... | math decided - shown as seconds | filter actors:>=5 | stats median(seconds) as median_seconds`, and keep the description as is. (If the intent was that a median over per-item values is already aggregate, then the description and README must say so instead — but AC4's wording "suppressing groups under 5 people" reads as suppression everywhere.) ## Checks run, and what is left to CI - `$PLOEG_VERIFY_SCRIPT` (gofmt on `apps/ploeg`): **passed** on this checkout. - `node --test test/insight.test.ts`: **6/6 pass**. `node --test test/config.test.ts`: **7/7 pass** (both include the new tests; run directly with node 24, which these modules allow since they import only `node:` built-ins). - `node scripts/check.mjs` (the `check` gate): **passed** (362 source modules, 43 JSON files). - The full `mise run verify` set (`typecheck`, `test`, `check`, `design:check`, `icons:check`, `license:check`, extension, helm, site, `mise run docs-check`) **could not run in this sandbox**: `npm install` fails — the sandbox has no registry egress (npm error network) and `node_modules` is absent — and `docs-check` additionally aborts because the `apps/ploeg` submodule is not checked out. `test/api-insight.test.ts` therefore could not execute here; it fails to load with `Cannot find package 'ws'`, which reproduces identically on the untouched `test/api-oidc.test.ts`, i.e. the failure is environmental, not in the new code. **Gates left to CI: `npm test` (especially `test/api-insight.test.ts`), `npm run typecheck`, and `mise run docs-check` after the docs changes.** I read the integration test against the code: it exercises the real route (no mocks around the code under test — the "fake collector" is a fixture `node:http` server, which is exactly what the Work Item's verification asks for), asserts the Faro payload shape (`meta.sdk.name`, `event.name`, `tenant.id`, the actor hash), that an unlisted property (`secret`) never reaches the store or the collector, that a >32 KB batch gets 413 and a viewer 403. The accessors match `createApplication`'s real return shape (`{ server, store, engine, agentHost, close }`, main.ts:51). ## Repository rules - Conventional commit (`feat(unfold): ...`) describing exactly this change. ✓ - No execution feature added; this is workbench-only measurement. ✓ - Source comments are API documentation on exported symbols (insight.ts, store.ts, config.ts), no reasoning comments. ✓ - Demo safety: the sink defaults to `off` (`loadConfig` sets `insight: insightSettings()` which is `undefined` with no env vars), the demo config never sets it, and the deterministic-demo promise (no invented model calls or spend) is untouched. ✓ - Production desired state stays in `webgrip/homelab-cluster`; the README says so explicitly. ✓ ## Non-blocking observations (a reviewer or follow-up may want these) - **O1.** The Work Item says "Blocked by the product-events ticket", but `development` has no product-event route, table or catalogue — this PR builds the RFC-0001 route/store/rollup as the foundation its integration test needs. That is the right call for this ticket's outcome and matches RFC-0001's specified contract (50 events/32 KB/202, catalogue drop semantics, 16-char base32 HMAC actor hash, 25-month retention), but the blocked-by ticket, when it lands its client side, should reuse this route rather than ship a second one. - **O2.** ADR-0023 specifies the actor key "rotated every 13 months"; `InsightService` stores `insight:actorKey` once and never rotates it. Not in this ticket's acceptance criteria — fine to defer, but it should be a tracked follow-up. - **O3.** An `events`-level sink drops rows from the pending queue after one failed send attempt (insight.ts:214 splices before `send`); they remain in `product_event`, and `aggregate` level is self-healing because `maintain()` re-sends the whole rollup hourly, but `events`-level events that fail once are never re-exported. Acceptable under AC3's letter ("never blocks"), worth a line in the docs when the homelab follow-up lands. - **O4.** `maintain()` runs on an hourly `setInterval` via `void this.maintain()` (insight.ts:184); a synchronous SQLite throw inside it would surface as an unhandled rejection on a bare `void`. This matches the codebase's existing timer convention (`AgentHost.startPolling`, host.ts:155), so I am not blocking on it, but a `.catch()` here would be cheap insurance. ## What a human should re-check The LogsQL queries are labelled proposed-with-RFC-0001 and cannot be validated against the homelab's VictoriaLogs field mapping from here; they need a look once real data flows, as the dashboard's own About panel says. And CI must come back green on the PR (especially `typecheck` and the full `npm test`, which this sandbox could not run) before merge. <sub>Posted by Ploeg on behalf of the reviewing agent. It could not push to this branch.</sub>
fix(unfold): suppress small groups in the median time to resolve panel
Some checks failed
[Workflow] On Pull Request / ploeg-pin (pull_request) Successful in 30s
[Workflow] On Pull Request / release-policy (pull_request) Failing after 23s
[Workflow] On Pull Request / checks (pull_request) Failing after 2m26s
[Workflow] On Pull Request / warnings (pull_request) Successful in 0s
046e01208f
The panel's query lacked the actors:>=5 stage its description and the
dashboard README promise, so Work Items touched by fewer than five
distinct people leaked into the median.

VIK-1897
Agent-Trace-Id: ploeg-8e547212c56b
Some checks failed
[Workflow] On Pull Request / ploeg-pin (pull_request) Successful in 30s
[Workflow] On Pull Request / release-policy (pull_request) Failing after 23s
[Workflow] On Pull Request / checks (pull_request) Failing after 2m26s
[Workflow] On Pull Request / warnings (pull_request) Successful in 0s
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin agent/vik-1897:agent/vik-1897
git switch agent/vik-1897

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch development
git merge --no-ff agent/vik-1897
git switch agent/vik-1897
git rebase development
git switch development
git merge --ff-only agent/vik-1897
git switch agent/vik-1897
git rebase development
git switch development
git merge --no-ff agent/vik-1897
git switch development
git merge --squash agent/vik-1897
git switch development
git merge --ff-only agent/vik-1897
git switch development
git merge agent/vik-1897
git push origin development
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
webgrip/unfold!249
No description provided.