diff --git a/.gitignore b/.gitignore index 5cde9d7a..4bc936a4 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/assets/dashboard_test.go b/assets/dashboard_test.go index 7834222d..069efa7b 100644 --- a/assets/dashboard_test.go +++ b/assets/dashboard_test.go @@ -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{} diff --git a/assets/grafana/dashboards/canton-localnet.json b/assets/grafana/dashboards/canton-localnet.json index fe2ec170..cbee8f53 100644 --- a/assets/grafana/dashboards/canton-localnet.json +++ b/assets/grafana/dashboards/canton-localnet.json @@ -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" } ] }, @@ -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": { @@ -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" } ] diff --git a/docs/dashboard-customization.md b/docs/dashboard-customization.md index b612f4f5..91282956 100644 --- a/docs/dashboard-customization.md +++ b/docs/dashboard-customization.md @@ -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 diff --git a/frontend/index.html b/frontend/index.html index e117ff55..aea43437 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,10 +1,10 @@ - +
- ++ {p.body} +
+ {p.detail && ( +. Tail size
-// and since duration are toolbar-tunable; auto-scroll-to-bottom is on
-// by default but disabled once the user scrolls up.
+// Polls docker logs for the selected container at LOG_POLL_MS. Tail
+// and since are toolbar-tunable; auto-scroll disables once the user
+// scrolls up.
const LOG_POLL_MS = 3000;
@@ -27,12 +25,10 @@ export function ContainerLogsModal({ open, instance, container, onClose }: Props
const [loading, setLoading] = useState(false);
const preRef = useRef(null);
const autoScrollRef = useRef(true);
- // Only close when both mousedown AND click originated on the overlay;
- // otherwise the click that opened the modal (mousedown on a row cell,
- // mouseup after the modal mounted) would immediately close it.
+ // Close only when both mousedown AND click landed on the overlay, else
+ // the opening click (mouseup after the modal mounts) closes it instantly.
const downOnOverlayRef = useRef(false);
- // Esc closes.
useEffect(() => {
if (!open) return;
function onKey(e: KeyboardEvent) {
@@ -73,8 +69,6 @@ export function ContainerLogsModal({ open, instance, container, onClose }: Props
};
}, [open, instance, container, tail, since]);
- // Auto-scroll to bottom on new content unless the user scrolled up;
- // tracked via a ref to avoid a state update per scroll event.
useEffect(() => {
if (!preRef.current || !autoScrollRef.current) return;
preRef.current.scrollTop = preRef.current.scrollHeight;
@@ -134,7 +128,7 @@ export function ContainerLogsModal({ open, instance, container, onClose }: Props
role="alert"
style={{
color: W.err,
- background: `${W.err}10`,
+ background: `${tint(W.err, 6)}`,
borderBottom: `1px solid ${W.err}`,
padding: "8px 16px",
fontSize: 12,
@@ -230,9 +224,8 @@ const modalStyle: React.CSSProperties = {
width: "min(900px, 95vw)",
height: "min(700px, 88vh)",
background: W.surface,
- border: `1px solid ${W.border}`,
- borderRadius: 4,
- boxShadow: "0 24px 64px rgba(0,0,0,0.6)",
+ border: `1px solid ${W.borderHi}`,
+ borderRadius: R.card,
display: "flex",
flexDirection: "column",
overflow: "hidden",
@@ -250,7 +243,7 @@ const selectStyle: React.CSSProperties = {
background: W.bg,
color: W.text,
border: `1px solid ${W.border}`,
- borderRadius: 2,
+ borderRadius: R.control,
padding: "2px 6px",
fontSize: 11,
fontFamily: wMono,
diff --git a/frontend/src/screens/ContractDetailDrawer.tsx b/frontend/src/screens/ContractDetailDrawer.tsx
index 56c20e85..95b07fc3 100644
--- a/frontend/src/screens/ContractDetailDrawer.tsx
+++ b/frontend/src/screens/ContractDetailDrawer.tsx
@@ -6,33 +6,21 @@ import {
type ContractRow,
type Role,
} from "../api";
-import { W, wMono, wideCaps } from "../tokens";
+import { W, wMono, wideCaps, tint, R } from "../tokens";
import { Button } from "../components/Button";
+import { MonoId } from "../components/MonoId";
import { IcX } from "../components/icons";
-// ContractDetailDrawer is a true right-side overlay: position-fixed
-// below the topbar so the ACS table keeps its full width. It opens
-// when a row is clicked. Fetches the deep view from
-// /api/instances/{name}/contracts/{cid}
-// (EventQueryService.GetEventsByContractId) for the create event's full
-// payload, signatories, observers, and archive metadata. While that
-// loads it shows the row-level ACS fields so the user always has
-// something to read.
-//
-// Keyboard: Esc closes, J/K move to the next/previous row. The parent
-// owns row navigation because it owns the filtered table state; the
-// drawer only owns the deep-view fetch lifecycle.
-
+// Right-side overlay showing a contract's deep view (payload, parties,
+// archive metadata), falling back to row-level ACS fields while it loads.
+// The parent owns J/K row navigation since it holds the filtered table state.
export interface ContractDetailDrawerProps {
instance: string;
role: Role;
/** Row data we already have from the ACS snapshot. */
row: ContractRow;
- /** Close the drawer. */
onClose: () => void;
- /** Move selection to the previous row (K / ArrowUp). */
onPrev?: () => void;
- /** Move selection to the next row (J / ArrowDown). */
onNext?: () => void;
}
@@ -49,7 +37,6 @@ export function ContractDetailDrawer({
| { kind: "ok"; detail: ContractDetail }
| { kind: "err"; message: string }
>({ kind: "loading" });
- const [copied, setCopied] = useState(false);
useEffect(() => {
let cancelled = false;
@@ -72,9 +59,8 @@ export function ContractDetailDrawer({
};
}, [instance, role, row.contract_id]);
- // Esc / J / K, listened on window so keystrokes work from anywhere
- // on the page. INPUT/TEXTAREA/contenteditable are ignored so typing
- // in the search box doesn't trigger navigation.
+ // Esc / J / K on window; skip when an editable element is focused so
+ // typing in the search box doesn't trigger navigation.
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
const active = document.activeElement as HTMLElement | null;
@@ -116,16 +102,6 @@ export function ContractDetailDrawer({
archived: false,
};
- const copyCid = async () => {
- try {
- await navigator.clipboard.writeText(detail.contract_id);
- setCopied(true);
- setTimeout(() => setCopied(false), 1100);
- } catch {
- // clipboard may be unavailable (http on non-localhost); silent
- }
- };
-
return (