Skip to content

feat: Plan 9 PR B — provider read APIs + provider view - #27

Merged
messagesgoel-blip merged 2 commits into
mainfrom
feat/dashboard-pr-b
Aug 7, 2026
Merged

feat: Plan 9 PR B — provider read APIs + provider view#27
messagesgoel-blip merged 2 commits into
mainfrom
feat/dashboard-pr-b

Conversation

@messagesgoel-blip

@messagesgoel-blip messagesgoel-blip commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Plan 9 PR B — provider data path

@coderabbitai review

Implements the PR B slice of docs/superpowers/plans/2026-08-01-plan-9-dashboard.md (PR A shell merged in #26).

Control plane

  • X-Tenant-Id validation in authenticateOidc (Decision 4): header validated against tenant_memberships, sets active tenantId/role; 403 on mismatch; falls back to first membership when absent. Pure helper selectActiveMembership + unit tests.
  • GET /v1/policies/active — active policy for caller tenant (404 when none).
  • GET /v1/decisions/aggregates?from&to&dimension — per-minute rollups summed across edges; window defaults to last 24h, capped at 31d (risk note).
  • GET /v1/decisions/samples?from&to&limit — sampled feed, newest first (limit <= 500).
  • GET /v1/decisions/agents?from&to&limit — provider agent list: distinct principals in the tenant's sampled decisions joined to current network_scores (blacklisted / score_reason surfaced from the score row only, never inferred — Decision 5).
  • GET /v1/scores/:principalId/history — current score + network_score_history items.
  • GET /v1/graph/summary — node/edge counts, top issuers by outgoing volume (read-only), latest_score_computed_at for the staleness banner (Decision 8).
  • GET /v1/edge-nodes — tenant fleet + sync lag vs high-water mark.
  • decisions router restructured: POST /batch stays apiKeyOnly; reads use authMiddleware (OIDC or API key).
  • resetTestData now truncates decision/policy/edge tables for suite isolation.

Dashboard

  • Provider view (/provider): trust-summary stacked bar chart (recharts), agent list, sampled decision feed, edge sync status, read-only active policy card.
  • Staleness banner when the latest score write is older than 1h (Decision 8).
  • vitest: staleness helper, aggregate pivot, panel components (incl. "blacklist not inferred from score==0").

Verification

  • CP: tsc --noEmit clean; 129 unit + 30 integration tests pass (7 new integration tests cover authz, tenant isolation, window bounds).
  • Dashboard: typecheck clean; 26 vitest pass; vite build succeeds.

Out of scope (per plan): policy PUT, API-key CRUD, agent-builder view (PR C); billing + admin (PR D). No new migrations (Decision 12).

Summary by CodeRabbit

  • New Features
    • Added provider dashboard views for trust trends, agents, decision activity, edge synchronization, graph health, and active policies.
    • Added authenticated APIs for tenant-scoped policies, scores, decisions, graph summaries, and edge nodes.
    • Added tenant selection and isolation support, including tenant-header validation.
    • Added time-range filtering with validation and practical limits.
  • Bug Fixes
    • Added stale-score warnings and clearer loading, error, and empty states.
  • Tests
    • Added comprehensive coverage for dashboard reads, authentication, tenant isolation, filtering, and data validation.

CP:
- authenticateOidc validates X-Tenant-Id against tenant_memberships
  (403 on mismatch; falls back to first membership when absent) — Decision 4
- GET /v1/policies/active (tenant-scoped)
- GET /v1/decisions/aggregates|samples|agents (tenant-scoped reads;
  bounded from/to window: default 24h, max 31d)
- GET /v1/scores/:principalId/history (current + network_score_history)
- GET /v1/graph/summary (counts, top issuers, latest score write time)
- GET /v1/edge-nodes (tenant fleet + sync lag fields)
- decisions router: /batch stays apiKeyOnly; reads use authMiddleware
- integration coverage for all new routes (authz, isolation, windows)

Dashboard:
- provider view: trust-summary stacked chart (recharts), agent list
  (score + blacklisted + score_reason; blacklist never inferred from
  score), sampled decision feed, edge sync status, active policy card
- staleness banner when latest score write is older than 1h (Decision 8)
- vitest: staleness + aggregate pivot + panel components
@cursor

cursor Bot commented Aug 7, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 50 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: aa9707e8-bace-4a65-a604-d7b8f5886a3b

📥 Commits

Reviewing files that changed from the base of the PR and between 60c92f7 and 8681675.

📒 Files selected for processing (6)
  • control-plane/src/lib/timeWindow.test.ts
  • control-plane/src/lib/timeWindow.ts
  • dashboard/src/lib/queryKeys.test.ts
  • dashboard/src/lib/queryKeys.ts
  • dashboard/src/pages/ProviderHomePage.tsx
  • dashboard/src/styles/index.css

Walkthrough

The control plane adds tenant-scoped provider read APIs for policies, decisions, scores, graph data, and edge nodes. The dashboard consumes these APIs and renders provider panels with charts, tables, synchronization status, policy data, and stale-score indicators.

Changes

Provider read APIs

Layer / File(s) Summary
Tenant context and bounded windows
control-plane/src/lib/*, control-plane/src/middleware/auth.ts
Tenant selection honors X-Tenant-Id when the user has that membership. Read windows default to 24 hours and reject invalid, reversed, or over-31-day ranges.
Read repositories
control-plane/src/domains/decision/*, control-plane/src/domains/edgenode/*, control-plane/src/domains/policy/*, control-plane/src/domains/score/*
Repositories return tenant decision aggregates, samples, agents, edge synchronization data, active policies, score history, and graph summaries.
Authenticated routes and validation
control-plane/src/routes/*, control-plane/src/app.ts, control-plane/src/testutil/testDb.ts, control-plane/src/__tests__/integration/dashboard-reads.test.ts
The new routes are mounted under /v1. Integration tests cover authentication, tenant isolation, filtering, ordering, limits, aggregation, and synchronization metadata.

Provider dashboard

Layer / File(s) Summary
Dashboard API and data utilities
dashboard/src/api/provider.ts, dashboard/src/lib/*, dashboard/package.json
Typed fetchers retrieve provider data. Aggregate rows become chart points, and score timestamps produce a one-hour staleness state.
Provider panels and styling
dashboard/src/pages/ProviderHomePage.tsx, dashboard/src/pages/provider/*, dashboard/src/styles/index.css, dashboard/src/pages/provider/components.test.tsx
The provider page renders trust charts, agents, decisions, edge synchronization, active policy data, loading states, errors, empty states, and stale-score banners.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ProviderHomePage
  participant provider_api
  participant ControlPlane
  participant Database
  ProviderHomePage->>provider_api: fetch provider dashboard data
  provider_api->>ControlPlane: request authenticated read APIs
  ControlPlane->>Database: query tenant-scoped provider data
  Database-->>ControlPlane: aggregates, samples, scores, nodes, policy
  ControlPlane-->>provider_api: typed API responses
  provider_api-->>ProviderHomePage: dashboard data
  ProviderHomePage->>ProviderHomePage: pivot aggregates and compute staleness
  ProviderHomePage-->>ProviderHomePage: render provider panels
Loading

Possibly related PRs

  • Numeracode/verilink#26: The dashboard changes replace the existing ProviderHomePage placeholder and extend its provider routes and styling.
  • Numeracode/verilink#12: The score read APIs consume score computation and persistence data from this control-plane foundation.
  • Numeracode/verilink#20: The decision, edge-node, policy, and score read APIs consume data produced by the synchronization pipeline.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.93% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the provider read APIs and provider dashboard view added by this pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dashboard-pr-b

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Plan 9: Provider read APIs + dashboard provider view

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add tenant-scoped provider read endpoints for policies, decisions, edges, and scores.
• Enforce X-Tenant-Id membership selection in OIDC auth with bounded time windows.
• Implement /provider dashboard view with charts, staleness banner, and tests.
Diagram

graph TD
  ui["Dashboard \"/provider\""] --> api["provider API client"] --> cp["Control Plane \"/v1\""] --> auth(["authMiddleware + X-Tenant-Id"]) --> db[("Postgres")]
  cp --> repo["Read repositories"] --> db
  ui --> panels["Provider panels + charts"]

  subgraph Legend
    direction LR
    _ui["UI/Modules"] ~~~ _mw(["Middleware"]) ~~~ _db[("Database")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Shared tenant-required middleware
  • ➕ Eliminates per-route Tenant required checks and keeps 403 behavior consistent.
  • ➕ Easier to audit tenant scoping across new routes.
  • ➖ Adds another middleware layer to the stack; may be overkill while route count is small.
  • ➖ Some routes (graph/scores) are intentionally non-tenant-scoped and would need bypass logic.
2. Central query coercion for numeric params
  • ➕ Avoids repeating parseInt(req.query.limit as string, 10) and reduces subtle NaN/edge cases.
  • ➕ Keeps handler logic focused on business intent (limit/window) vs parsing.
  • ➖ Requires extending or adjusting defineHandler/request typing conventions.
  • ➖ Not strictly necessary given current validation bounds.

Recommendation: Current approach is sound for PR B: clear separation of ingest vs read paths, explicit tenant scoping where required, and shared-graph reads called out in docs/comments. Consider a follow-up to introduce a requireTenant middleware/helper shared across routers, and to standardize query param coercion once more endpoints are added in PR C/D.

Files changed (34) +2214 / -18

Enhancement (24) +1144 / -8
app.tsMount new provider read routers +8/-2

Mount new provider read routers

• Registers policies, scores, graph, and edge-nodes routers under /v1. Moves these from placeholder comments into the live app routing table.

control-plane/src/app.ts

decisionReadRepository.tsDecision read repository for aggregates, samples, and agent list +113/-0

Decision read repository for aggregates, samples, and agent list

• Introduces SQL read helpers for per-minute aggregates (summed across edges), sampled decision feeds (newest-first), and distinct agent rows joined to current network_scores. Explicitly treats 'blacklisted' as sourced only from network_scores.

control-plane/src/domains/decision/decisionReadRepository.ts

edgeNodeRepository.tsEdge fleet read with sync lag computation +40/-0

Edge fleet read with sync lag computation

• Adds tenant-scoped edge node listing with lag computed against global sync_events high-water mark. Includes cursor defaults for edges that never synced.

control-plane/src/domains/edgenode/edgeNodeRepository.ts

policyRepository.tsActive policy read for tenant +33/-0

Active policy read for tenant

• Adds a repository function to fetch the active policy row for a tenant, returning null when none exists. Normalizes allow_sample_rate to float for API use.

control-plane/src/domains/policy/policyRepository.ts

scoreReadRepository.tsScore reads plus graph health summary queries +106/-0

Score reads plus graph health summary queries

• Adds read accessors for current score, score history ordered by sync_version, and a graph summary (principal counts, attestation counts, top issuers, latest score computed timestamp). Uses parallel queries for summary construction.

control-plane/src/domains/score/scoreReadRepository.ts

activeMembership.tsSelect active tenant membership from X-Tenant-Id +33/-0

Select active tenant membership from X-Tenant-Id

• Implements Plan 9 Decision 4 tenant selection: validates X-Tenant-Id against memberships, falls back to first membership, and returns null tenant when none exist. Throws a 403 AppError on invalid tenant selection.

control-plane/src/lib/activeMembership.ts

timeWindow.tsResolve bounded from/to window for read APIs +30/-0

Resolve bounded from/to window for read APIs

• Introduces a helper that parses optional from/to values, defaults to last 24h, enforces from <= to, and caps range at 31 days. Uses AppError(400) for invalid inputs.

control-plane/src/lib/timeWindow.ts

auth.tsValidate X-Tenant-Id against memberships during OIDC auth +8/-2

Validate X-Tenant-Id against memberships during OIDC auth

• Updates authenticateOidc to honor X-Tenant-Id by selecting an active membership via selectActiveMembership. Ensures mismatched tenant headers are rejected rather than silently ignored.

control-plane/src/middleware/auth.ts

decisions.tsSplit decision ingest vs read routes with tenant/window handling +83/-3

Split decision ingest vs read routes with tenant/window handling

• Keeps POST /batch protected by apiKeyOnly while adding authMiddleware-protected GET routes for aggregates, samples, and agents. Enforces tenant presence for read endpoints and applies bounded time windows and limit caps.

control-plane/src/routes/decisions.ts

edgeNodes.tsAdd GET /v1/edge-nodes tenant fleet endpoint +30/-0

Add GET /v1/edge-nodes tenant fleet endpoint

• Introduces a tenant-scoped endpoint returning edge nodes with cursor/high-water/lag fields. Requires authentication and a selected tenant.

control-plane/src/routes/edgeNodes.ts

graph.tsAdd GET /v1/graph/summary endpoint +26/-0

Add GET /v1/graph/summary endpoint

• Adds an authenticated endpoint exposing global graph counts, top issuers by outgoing attestations, and the latest score write time for dashboard staleness detection. Intentionally not tenant-scoped per shared-graph decision.

control-plane/src/routes/graph.ts

policies.tsAdd GET /v1/policies/active endpoint +33/-0

Add GET /v1/policies/active endpoint

• Adds a tenant-scoped endpoint that returns the active policy or 404 when none exists. Requires authentication and an active tenant selection.

control-plane/src/routes/policies.ts

scores.tsAdd GET /v1/scores/:principalId/history endpoint +33/-0

Add GET /v1/scores/:principalId/history endpoint

• Adds an authenticated endpoint returning current score plus score history for a principal, with an optional limit. Treats scores as global/shared rather than tenant-scoped.

control-plane/src/routes/scores.ts

provider.tsTyped client for provider read APIs +119/-0

Typed client for provider read APIs

• Introduces typed fetch functions for aggregates, samples, agents, edge nodes, graph summary, and active policy. Handles 404 for active policy as a null result for easier UI consumption.

dashboard/src/api/provider.ts

aggregates.tsAggregate pivoting for stacked chart series +46/-0

Aggregate pivoting for stacked chart series

• Adds helpers to pivot per-minute aggregate rows into chart-ready points and to format bucket labels as local HH:MM. Ensures unknown actions don't break chart bucketing.

dashboard/src/lib/aggregates.ts

staleness.tsStaleness detection helper for score writes +15/-0

Staleness detection helper for score writes

• Implements Plan 9 Decision 8 staleness rule: treat missing/invalid timestamps as stale and flag writes older than one hour. Intended for provider and future agent-builder banners.

dashboard/src/lib/staleness.ts

ProviderHomePage.tsxImplement provider home page with data panels +81/-1

Implement provider home page with data panels

• Builds the provider view by wiring React Query to new provider API fetchers and rendering panels for trust summary, agents, decision feed, edge sync status, and active policy. Adds a reusable panel loading/error state and shows staleness banner based on graph summary timestamp.

dashboard/src/pages/ProviderHomePage.tsx

AgentList.tsxAgent table with score/blacklist status rendering +51/-0

Agent table with score/blacklist status rendering

• Renders a table of distinct agents with decision counts, current score, and status badges. Explicitly avoids inferring blacklist from score values and shows an unscored state when score fields are null.

dashboard/src/pages/provider/AgentList.tsx

DecisionFeed.tsxSampled decision feed table +39/-0

Sampled decision feed table

• Adds a table for sampled decisions showing time, fingerprint, action badge, score, and reason. Includes an empty state for quiet tenants/windows.

dashboard/src/pages/provider/DecisionFeed.tsx

EdgeSyncStatus.tsxEdge sync status table with lag highlighting +39/-0

Edge sync status table with lag highlighting

• Displays tenant edge nodes with cursor, lag, and last sync time. Uses BigInt lag comparison to highlight lagging edges and handles edges that never synced.

dashboard/src/pages/provider/EdgeSyncStatus.tsx

PolicyCard.tsxRead-only active policy summary card +36/-0

Read-only active policy summary card

• Adds a compact key/value display for the active policy and renders a null state when no policy exists. Notes that editing arrives in Plan 9 PR C.

dashboard/src/pages/provider/PolicyCard.tsx

StalenessBanner.tsxNon-blocking stale scores banner component +8/-0

Non-blocking stale scores banner component

• Adds a small banner component used when the score staleness threshold is exceeded. Renders nothing when the data is considered fresh.

dashboard/src/pages/provider/StalenessBanner.tsx

TrustSummaryChart.tsxStacked bar chart for per-minute decision volume +39/-0

Stacked bar chart for per-minute decision volume

• Implements a recharts stacked BarChart over allow/deny/passthrough counts with legend/tooltip and responsive sizing. Handles empty series with a muted empty state.

dashboard/src/pages/provider/TrustSummaryChart.tsx

index.cssProvider view styling for panels, tables, badges, banner, chart +95/-0

Provider view styling for panels, tables, badges, banner, chart

• Adds CSS for panel spacing, error text, staleness banner, table layout, numeric alignment, action/status badges, key/value grid, and chart spacing. Scoped to provider view UI needs.

dashboard/src/styles/index.css

Tests (7) +677 / -0
dashboard-reads.test.tsIntegration coverage for new provider read endpoints +380/-0

Integration coverage for new provider read endpoints

• Adds integration tests for policies/active, decisions aggregates/samples/agents, score history, graph summary, and edge-nodes. Validates tenant isolation, default/maximum windows, auth requirements, ordering, and cross-edge aggregate summing.

control-plane/src/tests/integration/dashboard-reads.test.ts

activeMembership.test.tsUnit tests for active tenant selection behavior +50/-0

Unit tests for active tenant selection behavior

• Covers default selection, explicit tenant header selection, mismatch 403, and membership-less scenarios. Ensures error status codes are correct via AppError assertions.

control-plane/src/lib/activeMembership.test.ts

timeWindow.test.tsUnit tests for bounded decision read time window +50/-0

Unit tests for bounded decision read time window

• Adds coverage for default 24h window, explicit windows, anchoring by 'to', invalid timestamps, reversed ranges, and enforcing the 31-day maximum range.

control-plane/src/lib/timeWindow.test.ts

testDb.tsImprove test isolation by truncating new dashboard tables +6/-0

Improve test isolation by truncating new dashboard tables

• Extends resetTestData to truncate decisions/policies/edge and sync cursor tables added for dashboard reads. Prevents cross-test contamination in integration suites.

control-plane/src/testutil/testDb.ts

aggregates.test.tsUnit tests for aggregate pivot/label helpers +72/-0

Unit tests for aggregate pivot/label helpers

• Tests pivoting rows into per-bucket stacked points, sorting behavior, summing duplicates, ignoring unknown actions, and label formatting robustness.

dashboard/src/lib/aggregates.test.ts

staleness.test.tsUnit tests for score staleness logic +30/-0

Unit tests for score staleness logic

• Covers fresh vs stale determinations relative to a 1-hour threshold, including boundary behavior, missing values, and invalid timestamps.

dashboard/src/lib/staleness.test.ts

components.test.tsxComponent tests for provider panels and invariants +89/-0

Component tests for provider panels and invariants

• Adds React Testing Library tests for staleness banner rendering, agent list status logic (including non-inference of blacklist from score==0), and decision feed rendering/empty states.

dashboard/src/pages/provider/components.test.tsx

Documentation (1) +8 / -7
HANDOVER.mdUpdate Plan 9 handover status for PR B in flight +8/-7

Update Plan 9 handover status for PR B in flight

• Updates the handover note date and status to reflect PR A merged and PR B in progress. Records next steps for PR C writes and agent-builder work.

docs/superpowers/plans/HANDOVER.md

Other (2) +385 / -3
package-lock.jsonLockfile update for recharts dependency +383/-2

Lockfile update for recharts dependency

• Adds recharts and its transitive dependencies to support chart rendering in the provider view. Updates package-lock to reflect the new dependency graph.

dashboard/package-lock.json

package.jsonAdd recharts dependency for provider trust chart +2/-1

Add recharts dependency for provider trust chart

• Adds recharts to dashboard dependencies for rendering the trust-summary stacked bar chart. Keeps existing React/TanStack stack intact.

dashboard/package.json

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@control-plane/src/domains/edgenode/edgeNodeRepository.ts`:
- Around line 23-35: Scope the hw CTE’s MAX(sync_version) calculation to the
caller tenant by filtering sync_events with the query’s tenant parameter before
computing high_water_version. Keep the outer edge-node and cursor joins
unchanged, and add an integration case where another tenant has a later
sync_events row to verify the reported lag and high-water mark remain
tenant-specific.

In `@control-plane/src/lib/timeWindow.ts`:
- Around line 7-13: Update parseOptional to validate non-empty string inputs
against the accepted ISO 8601 timestamp format before calling new Date,
rejecting values such as “January 2, 2024” and “01/02/2024” with the existing
BAD_REQUEST error. Add tests covering these non-ISO inputs while preserving null
handling and valid ISO parsing.

In `@control-plane/src/routes/scores.ts`:
- Around line 21-28: Enforce tenant scoping in both handlers: in scores.ts,
require req.user.tenantId and pass it to the scoreRead methods so principal
authorization occurs before returning score data; in graph.ts, require the
tenant ID and use the tenant-scoped graph-summary query. Add integration tests
using tenant B credentials to verify tenant A’s score and graph data, including
global counts, issuer activity, and score-write metadata, cannot be read.
Affected sites: control-plane/src/routes/scores.ts lines 21-28 and
control-plane/src/routes/graph.ts lines 20-22.

In `@dashboard/src/pages/ProviderHomePage.tsx`:
- Around line 41-49: Update the six useQuery definitions in ProviderHomePage,
including summaryQuery, aggregatesQuery, agentsQuery, samplesQuery, edgesQuery,
and policyQuery, to include the active tenant ID in each queryKey so tenant
changes partition cached data and trigger refetches. Add a tenant-switch test
verifying that results from the previous tenant are not rendered after switching
tenants.

In `@dashboard/src/styles/index.css`:
- Around line 263-266: Update the color declaration in .badge--passthrough so
its text achieves at least 4.5:1 contrast against the existing `#f0ead2`
background, while preserving the badge’s current styling and structure.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e4bf1cc9-a4f9-4b10-b19e-38321410c258

📥 Commits

Reviewing files that changed from the base of the PR and between cd5a043 and 60c92f7.

⛔ Files ignored due to path filters (1)
  • dashboard/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (33)
  • control-plane/src/__tests__/integration/dashboard-reads.test.ts
  • control-plane/src/app.ts
  • control-plane/src/domains/decision/decisionReadRepository.ts
  • control-plane/src/domains/edgenode/edgeNodeRepository.ts
  • control-plane/src/domains/policy/policyRepository.ts
  • control-plane/src/domains/score/scoreReadRepository.ts
  • control-plane/src/lib/activeMembership.test.ts
  • control-plane/src/lib/activeMembership.ts
  • control-plane/src/lib/timeWindow.test.ts
  • control-plane/src/lib/timeWindow.ts
  • control-plane/src/middleware/auth.ts
  • control-plane/src/routes/decisions.ts
  • control-plane/src/routes/edgeNodes.ts
  • control-plane/src/routes/graph.ts
  • control-plane/src/routes/policies.ts
  • control-plane/src/routes/scores.ts
  • control-plane/src/testutil/testDb.ts
  • dashboard/package.json
  • dashboard/src/api/provider.ts
  • dashboard/src/lib/aggregates.test.ts
  • dashboard/src/lib/aggregates.ts
  • dashboard/src/lib/staleness.test.ts
  • dashboard/src/lib/staleness.ts
  • dashboard/src/pages/ProviderHomePage.tsx
  • dashboard/src/pages/provider/AgentList.tsx
  • dashboard/src/pages/provider/DecisionFeed.tsx
  • dashboard/src/pages/provider/EdgeSyncStatus.tsx
  • dashboard/src/pages/provider/PolicyCard.tsx
  • dashboard/src/pages/provider/StalenessBanner.tsx
  • dashboard/src/pages/provider/TrustSummaryChart.tsx
  • dashboard/src/pages/provider/components.test.tsx
  • dashboard/src/styles/index.css
  • docs/superpowers/plans/HANDOVER.md

Comment on lines +23 to +35
`WITH hw AS (
SELECT COALESCE(MAX(sync_version), 0) AS high_water_version FROM sync_events
)
SELECT en.id, en.name, en.status, en.last_seen_at,
en.last_sync_version::text AS last_sync_version, en.created_at,
sc.last_cursor::text AS last_cursor, sc.last_sync_at,
hw.high_water_version::text AS high_water_version,
(hw.high_water_version - COALESCE(sc.last_cursor, 0))::text AS lag
FROM edge_nodes en
LEFT JOIN sync_cursors sc
ON sc.tenant_id = en.tenant_id AND sc.edge_node_id = en.id
CROSS JOIN hw
WHERE en.tenant_id = $1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Scope the sync high-water mark to the caller tenant.

Line 23 calculates high_water_version from every sync_events row. The outer query returns one tenant’s edges and cursors.

For example, a tenant A edge at cursor 5 reports a lag of 95 when tenant A has events only through version 10 and tenant B has an event at version 100. This also exposes cross-tenant synchronization progress.

Proposed fix
      `WITH hw AS (
-       SELECT COALESCE(MAX(sync_version), 0) AS high_water_version FROM sync_events
+       SELECT COALESCE(MAX(sync_version), 0) AS high_water_version
+       FROM sync_events
+       WHERE tenant_id = $1
      )

Add an integration case with a later sync_events row for another tenant.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
`WITH hw AS (
SELECT COALESCE(MAX(sync_version), 0) AS high_water_version FROM sync_events
)
SELECT en.id, en.name, en.status, en.last_seen_at,
en.last_sync_version::text AS last_sync_version, en.created_at,
sc.last_cursor::text AS last_cursor, sc.last_sync_at,
hw.high_water_version::text AS high_water_version,
(hw.high_water_version - COALESCE(sc.last_cursor, 0))::text AS lag
FROM edge_nodes en
LEFT JOIN sync_cursors sc
ON sc.tenant_id = en.tenant_id AND sc.edge_node_id = en.id
CROSS JOIN hw
WHERE en.tenant_id = $1
`WITH hw AS (
SELECT COALESCE(MAX(sync_version), 0) AS high_water_version
FROM sync_events
WHERE tenant_id = $1
)
SELECT en.id, en.name, en.status, en.last_seen_at,
en.last_sync_version::text AS last_sync_version, en.created_at,
sc.last_cursor::text AS last_cursor, sc.last_sync_at,
hw.high_water_version::text AS high_water_version,
(hw.high_water_version - COALESCE(sc.last_cursor, 0))::text AS lag
FROM edge_nodes en
LEFT JOIN sync_cursors sc
ON sc.tenant_id = en.tenant_id AND sc.edge_node_id = en.id
CROSS JOIN hw
WHERE en.tenant_id = $1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@control-plane/src/domains/edgenode/edgeNodeRepository.ts` around lines 23 -
35, Scope the hw CTE’s MAX(sync_version) calculation to the caller tenant by
filtering sync_events with the query’s tenant parameter before computing
high_water_version. Keep the outer edge-node and cursor joins unchanged, and add
an integration case where another tenant has a later sync_events row to verify
the reported lag and high-water mark remain tenant-specific.

Comment thread control-plane/src/lib/timeWindow.ts
Comment on lines +21 to +28
async handler(req, res) {
const principalId = req.params.principalId as string;
const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : 100;
const [current, items] = await Promise.all([
scoreRead.getCurrentScore(principalId),
scoreRead.getScoreHistory(principalId, limit),
]);
ok(res, { principal_id: principalId, current, items });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Enforce tenant scope for score and graph reads.

The PR objective requires tenant-scoped score and graph reads. These handlers authenticate the caller but do not use req.user.tenantId.

Any authenticated tenant can request a known principalId score history. Any authenticated tenant can also retrieve global principal counts, issuer activity, and score-write metadata.

  • control-plane/src/routes/scores.ts#L21-L28: require a tenant ID and pass it to repository methods that authorize the principal before returning its score data.
  • control-plane/src/routes/graph.ts#L20-L22: require a tenant ID and pass it to a tenant-scoped graph-summary query.
  • Add integration tests that use tenant B credentials against tenant A score and graph data.
📍 Affects 2 files
  • control-plane/src/routes/scores.ts#L21-L28 (this comment)
  • control-plane/src/routes/graph.ts#L20-L22
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@control-plane/src/routes/scores.ts` around lines 21 - 28, Enforce tenant
scoping in both handlers: in scores.ts, require req.user.tenantId and pass it to
the scoreRead methods so principal authorization occurs before returning score
data; in graph.ts, require the tenant ID and use the tenant-scoped graph-summary
query. Add integration tests using tenant B credentials to verify tenant A’s
score and graph data, including global counts, issuer activity, and score-write
metadata, cannot be read. Affected sites: control-plane/src/routes/scores.ts
lines 21-28 and control-plane/src/routes/graph.ts lines 20-22.

Comment thread dashboard/src/pages/ProviderHomePage.tsx Outdated
Comment thread dashboard/src/styles/index.css
- timeWindow: reject Date-parseable but non-ISO 8601 forms (e.g. 01/02/2024)
  to avoid ambiguous from/to; accept date-only + zoned timestamps. (+tests)
- ProviderHomePage: embed active tenant id in query keys so a tenant switch
  partitions the cache and refetches instead of showing stale data.
  (providerQueryKeys helper + tests)
- badge--passthrough: darken text to #6b5d23 (5.4:1 on #f0ead2) for WCAG AA.

Skipped (replied inline):
- edge-node high-water tenant scoping: sync_version is a global monotonic
  counter (global advisory-lock allocator); matches merged listSyncLag (#16).
- scores/graph tenant scoping: trust graph + network scores are global/shared
  by Plan 9 Decision 5; any authenticated tenant member may read them.
@messagesgoel-blip

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

Addressed the actionable findings in 8681675:

  • timeWindow ISO validation — now rejects Date-parseable but non-ISO 8601 forms (01/02/2024, January 2, 2024, 2024/01/02) while accepting date-only and zoned timestamps; +2 tests.
  • ProviderHomePage query keys — active tenant id is now embedded in every query key (providerQueryKeys helper) so a tenant switch partitions the cache and refetches; +3 tests.
  • badge--passthrough contrast — text darkened to #6b5d23 (5.4:1 on #f0ead2, WCAG AA).

Two findings intentionally skipped, with rationale:

  • edge-node high-water tenant scopingsync_events.sync_version is a global monotonic counter (allocated under the global pg_advisory_xact_lock(8392018)), so an edge's last_cursor is a position in the global stream and lag must be global_high_water - edge_cursor. This mirrors the merged listSyncLag (Plan 7 PR B, feat: Plan 7 PR B — sync_cursors, lag endpoint, SSE integration #16) used by /v1/admin/sync/lag. Tenant-scoping the high water would diverge from that production behavior and understate lag, so I kept parity rather than change read semantics in a dashboard PR.
  • scores/graph tenant scoping — by Plan 9 Decision 5 the trust graph is shared: "any authenticated tenant member" may read /v1/graph/summary, and network_scores is a global materialized view (no tenant_id). Per-principal score history is likewise a global trust fact (the agent-builder view in PR C reads it for observed principals, not only owned ones). Tenant isolation is enforced on the tenant-owned surfaces (decision_*, policies, edge-nodes); the global graph/scores are intentionally shared. Adding tenant scoping here would break the locked design.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Changed files in the follow-up commit =='
git show --stat --oneline 8681675

echo
echo '== Relevant symbols and changed regions =='
for file in \
  control-plane/src/lib/timeWindow.ts \
  control-plane/src/lib/timeWindow.test.ts \
  dashboard/src/pages/ProviderHomePage.tsx \
  dashboard/src/styles/index.css \
  control-plane/src/domains/edgenode/edgeNodeRepository.ts \
  control-plane/src/domains/score/scoreReadRepository.ts
do
  if [ -f "$file" ]; then
    echo "--- $file ---"
    ast-grep outline "$file" --items all 2>/dev/null || true
  fi
done

echo
echo '== Follow-up diff =='
git show --format=fuller --find-renames 8681675 -- \
  control-plane/src/lib/timeWindow.ts \
  control-plane/src/lib/timeWindow.test.ts \
  dashboard/src/pages/ProviderHomePage.tsx \
  dashboard/src/styles/index.css \
  control-plane/src/domains/edgenode/edgeNodeRepository.ts \
  control-plane/src/domains/score/scoreReadRepository.ts

echo
echo '== Global sync counter and existing admin lag implementation =='
rg -n -C 4 --glob '*.{ts,sql}' \
  'pg_advisory_xact_lock\(8392018\)|sync_version|listSyncLag|global_high_water|high.water' \
  control-plane

echo
echo '== Plan 9 Decision 5 references =='
rg -n -C 3 --glob '*.{md,ts}' \
  'Decision 5|shared.*trust graph|network_scores|graph/summary|score history' \
  docs control-plane 2>/dev/null || true

echo
echo '== Provider query-key tests =='
rg -n -C 5 --glob '*.{test,spec}.{ts,tsx}' \
  'providerQueryKeys|ProviderHomePage|tenant' \
  dashboard

Length of output: 108605


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Query keys and active tenant source =='
for file in dashboard/src/lib/queryKeys.ts dashboard/src/auth/AuthProvider.tsx dashboard/src/api/provider.ts; do
  if [ -f "$file" ]; then
    echo "--- $file ---"
    cat -n "$file"
  fi
done

echo
echo '== Tenant header construction and provider endpoint usage =='
rg -n -C 4 --glob '*.{ts,tsx}' \
  'X-Tenant-Id|tenantId|fetchGraphSummary|fetchAggregates|fetchAgents|fetchSamples|fetchEdgeNodes|fetchActivePolicy' \
  dashboard/src

echo
echo '== Full PR diff against merge base =='
base="$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main)"
git diff --check "$base"...HEAD
git diff --stat "$base"...HEAD
git diff --find-renames "$base"...HEAD -- \
  control-plane/src \
  dashboard/src \
  dashboard/package.json \
  control-plane/package.json

echo
echo '== Offset-free date-time regression candidates =='
rg -n -C 4 --glob '*.{ts,tsx}' \
  'resolveTimeWindow\(|ISO_8601_RE|T\d{2}:\d{2}(?!.*(?:Z|[+-]\d{2}:?\d{2}))' \
  control-plane/src

Length of output: 21521


@messagesgoel-blip I verified the follow-up changes.

The provider query keys now partition the cache by active tenant. The passthrough badge color now meets the stated contrast target.

I withdraw the edge-node high-water and graph/score scoping findings. The implementation matches the global sync stream and shared-graph design.

One issue remains in control-plane/src/lib/timeWindow.ts, Line 11. The timezone suffix is optional in ISO_8601_RE. The API still accepts offset-free date-time values such as 2026-08-01T12:00. JavaScript parses these values in the server-local timezone. Require Z or a numeric offset whenever a time component is present. Keep date-only values valid. Add a regression test for an offset-free date-time value.


✏️ Learnings added
Learnt from: messagesgoel-blip
URL: https://github.com/Numeracode/verilink/pull/27

Timestamp: 2026-08-07T03:37:58.572Z
Learning: In the Verilink control plane, `sync_events.sync_version` is a global monotonic stream position allocated with the transaction-scoped advisory lock key `8392018`. Per-edge synchronization lag must use the global `MAX(sync_events.sync_version)` minus the edge cursor, even when the edge list is tenant-scoped. This matches `control-plane/src/domains/sync/syncCursorRepository.ts` `listSyncLag`.

Learnt from: messagesgoel-blip
URL: https://github.com/Numeracode/verilink/pull/27

Timestamp: 2026-08-07T03:37:58.572Z
Learning: For Plan 9 provider reads, the trust graph and `network_scores` are global shared data. Any authenticated tenant member may read `GET /v1/graph/summary` and global per-principal score history. Tenant isolation applies to tenant-owned decision, policy, and edge-node data.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@qodo-code-review

qodo-code-review Bot commented Aug 7, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Action required

1. Tenant cache key leak ✓ Resolved 🐞 Bug ≡ Correctness
Description
ProviderHomePage uses tenant-independent React Query queryKeys, so changing the active tenant
(X-Tenant-Id) can keep rendering cached results from the previous tenant until an explicit
refetch/invalidation occurs. This can show the wrong tenant’s aggregates/agents/samples/edges/policy
in the Provider view.
Code

dashboard/src/pages/ProviderHomePage.tsx[R41-44]

+  const summaryQuery = useQuery({ queryKey: ['graph-summary'], queryFn: fetchGraphSummary });
+  const aggregatesQuery = useQuery({
+    queryKey: ['decision-aggregates'],
+    queryFn: () => fetchAggregates('all'),
Relevance

●●● Strong

Tenant isolation issues are treated seriously; include tenantId in React Query keys to prevent
cross-tenant cache reuse.

PR-#24
PR-#16

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The app allows changing the active tenant id and sends it as X-Tenant-Id, but the Provider page’s
React Query keys do not include tenant id, so cached responses can be reused across tenant changes.

dashboard/src/pages/ProviderHomePage.tsx[40-50]
dashboard/src/layouts/AppShell.tsx[30-40]
dashboard/src/api/client.ts[16-33]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The Provider dashboard queries are cached under keys like `['decision-aggregates']` without including the active tenant id, so React Query reuses cached data when the user changes `X-Tenant-Id`.

### Issue Context
- The active tenant is editable in the shell and stored in sessionStorage.
- `apiFetch` attaches `X-Tenant-Id` from that stored value.

### Fix Focus Areas
- dashboard/src/pages/ProviderHomePage.tsx[41-49]
- dashboard/src/layouts/AppShell.tsx[30-40]
- dashboard/src/api/client.ts[16-33]

### Suggested fix
- Read `tenantId` via `useAuth()` in `ProviderHomePage`.
- Include `tenantId` in every tenant-scoped query key, e.g.:
 - `['decision-aggregates', tenantId]`
 - `['decision-agents', tenantId]`
 - `['decision-samples', tenantId]`
 - `['edge-nodes', tenantId]`
 - `['policy-active', tenantId]`
- Optionally set `enabled: Boolean(tenantId)` for tenant-required endpoints to avoid rendering cached data / repeated 403s when no tenant is selected.
- (Optional) Keep global queries like `graph-summary` unscoped if they’re truly global.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Limit parse/validate mismatch 🐞 Bug ≡ Correctness
Description
New routes validate limit using Number() (so inputs like 1e2 or 1.5 pass min/max checks) but
then parse with parseInt(), causing the executed limit to differ from the validated value. This
produces incorrect paging/limits and can lead to confusing API behavior for the new read endpoints.
Code

control-plane/src/routes/decisions.ts[R83-86]

+      const tenantId = requireTenant(req);
+      const { from, to } = resolveTimeWindow(req.query.from, req.query.to);
+      const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : 50;
+      const items = await decisionRead.getSamples({ tenantId, from, to, limit });
Relevance

●●● Strong

Deterministic correctness fix; team has accepted numeric parsing/validation hardening before.

PR-#15

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Validation uses Number(str) for numeric query params, while the new routes parse the same value
using parseInt(), which interprets some valid numeric strings differently (e.g., scientific
notation).

control-plane/src/shared/http/defineHandler.ts[20-39]
control-plane/src/routes/decisions.ts[72-86]
control-plane/src/routes/scores.ts[16-24]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`defineHandler` validates `type: 'number'` using `Number(str)`, but new handlers parse `limit` via `parseInt()`. Values like `limit=1e2` validate as 100 but execute as 1.

### Issue Context
This pattern appears in the new decision read routes and the new scores history route.

### Fix Focus Areas
- control-plane/src/shared/http/defineHandler.ts[20-39]
- control-plane/src/routes/decisions.ts[76-86]
- control-plane/src/routes/decisions.ts[99-110]
- control-plane/src/routes/scores.ts[18-28]

### Suggested fix
Pick one consistent semantic and apply it end-to-end:
- If `limit` must be an integer (recommended for SQL `LIMIT`):
 - In each handler, parse with a strict integer check:
   - `const raw = req.query.limit as string | undefined;`
   - `if (raw && !/^\d+$/.test(raw)) throw new AppError(CODES.BAD_REQUEST, 'limit must be an integer');`
   - `const limit = raw ? parseInt(raw, 10) : <default>;`
 - Keep the existing min/max constraints (or re-check after parsing).
- Alternatively, enhance `defineHandler` to support an explicit integer type (e.g., `type: 'int'`) and reuse it for `limit`/`offset` across routes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Read APIs lack indexes 🐞 Bug ➹ Performance
Description
The new decision read APIs filter decision_samples by tenant_id and decided_at (and group by
principal_id), but the schema only defines UNIQUE(edge_node_id, wal_seq); as data grows this
creates a high risk of expensive scans/sorts for the Provider dashboard reads. decision_aggregates
likewise lacks an index aligned to the new tenant+time+dimension query pattern (its unique index is
keyed by (tenant_id, edge_node_id, ...), but the read query does not constrain edge_node_id).
Code

control-plane/src/domains/decision/decisionReadRepository.ts[R59-62]

+     FROM decision_samples
+     WHERE tenant_id = $1
+       AND decided_at >= $2
+       AND decided_at <= $3
Relevance

●● Moderate

Likely needs new migrations/indexes; repo sometimes adds DB indexes but PR scope may avoid
migrations.

PR-#4

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new repository queries rely on tenant/time filtering and ordering, but the underlying tables
(per migrations) do not define indexes for those predicates, making slow query plans likely as the
tables grow.

control-plane/src/domains/decision/decisionReadRepository.ts[14-33]
control-plane/src/domains/decision/decisionReadRepository.ts[49-68]
control-plane/src/domains/decision/decisionReadRepository.ts[82-110]
control-plane/migrations/006_audit/migration.sql[23-53]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
New read queries over `decision_samples`/`decision_aggregates` are tenant+time bounded and ordered, but there are no supporting indexes for those access patterns in the current schema.

### Issue Context
- `getSamples` reads by `tenant_id` + `decided_at` range and orders by `decided_at DESC`.
- `listAgents` reads by `tenant_id` + `decided_at` range and groups by `principal_id`.
- `getAggregates` reads by `tenant_id` + `bucket_minute` range + `dimension_kind`, but the existing unique index is keyed by `(tenant_id, edge_node_id, ...)` and the query does not filter by `edge_node_id`.

### Fix Focus Areas
- control-plane/src/domains/decision/decisionReadRepository.ts[14-33]
- control-plane/src/domains/decision/decisionReadRepository.ts[49-68]
- control-plane/src/domains/decision/decisionReadRepository.ts[82-112]
- control-plane/migrations/006_audit/migration.sql[23-53]

### Suggested fix
Add migrations to create indexes aligned with the new reads (validate with `EXPLAIN` on representative data):
- For samples feed:
 - `CREATE INDEX ... ON decision_samples (tenant_id, decided_at DESC, id DESC);`
- For agents aggregation (optional depending on plan):
 - `CREATE INDEX ... ON decision_samples (tenant_id, decided_at, principal_id);`
- For aggregates rollups:
 - `CREATE INDEX ... ON decision_aggregates (tenant_id, dimension_kind, bucket_minute);`
Consider `CONCURRENTLY` if applied to a live DB, and ensure indexes don’t unduly impact ingest write throughput.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

4. Hard-coded DB creds and secret 📘 Rule violation ⛨ Security
Description
The new integration test hard-codes a Postgres connection string containing credentials and sets
API_KEY_HMAC_SECRET to a literal value, which violates the requirement to keep secrets/credentials
out of source. This can trigger secret scanners (gitleaks) and increases risk of accidental reuse
outside test contexts.
Code

control-plane/src/tests/integration/dashboard-reads.test.ts[R1-3]

+process.env.DATABASE_URL ||=
+  'postgresql://verilink:********@127.0.0.1:15432/verilink_test';
+process.env.API_KEY_HMAC_SECRET ||= 'test-hmac-secret-for-integration';
Relevance

● Weak

Same hardcoded test DATABASE_URL/API_KEY_HMAC_SECRET removal was previously rejected despite
gitleaks concerns.

PR-#21
PR-#16

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2373972 requires that source code contain no secrets/credentials detectable by
gitleaks. The added lines set process.env.DATABASE_URL to a DSN embedding a username/password and
set process.env.API_KEY_HMAC_SECRET to a literal secret value.

Rule 2373972: Source code must contain no secrets or credentials as detected by gitleaks
control-plane/src/tests/integration/dashboard-reads.test.ts[1-3]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The integration test hard-codes credentials (`postgresql://verilink:********@...`) and a static `API_KEY_HMAC_SECRET` value in source.

## Issue Context
Compliance requires that source code contains no secrets/credentials as detected by gitleaks; even test-only credentials should be provided via environment/config (CI, local `.env`, or test harness setup) rather than committed as literals.

## Fix Focus Areas
- control-plane/src/__tests__/integration/dashboard-reads.test.ts[1-3]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context used
✅ Compliance rules (platform): 18 rules

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread dashboard/src/pages/ProviderHomePage.tsx Outdated
Comment on lines +83 to +86
const tenantId = requireTenant(req);
const { from, to } = resolveTimeWindow(req.query.from, req.query.to);
const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : 50;
const items = await decisionRead.getSamples({ tenantId, from, to, limit });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Limit parse/validate mismatch 🐞 Bug ≡ Correctness

New routes validate limit using Number() (so inputs like 1e2 or 1.5 pass min/max checks) but
then parse with parseInt(), causing the executed limit to differ from the validated value. This
produces incorrect paging/limits and can lead to confusing API behavior for the new read endpoints.
Agent Prompt
### Issue description
`defineHandler` validates `type: 'number'` using `Number(str)`, but new handlers parse `limit` via `parseInt()`. Values like `limit=1e2` validate as 100 but execute as 1.

### Issue Context
This pattern appears in the new decision read routes and the new scores history route.

### Fix Focus Areas
- control-plane/src/shared/http/defineHandler.ts[20-39]
- control-plane/src/routes/decisions.ts[76-86]
- control-plane/src/routes/decisions.ts[99-110]
- control-plane/src/routes/scores.ts[18-28]

### Suggested fix
Pick one consistent semantic and apply it end-to-end:
- If `limit` must be an integer (recommended for SQL `LIMIT`):
  - In each handler, parse with a strict integer check:
    - `const raw = req.query.limit as string | undefined;`
    - `if (raw && !/^\d+$/.test(raw)) throw new AppError(CODES.BAD_REQUEST, 'limit must be an integer');`
    - `const limit = raw ? parseInt(raw, 10) : <default>;`
  - Keep the existing min/max constraints (or re-check after parsing).
- Alternatively, enhance `defineHandler` to support an explicit integer type (e.g., `type: 'int'`) and reuse it for `limit`/`offset` across routes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +59 to +62
FROM decision_samples
WHERE tenant_id = $1
AND decided_at >= $2
AND decided_at <= $3

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

4. Read apis lack indexes 🐞 Bug ➹ Performance

The new decision read APIs filter decision_samples by tenant_id and decided_at (and group by
principal_id), but the schema only defines UNIQUE(edge_node_id, wal_seq); as data grows this
creates a high risk of expensive scans/sorts for the Provider dashboard reads. decision_aggregates
likewise lacks an index aligned to the new tenant+time+dimension query pattern (its unique index is
keyed by (tenant_id, edge_node_id, ...), but the read query does not constrain edge_node_id).
Agent Prompt
### Issue description
New read queries over `decision_samples`/`decision_aggregates` are tenant+time bounded and ordered, but there are no supporting indexes for those access patterns in the current schema.

### Issue Context
- `getSamples` reads by `tenant_id` + `decided_at` range and orders by `decided_at DESC`.
- `listAgents` reads by `tenant_id` + `decided_at` range and groups by `principal_id`.
- `getAggregates` reads by `tenant_id` + `bucket_minute` range + `dimension_kind`, but the existing unique index is keyed by `(tenant_id, edge_node_id, ...)` and the query does not filter by `edge_node_id`.

### Fix Focus Areas
- control-plane/src/domains/decision/decisionReadRepository.ts[14-33]
- control-plane/src/domains/decision/decisionReadRepository.ts[49-68]
- control-plane/src/domains/decision/decisionReadRepository.ts[82-112]
- control-plane/migrations/006_audit/migration.sql[23-53]

### Suggested fix
Add migrations to create indexes aligned with the new reads (validate with `EXPLAIN` on representative data):
- For samples feed:
  - `CREATE INDEX ... ON decision_samples (tenant_id, decided_at DESC, id DESC);`
- For agents aggregation (optional depending on plan):
  - `CREATE INDEX ... ON decision_samples (tenant_id, decided_at, principal_id);`
- For aggregates rollups:
  - `CREATE INDEX ... ON decision_aggregates (tenant_id, dimension_kind, bucket_minute);`
Consider `CONCURRENTLY` if applied to a live DB, and ensure indexes don’t unduly impact ingest write throughput.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@messagesgoel-blip
messagesgoel-blip merged commit 842bf86 into main Aug 7, 2026
6 checks passed
@messagesgoel-blip
messagesgoel-blip deleted the feat/dashboard-pr-b branch August 7, 2026 03:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant