Views¶
Matrix View Layout¶
Grid Structure¶
.matrix {
display: grid;
grid-template-columns: 180px repeat(N, minmax(140px, max-content));
column-gap: 6px;
row-gap: 9px; /* clear visual separation between service rows */
}
- First column (180px): Service name, sticky-left during horizontal scroll (
position: sticky; left: 0). Background:--glass-strong+backdrop-filterso content doesn't show through. - Env columns:
minmax(140px, max-content)β each column expands to fit widest cell. Columns consume available viewport width when content allows. - Service-row accent:
border-left: 2px solid rgba(var(--accent-rgb), 0.35)on.row-head. - Matrix shell:
overflow-x: auto; overflow-y: visible. Bottom hairline:border-bottom: 1px solid var(--glass-edge-2)closes the container visually. - Column headers: environment tag in
.env-tagmonospace pill.
Filtering¶
- Service filter: inline
pInputTextin topbar. Case-insensitive substring match against component name. Matching toggles.is-hiddenon.rowelements. - Failures-only: inline
p-toggleSwitchpill. When ON, hides service rows that have no failed states (fail-last, run-fail-last, run-fail-only).
Column Controls¶
Show / Hide¶
- The
βColumns button (Matrix-only topbar icon) opens a popover listing every environment with a checkbox. - Unchecking an environment fully removes that column β header cell and all body cells β and the matrix re-renders from the visible, ordered environments. No placeholder or empty column remains.
- The last visible environment cannot be unchecked (click is blocked).
- "Show all Β· reset order" (popover footer action) restores all environments and the default column order in one step.
Drag Reorder¶
- Visible column headers (
.col-head) carry aβ Ώgrip glyph (.col-drag-grip), aredraggable="true", and havecursor: grab/ tooltip"Drag to reorder". - Dragging uses the HTML5 Drag and Drop API (
dragstart/dragover/dropevents on.col-headelements). - The dragged column fades to 40% opacity; the drop target gains an accent dashed outline.
- On drop: the column order updates, the new order is persisted, the matrix re-renders, and the picker reflects the new order.
Persistence¶
Column state persists client-side to localStorage. Both keys are cleared by "Show all Β· reset order".
| Key | Format | Content |
|---|---|---|
dd:colOrder |
JSON array | Ordered permutation of all environment names. |
dd:colHidden |
Comma-separated string | Names of hidden environments. Empty string = none hidden. |
On load: the persisted order is validated as a full permutation of the known environment set; if stale (environment added or removed) it falls back to the default environment order. Hidden-set entries not in the known environment set are ignored.
Swimlanes View Layout¶
Overall Structure¶
Canvas & Lanes¶
- One horizontal lane per service. Each lane has a header (
.lane-head) and a content area containing a dedicated<ngx-graph>instance. - One
<ngx-graph>per service β since no cross-service edges exist (each lane is self-contained), each service gets its own graph component. This keeps data binding simple. - Multiple disconnected DAGs within a lane stack vertically (top-to-bottom), not horizontally. Dagre handles disconnected subgraphs natively.
- Time axis: left-to-right within each DAG, based on
happened_at.
ngx-graph Configuration¶
<ngx-graph
[nodes]="laneNodes" <!-- Node[] for this service -->
[links]="laneLinks" <!-- Edge[] derived from parrent_deployments -->
[view]="[canvasWidth, laneHeight]"
layout="dagre"
[layoutSettings]="{
orientation: 'LR', // left-to-right (time axis)
rankPadding: 60, // gap between rank columns
nodePadding: 12, // vertical gap between parallel nodes
edgePadding: 8,
multigraph: true
}"
[curve]="curveBundle" // d3 curve for smooth edges
[autoZoom]="false"
[panningEnabled]="false" // outer .vis-canvas scrolls instead
[zoomEnabled]="false"
(select)="onNodeSelect($event)"
>
<ng-template #nodeTemplate let-node> ... </ng-template>
<ng-template #linkTemplate let-link> ... </ng-template>
<ng-template #defsTemplate> ... </ng-template>
</ngx-graph>
Data Mapping¶
| ngx-graph Input | Source | Mapping |
|---|---|---|
[nodes] |
Deployment events for this service | { id: event.id, label: event.version, data: { ...event } }. Attach the full domain-model record in data so the node template can render all fields. |
[links] |
parrent_deployments |
For each node, emit one link per parent: { id: nodeId+parentId, source: parentId, target: nodeId, data: { status: parentNode.status } }. Only intra-service parents produce links. |
[view] |
Computed | [availableWidth, computedLaneHeight]. Re-evaluate on attribute toggle and window resize. |
Custom Node Template (#nodeTemplate)¶
The #nodeTemplate receives let-node with node.data containing the full deployment record. Render the vis-card layout (see Β§ Swimlane Node Card) as an SVG <foreignObject> wrapping an HTML .vis-card div β this preserves the glass aesthetic, backdrop-filter, and the 2-column CSS grid layout.
<ng-template #nodeTemplate let-node>
<svg:foreignObject
[attr.width]="node.dimension.width"
[attr.height]="node.dimension.height">
<xhtml:div class="vis-card"
[class.s-success]="node.data.currentStatus === 'success'"
[class.s-progress]="node.data.currentStatus === 'in-progress'"
[class.s-failure]="node.data.currentStatus === 'failure'"
[class.s-never-deployed]="node.data.neverDeployed"
[class.is-selected]="node.data.id === selectedNodeId">
<!-- never-deployed: render neutral surface + status chip (hue from node.data.status) -->
<!-- ctx-badge overlay (.ctx-row): present when node.data.nextStatus is set -->
<!-- vis-card internal structure per Β§ Swimlane Node Card -->
</xhtml:div>
</svg:foreignObject>
</ng-template>
Node sizing: ngx-graph uses
node.dimension.width/node.dimension.heightfor layout. Pre-calculate these from the node's visible fields (or render once hidden, measure, then update). When attribute toggles change visible fields, update dimensions and callgraph.update$.next(true)to trigger relayout.
Custom Link Template (#linkTemplate)¶
Status-colored edges with arrow markers. The link's data.status determines stroke color.
<ng-template #linkTemplate let-link>
<svg:g class="edge">
<svg:path
[attr.d]="link.line"
[attr.stroke]="getEdgeColor(link.data.status)"
stroke-width="1.5"
fill="none"
marker-end="url(#arrow)" />
</svg:g>
</ng-template>
<ng-template #defsTemplate>
<svg:marker id="arrow" viewBox="0 -5 10 10"
refX="8" refY="0" markerWidth="4" markerHeight="4"
orient="auto">
<svg:path d="M0,-5L10,0L0,5" fill="currentColor" />
</svg:marker>
</ng-template>
Status Colour Map¶
Tile / card colour (the 3 effective statuses β drive box colour, edge stroke, and card class):
| Status | Hue | Token | Icon |
|---|---|---|---|
success |
emerald | --emerald |
β |
in-progress |
amber | --amber |
spinner β΄ |
failure |
coral | --coral |
β |
Next-deployment badge (the 5 non-effective statuses β rendered as .ctx-badge layered on the tile/card; never drive box colour):
| Status | Hue | Token | Icon | Description |
|---|---|---|---|---|
pending |
slate | --slate |
β |
created, not started |
queued |
blue | --blue |
β‘ |
queued to run |
waiting |
violet | --violet |
β· |
blocked on approval / wait timer |
cancelled |
grey | --grey |
β |
run cancelled |
rejected |
rose | --rose |
β |
reviewer denied β never ran |
The next badge shows the latest deployment beyond the live one (if any). It is present on both Matrix tiles and Swimlane cards.
Legend. Each view (Matrix / Swimlanes) carries its own legend popover (#legend-matrix / #legend-vis), swapped on view change. Three sections:
- Status key β "Environment state" (3 effective) + "Next deployment" (5 context): icon + swatch + meaning.
- Field reference β each visible field rendered AS IT APPEARS + its meaning (matrix MATRIX_FIELDS / swimlane SWIMLANE_FIELDS).
- Layout guide β Matrix: tile layouts (split / prev. failed / never-deployed / empty). Swimlanes: edges = parentβchild + the correlation predicate.
Inspector. The inspector panel shows the effective deployment's fields first, then a dotted separator, then a next group for the next-deployment entry (if present). The history drawer shows all 8 statuses as distinct entries, with the next deployment leading.
Edge Color Mapping¶
Edges carry the parent node's effective status. All 8 status values map to a stroke colour (next-status nodes that appear in the history DAG use the same hue table):
| Parent Status | Stroke Color (dark) | Token |
|---|---|---|
success |
emerald | --emerald |
in-progress |
amber | --amber |
failure |
coral | --coral |
pending |
slate | --slate |
queued |
blue | --blue |
waiting |
violet | --violet |
cancelled |
grey | --grey |
rejected |
rose | --rose |
Layout Constraints¶
- DAG edges must never cross node bounding boxes β dagre's rank-based layout satisfies this for standard graphs; validate with dense fixtures.
- Per-rank column spacing = the maximum card width in that rank plus the
rankPaddinggap. - Canvas scrolls horizontally via
.vis-canvas { overflow-x: auto }when graph width exceeds viewport. - On attribute toggle β recalculate node dimensions β
graph.update$.next(true)to relayout. - Lanes pack densely with minimal inter-lane gap (~8px margin between stacked
<ngx-graph>instances).
Collapse / Expand (#309)¶
Default state. All lanes start collapsed (persisted to localStorage).
Controls (Swimlanes view only): - Chevron (βΊ) per lane β toggles that lane between collapsed and expanded. - Collapse/Expand all button β flips every lane simultaneously. - Auto-scroll to change toggle (default ON) β when any lane (collapsed or expanded) receives a simulated event and is off-screen, scrolls it into view.
Collapsed form β vector.
- A collapsed lane renders via the identical card + SVG-edge machinery as the expanded form, restricted to the service's newest-event vector (rootβtip chain). No separate renderer, no flex overlay, no custom arrow connectors.
- Algorithm: find tip = node with max happened_at; walk backward through parrent_deployments; at a merge (multiple parents) follow the parent with the newest happened_at; stop at root. Feed the chain into the standard partitionDAGs β rankNodes β assignTracks β position β card β SVG-edge pipeline.
- Only intra-chain parrent_deployments produce edges β no dangling arrowheads, no cross-lane edges.
- If tip is isolated (no parents), the vector is a single card with no edges.
- Lane height is computed naturally from the chain's single-track layout (same packing formula as expanded). A pure single-chain lane is pixel-identical collapsed vs expanded.
Expanded form. Full multi-DAG rendering, unchanged from the base Swimlanes spec.
Interactions:
- State persists across reloads and Matrix β Swimlanes switches.
- "Simulate event" button appears on every lane (collapsed and expanded). It mutates the tip node's status β rebuilds via the unified renderer β flashes the tip card (collapsed = vector tip; expanded = newest-event DAG node; both located in #cards-layer by data-node-id) β auto-scrolls if the lane is off-screen and auto-scroll is ON.
Analytics View Layout¶
The Analytics view is the 3rd tab in the top-nav segmented control. All data is server-side computed β the SPA fetches, renders, never aggregates client-side.
Shell Structure¶
.an-shell {
/* full-width scrollable column */
}
.an-header {
/* title block + period selector, flex row */
}
.an-kpi-band {
display: grid;
grid-template-columns: repeat(4, 1fr);
/* responsive: repeat(2, 1fr) below the tablet breakpoint */
}
.an-grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: <token>;
}
Period Selector¶
- Three buttons: 7d, 14d, 30d. Single active state (
.is-active). - Sends
?window=7d|14d|30dto every/api/analytics/*call. - When
window.clamped === truein any response, surface the clamp indication next to the active button (e.g. subtitle reads "14 days Β· bounded by HISTORY_RETENTION_DAYS"). - Default selection: 14d. The API
windowquery param itself defaults to 7d when omitted, but the SPA always sends an explicit value β no contradiction with openapi.yaml / api-guidelines Β§12.
DORA KPI Band¶
Four .an-kpi-card tiles in .an-kpi-band, one per DORA key:
| Card | Key | Unit | approximated |
|---|---|---|---|
| Deployment Frequency | deployment_frequency |
per day | false |
| Lead Time for Changes | lead_time |
hours | true |
| Change Failure Rate | change_failure_rate |
ratio (0β1) | false |
| Time to Restore | time_to_restore |
minutes | false |
Each card:
- .an-kpi-label β key name.
- .an-kpi-value β formatted value + unit.
- .an-class-chip β DORA performance band (elite / high / medium / low); colour-coded (emerald / blue / amber / coral).
- .an-trend-chip β signed trend_delta vs prior half-window; .good (emerald) / .bad (coral) / .flat (neutral). Direction semantics: up is good for frequency; up is bad for CFR, lead-time, and MTTR.
- .an-kpi-sparkline β inline SVG spark-line over the sparkline[] per-day series.
- When approximated: true (lead time only): render a visible approximation label; never present the value as measured commitβprod lead time.
8-Chart Grid¶
The .an-grid is a 12-column grid. Each .an-card carries one chart. Layout row-by-row:
| # | Chart | CSS span | .an-chart-body height modifier |
API endpoint |
|---|---|---|---|---|
| 1 | Deployment frequency over time | .an-span-8 |
.an-h-freq (185 px) |
GET /api/analytics/frequency |
| 5 | Status distribution (donut) | .an-span-4 |
.an-h-donut (auto) |
GET /api/analytics/status-distribution |
| 2 | Change failure rate trend | .an-span-6 |
default | GET /api/analytics/change-failure-rate |
| 3 | Deployment duration distribution | .an-span-6 |
default | GET /api/analytics/duration-histogram |
| 4 | Promotion funnel | .an-span-4 |
.an-h-funnel (175 px) |
GET /api/analytics/promotion-funnel |
| 6 | Deploy heatmap | .an-span-8 |
.an-h-heat (175 px) |
GET /api/analytics/heatmap |
| 8 | Top deployers | .an-span-6 |
.an-h-actors (210 px) |
GET /api/analytics/top-deployers |
| 7 | Time to restore β recent incidents | .an-span-6 |
.an-h-mttr (200 px) |
GET /api/analytics/incidents |
Span values β an-span-12 / an-span-8 / an-span-6 / an-span-4 map to grid-column: span 12/8/6/4 respectively. Below the tablet breakpoint all spans collapse to span 12.
Per-chart notes¶
- Frequency (
span-8). Stacked bar chart βsuccess(emerald) andfailure(coral) per UTC day. X-axis = dates, Y-axis = count. - Status distribution (
span-4). Donut chart (.an-donut-wrap): SVG donut +.an-donut-legendgrid (dot / name / count / %). All 8 statuses always present (zero-filled β stable slice set). - CFR trend (
span-6). Line chart β dailyrateline + dashedelite_threshold(0.15) reference line. - Duration distribution (
span-6). Histogram bars over durationbins(minutes) +p50andp95reference markers. - Promotion funnel (
span-4). Funnel (sankey) β stages defined byANALYTICS_FUNNEL_ENVIRONMENTS(defaultdev,staging,qa,preprod,prod); each stage showscountandconversion. - Deploy heatmap (
span-8). 7 rows (day-of-week SunβSat) Γ 24 columns (UTC hour); cell intensity =count. Sparse β absent cells rendered as zero. - Top deployers (
span-6). Horizontal bar / leaderboard βactor+count, descending;limitdefault 10. - Metric definition.
count= distinct deployments that reachedSuccess, credited to the actor of the deployment's EARLIEST event (the trigger). Deployments that never reachedSuccess(cancelled, rejected, failure-only, etc.) are excluded. A null earliest-event actor is reported as"unknown". Bots are not filtered β if a bot triggered the deployment it is the legitimate deployer. - Time to restore (
span-6). Incident list β worst first (unresolved first, then longestduration_minutes); each row:service,environment, elapsed, severity chip (low/medium/high/critical). - Incident pairing semantics (one incident per outage). Consecutive
Failureevents within the same(service, environment)slot are coalesced into a single incident: the FIRST failure opens it (failed_at); further failures before recovery are ignored (same outage). The nextSuccesscloses it (restored_at= that success time; duration = first-failure β first-recovery). ASuccesswhile no incident is open is ignored. If no closingSuccessarrives within the window, the incident is emitted withrestored_at: null(genuinely unrecovered).
Extension View Layout¶
The browser extension presents four distinct surfaces (no persistent canvas β each surface is self-contained):
| Surface | Entry point | Container |
|---|---|---|
| Toolbar badge | Always visible | Browser toolbar icon |
| Deployment list popup | Click toolbar icon | Browser popup (~360px wide) |
| Notification toasts | Background SW on SSE event | Browser native notification |
| Config / options | Extension options page or popup settings tab | Full options page |
Popup Panel Layout¶
Stateless list β re-fetches GET /api/deployments on open and on storage change.
βββββββββββββββββββββββββββββββ
β Loadingβ¦ / Unconfigured / β
β Paused / (empty) β
β βββββββββββββββββββββββββ β
β [status-chip] Service β β row 1 (newest)
β Environment Β· Version β
β @actor Β· 3h ago / UTC β
β [Open run #NNN] β
β βββββββββββββββββββββββββ β
β [status-chip] Service β β row 2 β¦
β β¦ β
β βββββββββββββββββββββββββ β
β [Open dashboard] β
βββββββββββββββββββββββββββββββ
- Fixed width ~360px; height content-driven.
- Shows last N events (newest-first); N =
popupCount(default 5), configurable 1β50. - Filtered by service+environment watch filter and status filter.
- "Open dashboard" shown whenever a URL is configured.
- Uses the same glass-surface tokens as the SPA (
.glass-base, ink tokens, status palette).
Config Panel Layout¶
βββββββββββββββββββββββββββββββ
β Dashboard URL [________] β
β Watching [βββββββ] ON β
β βββββββββββββββββββββββββ β
β [Watch all except|Watch only] β
β Services [ ] svc-a [ ] svc-b β
β Environments [ ] prod [ ] staging β
β βββββββββββββββββββββββββ β
β Status filter β
β [ ] pending [ ] queued β¦ β
β βββββββββββββββββββββββββ β
β Show last N events [5] β
βββββββββββββββββββββββββββββββ
- Master Watching switch is prominent at top.
- Filter section (watch scope + status filter + popup count) is visually dimmed when switch is OFF.
- Mode segmented control + two checkbox lists (services, environments) + status checkboxes (8) + count picker (1β50).
- Persists all settings to extension storage.