Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ CLAUDE.md
# index.html is tracked so go:embed has at least one match on a
# fresh clone; `make frontend` overwrites it with the real bundle.
# See internal/ui/assets.go.
#
# DO NOT commit a real Vite build's index.html over the placeholder: the
# hashed assets/*.js and *.css it references are git-ignored, so a checkout
# would point at files that don't exist. Keep the DEVKIT_FRONTEND_PLACEHOLDER
# version tracked; let `make frontend` produce the real bundle at build time.
internal/ui/dist/*
!internal/ui/dist/index.html
.worktrees/
Expand Down
2 changes: 1 addition & 1 deletion assets/dashboard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func TestDashboardHasACSAndThroughputPanels(t *testing.T) {
t.Fatalf("parse dashboard: %v", err)
}
want := map[int]string{
14: "ACS Lookup Buffer Length",
14: "ACS Lookup Buffer",
15: "Top 10 gRPC Methods by Throughput (ops/s, 5m)",
}
got := map[int]string{}
Expand Down
26 changes: 13 additions & 13 deletions assets/grafana/dashboards/canton-localnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,15 @@
{
"id": 3,
"type": "stat",
"title": "Sequencer Submission Latency (p95)",
"title": "Sequencer Submission Latency (avg)",
"description": "Mean sequencing time (sum/count). Stock Splice 0.6.4 exports this histogram with only the +Inf bucket, so histogram_quantile percentiles are NaN — the mean is the reliable figure. Add p50/p95 panels on Splice versions whose histograms carry finite le buckets.",
"datasource": "Prometheus",
"gridPos": { "h": 6, "w": 6, "x": 12, "y": 0 },
"fieldConfig": { "defaults": { "unit": "s" } },
"targets": [
{
"expr": "histogram_quantile(0.95, sum(rate(daml_sequencer_client_submissions_sequencing_duration_seconds_bucket{instance=~\"$instance\"}[5m])) by (le))",
"legendFormat": "p95"
"expr": "sum(rate(daml_sequencer_client_submissions_sequencing_duration_seconds_sum{instance=~\"$instance\"}[5m])) / sum(rate(daml_sequencer_client_submissions_sequencing_duration_seconds_count{instance=~\"$instance\"}[5m]))",
"legendFormat": "avg"
}
]
},
Expand Down Expand Up @@ -110,25 +112,23 @@
{
"id": 13,
"type": "timeseries",
"title": "Submission Sequencing Latency",
"title": "Submission Sequencing Latency (avg)",
"description": "Mean sequencing time per component (sum/count). histogram_quantile percentiles are NaN on stock Splice 0.6.4 — its histogram carries only the +Inf bucket.",
"datasource": "Prometheus",
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 14 },
"fieldConfig": { "defaults": { "unit": "s" } },
"targets": [
{
"expr": "histogram_quantile(0.50, sum(rate(daml_sequencer_client_submissions_sequencing_duration_seconds_bucket{instance=~\"$instance\"}[5m])) by (le, component))",
"legendFormat": "p50 {{component}}"
},
{
"expr": "histogram_quantile(0.95, sum(rate(daml_sequencer_client_submissions_sequencing_duration_seconds_bucket{instance=~\"$instance\"}[5m])) by (le, component))",
"legendFormat": "p95 {{component}}"
"expr": "sum by (component) (rate(daml_sequencer_client_submissions_sequencing_duration_seconds_sum{instance=~\"$instance\"}[5m])) / sum by (component) (rate(daml_sequencer_client_submissions_sequencing_duration_seconds_count{instance=~\"$instance\"}[5m]))",
"legendFormat": "avg {{component}}"
}
]
},
{
"id": 14,
"type": "stat",
"title": "ACS Lookup Buffer Length",
"description": "ACS-related index lookup buffer length across participants. Stock Splice 0.6.4 does not expose total active-contract cardinality as a Prometheus metric; use the Web UI Explorer / JSON API ACS lookup for exact active contract counts.",
"title": "ACS Lookup Buffer",
"description": "Active-contracts index buffer size across participants. Stock Splice 0.6.4 does not expose total active-contract cardinality as a Prometheus metric; use the Web UI Explorer / JSON API ACS lookup for exact active contract counts.",
"datasource": "Prometheus",
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 22 },
"options": {
Expand All @@ -139,7 +139,7 @@
},
"targets": [
{
"expr": "sum(daml_participant_api_index_db_active_contract_lookup_batch_buffer_length{instance=~\"$instance\"})",
"expr": "sum(daml_participant_api_index_active_contracts_buffer_size{instance=~\"$instance\"})",
"legendFormat": "ACS lookup buffer"
}
]
Expand Down
6 changes: 3 additions & 3 deletions docs/dashboard-customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ non-existent `canton_*` names.
|---|---|---|---|
| Ledger TPS (5m avg) | stat | `sum(rate(daml_participant_api_indexer_updates{instance=~"$instance"}[5m])) or vector(0)` | Steady-state ledger throughput. Drops here usually point at participant or sequencer back-pressure. |
| Active Participants | stat | `count(up{component="canton", instance=~"$instance"} == 1)` | How many Canton nodes Prometheus can scrape right now. Anything less than expected means a node is unscrapeable. |
| Sequencer Submission Latency (p95) | stat | `histogram_quantile(0.95, sum(rate(daml_sequencer_client_submissions_sequencing_duration_seconds_bucket{instance=~"$instance"}[5m])) by (le))` | Tail latency from client submit to sequenced commit. This is the closest audited “command completion” latency on stock Splice 0.6.4. |
| Sequencer Submission Latency (avg) | stat | `sum(rate(daml_sequencer_client_submissions_sequencing_duration_seconds_sum{instance=~"$instance"}[5m])) / sum(rate(daml_sequencer_client_submissions_sequencing_duration_seconds_count{instance=~"$instance"}[5m]))` | Mean time from client submit to sequenced commit. Stock Splice 0.6.4 exports this histogram with only the `+Inf` bucket, so `histogram_quantile` percentiles are NaN — the mean (sum/count) is the reliable figure. |
| DB Connections (in use) | stat | `sum(db_client_connections_usage{state="used", instance=~"$instance"})` | Active DB pool usage across the stack. A creeping value here is the early signal for connection-pool pressure. |
| Transactions per Second | timeseries | `rate(daml_participant_api_indexer_updates{instance=~"$instance"}[1m]) or vector(0)` | Same signal as the TPS stat, broken out over time so you can see bursts and stalls. |
| JVM Heap Used (per node) | timeseries | `jvm_memory_used_bytes{jvm_memory_type="heap", instance=~"$instance"}` | Heap pressure per component. A sawtooth rising baseline is the classic memory-leak shape. |
| Sequencer Block Event Rate | timeseries | `rate(daml_sequencer_block_events_total{instance=~"$instance"}[1m])` | Sequencer-level event rate. Useful for separating ledger-layer slowness from transport-layer stalls. |
| Submission Sequencing Latency | timeseries | p50 + p95 of `daml_sequencer_client_submissions_sequencing_duration_seconds_bucket` grouped by `component` | Shows whether latency is isolated to one node or systemic. Diverging p50/p95 is the early sign of queueing or retries. |
| ACS Lookup Buffer Length | stat | `sum(daml_participant_api_index_db_active_contract_lookup_batch_buffer_length{instance=~"$instance"})` | ACS-related index lookup buffer length. Stock Splice 0.6.4 does not expose total active-contract cardinality as a Prometheus metric; use the Explorer / JSON API ACS lookup for exact counts. |
| Submission Sequencing Latency (avg) | timeseries | mean (sum/count) of `daml_sequencer_client_submissions_sequencing_duration_seconds` grouped by `component` | Mean sequencing time per component, so you can see whether latency is isolated to one node. Percentiles need finite histogram buckets, which stock Splice 0.6.4 does not provide. |
| ACS Lookup Buffer | stat | `sum(daml_participant_api_index_active_contracts_buffer_size{instance=~"$instance"})` | Active-contracts index buffer size. Stock Splice 0.6.4 does not expose total active-contract cardinality as a Prometheus metric; use the Explorer / JSON API ACS lookup for exact counts. |
| Top 10 gRPC Methods by Throughput (ops/s, 5m) | bar gauge | `topk(10, sum by (grpc_method_name) (rate(daml_grpc_server_handled_total{instance=~"$instance"}[5m])))` | API throughput by live gRPC method. Stock Splice 0.6.4 does not expose template-grain submission counters. |

For the full metric-family audit and substitution table, see
Expand Down
4 changes: 2 additions & 2 deletions frontend/index.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<!doctype html>
<html lang="en">
<html lang="en" data-theme="light">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="theme-color" content="#0B0F1A" />
<meta name="theme-color" content="#fbfbfc" />
<title>canton-devkit</title>
</head>
<body>
Expand Down
13 changes: 7 additions & 6 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { SCHEMA_VERSION, fetchVersion } from "./api";
import { Shell } from "./shell/Shell";
import { InstanceSelectionProvider } from "./shell/useInstanceSelection";
import { ErrorBoundary } from "./shell/ErrorBoundary";
import { ConfirmHost } from "./components/ConfirmDialog";
import { Dashboard } from "./screens/Dashboard";
import { DoctorScreen } from "./screens/DoctorScreen";
import { Placeholder } from "./screens/Placeholder";
Expand All @@ -15,9 +16,8 @@ import { AgentSkillsScreen } from "./screens/AgentSkillsScreen";
import { TokensScreen } from "./screens/TokensScreen";
import { W } from "./tokens";

// App boots with a schema-version handshake against the backend and
// renders the shell only on a match — a UI bundle must never silently
// mis-decode responses from a backend with a different schema.
// Boots with a schema-version handshake and renders the shell only on a
// match, so the bundle never mis-decodes a mismatched backend's responses.
export function App() {
const [status, setStatus] = useState<"loading" | "ready" | "mismatch" | "offline">(
"loading",
Expand Down Expand Up @@ -49,13 +49,14 @@ export function App() {
<Shell>
<RoutedSurface />
</Shell>
{/* One confirm-dialog host; confirmDialog() from anywhere resolves against it. */}
<ConfirmHost />
</InstanceSelectionProvider>
);
}

