Files
zeekayandHanzo Dev 456b5a860e feat: fold the dashboards service in — one Langfuse surface, one analytics shim
hanzoai/dashboards shipped as its own binary (msgType 206) serving five console
tRPC routers: dashboards, widgets, table batch-actions, view presets, monitors.
It is now this service's 200+ ordinal range and its dash_*.go files.

They were never two concerns. A dashboard is a saved QUERY over exactly the
traces, observations and scores this service owns, and both halves were blocked
on the SAME ClickHouse shim — kept apart, that shim had to be written twice
against two copies of one OLTP model, and chart/scoreHistogram could not reach
the scores they aggregate without a network hop. The duplication was already
visible in the capability bits: both services had independently minted a
PermAnalyticsRead. Merged, there is one.

Ordinals are the standalone binary's 0–29 offset by 200, not renumbered into the
gaps: the offset is mechanical and reversible, renumbering would silently
repoint a client built against the old ids.

Three conventions were doubled and are now single:

- TENANCY. authorize() answered both "may this caller invoke this method" and
  "whose rows may it touch", returning a defaultOrg the handlers ignored while
  each re-read projectId from its own params. Split: authorize() is the cap gate,
  scope() is the ONE tenant resolver, and its project is passed to every handler.
  A pipelined call now INHERITS its target's project instead of re-reading the
  payload, so pipelining cannot be used to hop tenants. Server.defaultOrg was
  dead once scope() existed and is deleted rather than left as unread state on a
  security-relevant struct.

- NOT-IMPLEMENTED. Observability answered stubs 200 + Empty{Stubbed:true};
  dashboards answered 501. Unified on 501 through one Server.stub helper. A
  stubbed 200 carrying an empty result is indistinguishable at the call site from
  a real query that matched nothing — the UI renders "no data" and the missing
  backend never surfaces. The status is the one field a caller cannot skip
  reading; a flag in the body is one it can, and eventually does.

- COLUMN NAMES. fDashDescription, fWidgetDescription and fDescription were three
  constants for the column "description". One constant per column name now.

scope() reads the payload's field at text @0 without knowing its type, which is
silently wrong for any request struct that puts another text field first — and
wrong by reading ANOTHER TENANT'S rows, not by failing. That assumption is now
pinned by TestEveryRequestStructCarriesProjectIdAtZero over the schema itself
(43 declarations, 9 result structs exempted). Verified it fails when the
invariant is broken, not just that it passes.

Verified: 22 tests green (14 obs + 8 dashboards, bodies ported byte-identical so
they diff against the originals) plus the invariant; go vet clean; make zap-gen
byte-identical on regen; CGO_ENABLED=0 build ok.

go.sum carried the same moved-tag corruption fixed in hanzoai/dashboards
(age/pq/zap hashes restored to the checksum DB; threshold v1.9.4→v1.9.5 and
orm v0.6.14→v0.6.16, both unbuildable from a clean cache).

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 13:32:12 -07:00
..