// RoutedSurface wraps each route element in its own ErrorBoundary,
// keyed by pathname, so a crash on one screen neither follows the
// user to the next route nor takes down the shell around it.
// Each route gets its own ErrorBoundary keyed by pathname, so a crash on
// one screen neither follows the user nor takes down the shell.
function RoutedSurface() {
const loc = useLocation();
return (
Expand Down
22 changes: 8 additions & 14 deletions frontend/src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
// The one button system for the Web UI (visuals in index.css under
// .bd-btn). Four variants with a strict usage contract:
//
// primary — THE one dominant action of a view or dialog (cobalt
// fill, ink text). At most one visible per context.
// secondary — the default: bordered, quiet (Refresh, Pause, Mint…).
// ghost — low-emphasis inline actions (Edit, close ×, chips).
// danger — destructive and irreversible only (Down, Burn,
// Scrub, force-restore). Filled red; use sparingly —
// recoverable actions like Stop/Pause stay secondary.
//
// Sizes: sm 28px (row/table actions — the console default) and
// md 36px (forms, dialog footers).
// Visuals in index.css under .bd-btn. Variant contract:
// primary — at most one dominant action per view/dialog.
// secondary — the default (bordered, quiet).
// ghost — low-emphasis inline actions.
// danger — destructive AND irreversible only; recoverable
// actions like Stop/Pause stay secondary.
// Sizes: sm 28px (default, row/table), md 36px (forms, dialog footers).

import type {
CSSProperties,
Expand All @@ -24,7 +18,7 @@ export type ButtonSize = "sm" | "md";
interface ButtonProps {
variant?: ButtonVariant;
size?: ButtonSize;
/** Icon slot — pass an icons.tsx glyph; it inherits text color. */
/** Icon slot — pass an icons.tsx glyph. */
icon?: ReactNode;
disabled?: boolean;
fullWidth?: boolean;
Expand Down
137 changes: 137 additions & 0 deletions frontend/src/components/ConfirmDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Promise-based confirm dialog:
// if (!(await confirmDialog({ title, body, confirmLabel, danger }))) return;
// A single ConfirmHost (mounted in App) handles the dispatched event.

import { useEffect, useState } from "react";
import { W, wMono, wSans, R, EASE, FAST } from "../tokens";
import { Button } from "./Button";

export interface ConfirmOptions {
title: string;
body: string;
/** Optional monospace detail line (the exact command / effect). */
detail?: string;
confirmLabel?: string;
danger?: boolean;
}

interface Pending extends ConfirmOptions {
resolve: (ok: boolean) => void;
}

const EVENT = "cdk-confirm";

export function confirmDialog(opts: ConfirmOptions): Promise<boolean> {
return new Promise((resolve) => {
window.dispatchEvent(
new CustomEvent<Pending>(EVENT, { detail: { ...opts, resolve } }),
);
});
}

export function ConfirmHost() {
const [p, setP] = useState<Pending | null>(null);

useEffect(() => {
function onReq(e: Event) {
setP((e as CustomEvent<Pending>).detail);
}
window.addEventListener(EVENT, onReq);
return () => window.removeEventListener(EVENT, onReq);
}, []);

useEffect(() => {
if (!p) return;
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") settle(false);
else if (e.key === "Enter") settle(true);
}
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [p]);

if (!p) return null;
function settle(ok: boolean) {
p?.resolve(ok);
setP(null);
}

return (
<div
role="dialog"
aria-modal="true"
aria-label={p.title}
onClick={() => settle(false)}
style={{
position: "fixed",
inset: 0,
zIndex: 200,
background: "color-mix(in srgb, #000 44%, transparent)",
display: "flex",
alignItems: "flex-start",
justifyContent: "center",
paddingTop: "18vh",
fontFamily: wSans,
animation: `cdk-fade ${FAST} ${EASE}`,
}}
>
<div
onClick={(e) => e.stopPropagation()}
style={{
width: "min(440px, 92vw)",
background: W.surface,
border: `1px solid ${W.borderHi}`,
borderRadius: R.dialog,
overflow: "hidden",
}}
>
<div style={{ padding: "16px 18px 14px" }}>
<h2 style={{ margin: "0 0 8px", fontSize: 15, fontWeight: 600, color: W.text }}>
{p.title}
</h2>
<p style={{ margin: 0, fontSize: 13, lineHeight: 1.5, color: W.text2 }}>
{p.body}
</p>
{p.detail && (
<div
style={{
marginTop: 10,
fontFamily: wMono,
fontSize: 11.5,
color: W.dim,
background: W.inset,
border: `1px solid ${W.border}`,
borderRadius: R.control,
padding: "7px 10px",
}}
>
{p.detail}
</div>
)}
</div>
<div
style={{
display: "flex",
justifyContent: "flex-end",
gap: 8,
padding: "12px 18px",
borderTop: `1px solid ${W.border}`,
background: W.sunken,
}}
>
<Button variant="ghost" size="md" onClick={() => settle(false)}>
Cancel
</Button>
<Button
variant={p.danger ? "danger" : "primary"}
size="md"
onClick={() => settle(true)}
>
{p.confirmLabel ?? "Confirm"}
</Button>
</div>
</div>
</div>
);
}
Loading
Loading