Dashboard
June 4, 2026 ยท View on GitHub
NanoTDB includes a lightweight browser UI served by the internal/web package.
The UI stays thin on the server side and does its rendering and refresh behavior
in the browser.
The dashboard is mobile-friendly and works for both narrow phone-width layouts and wider desktop-style layouts.
Screenshots
Dashboard (Mobile-Friendly)

Caption: Compact mobile-width dashboard for quick CPU, memory, disk, and sensor checks.
Dashboard (Wide Desktop Layout)

Caption: Wider desktop dashboard layout for broader operational views and denser widget placement.
Explore

Caption: Ad hoc metric exploration with a wide metric picker, last-value cards, and a live chart.
Dashboard Editor

Caption: In-browser editor for groups, widgets, series, preview, validation, and save.
Pages
/and/dashboardserve the configurable dashboard./dashboard/editserves the in-browser dashboard editor./exploreserves the manual database and metric explorer./engineserves the operational engine view for database, file, runtime, and active settings inspection.
Getting Started
Initialize a data directory:
./nanotdb --init --config ~/nanotdb-data/engine.toml
That creates:
engine.tomlfor server configuration.dashboard.jsonfor the default dashboard layout.
Start the server:
./nanotdb --config ~/nanotdb-data/engine.toml
Then open:
http://localhost:8428/http://localhost:8428/dashboardhttp://localhost:8428/dashboard/edithttp://localhost:8428/explorehttp://localhost:8428/engine
Dashboard Config
dashboard.json is file-backed and editable. The embedded default sample is tuned
to the metric names emitted by drip in cmd/drip/drip.toml.
Example:
{
"title": "NanoTDB Dashboard",
"default_db": "metrics",
"groups": [
{
"id": "overview",
"label": "Overview",
"widgets": ["system_snapshot", "load_history", "storage_snapshot"]
},
{
"id": "temperatures",
"label": "Temperatures",
"widgets": ["temperature_snapshot", "temperature_history"]
}
],
"widgets": {
"system_snapshot": {
"type": "numbers",
"title": "System Snapshot",
"refresh_sec": 10,
"series": [
{ "label": "Load 1m", "metric": "sys.load1", "transform": { "decimals": 2 } },
{ "label": "Load 5m", "metric": "sys.load5", "transform": { "decimals": 2 } },
{ "label": "Mem Avail", "metric": "mem.available", "transform": { "factor": 0.000001, "unit": " GB", "decimals": 1 }, "thresholds": { "direction": "below", "warning": 1.0, "critical": 0.5 } },
{ "label": "CPU Clock", "metric": "cpu.freq_khz", "transform": { "factor": 0.000001, "unit": " GHz", "decimals": 2 } },
{ "label": "CPU Temp", "metric": "temp.cpu", "transform": { "factor": 0.001, "unit": " C", "decimals": 1 }, "thresholds": { "direction": "above", "warning": 70, "critical": 80 } }
]
},
"load_history": {
"type": "line_chart",
"title": "Load Average",
"refresh_sec": 15,
"lookback": "6h",
"interval": "1m",
"series": [
{ "label": "Load 1m", "metric": "sys.load1" },
{ "label": "Load 5m", "metric": "sys.load5" },
{ "label": "Load 15m", "metric": "sys.load15" }
]
},
"storage_snapshot": {
"type": "numbers",
"title": "Storage Snapshot",
"refresh_sec": 15,
"series": [
{ "label": "Root Used %", "metric": "diskfs.root.used_pct", "transform": { "unit": "%", "decimals": 1 }, "thresholds": { "direction": "above", "warning": 80, "critical": 90 } },
{ "label": "Root Free", "metric": "diskfs.root.bytes_avail", "transform": { "factor": 0.0000000009313225746154785, "unit": " GiB", "decimals": 1 }, "thresholds": { "direction": "below", "warning": 8, "critical": 4 } },
{ "label": "SD Write ms", "metric": "disk.sd_write_probe_ms", "transform": { "unit": " ms", "decimals": 1 }, "thresholds": { "direction": "above", "warning": 25, "critical": 50 } }
]
},
"temperature_snapshot": {
"type": "numbers",
"title": "Temperatures",
"refresh_sec": 15,
"series": [
{ "label": "CPU", "metric": "temp.cpu", "transform": { "factor": 0.001, "unit": " C", "decimals": 1 }, "thresholds": { "direction": "above", "warning": 70, "critical": 80 } },
{ "label": "Office Dry", "metric": "temp.office_dry.mdeg", "transform": { "factor": 0.001, "unit": " C", "decimals": 1 } },
{ "label": "Office Wet", "metric": "temp.office_wet.mdeg", "transform": { "factor": 0.001, "unit": " C", "decimals": 1 } },
{ "label": "Outdoor", "metric": "temp.out_dry.mdeg", "transform": { "factor": 0.001, "unit": " C", "decimals": 1 } }
]
},
"temperature_history": {
"type": "line_chart",
"title": "Temperature History",
"refresh_sec": 30,
"lookback": "12h",
"interval": "2m",
"series": [
{ "label": "CPU", "metric": "temp.cpu", "transform": { "factor": 0.001, "unit": " C", "decimals": 1 } },
{ "label": "Office Dry", "metric": "temp.office_dry.mdeg", "transform": { "factor": 0.001, "unit": " C", "decimals": 1 } },
{ "label": "Office Wet", "metric": "temp.office_wet.mdeg", "transform": { "factor": 0.001, "unit": " C", "decimals": 1 } },
{ "label": "Outdoor", "metric": "temp.out_dry.mdeg", "transform": { "factor": 0.001, "unit": " C", "decimals": 1 } }
]
}
}
}
UI-only display conversion is configured per series using transform, for example
{"factor": 0.001, "unit": " C", "decimals": 1} to convert millidegrees to
degrees in the browser.
Widgets can also opt out of timer-based refresh with "auto_refresh": false.
This is useful for long-lookback charts or historical widgets that should only
update when refreshed manually.
In the live dashboard, line-chart widgets also expose a local lookback picker in
the widget header so users can temporarily widen or narrow the visible time
window without editing dashboard.json.
Aggregate-backed charts are also supported. A widget can either stay a normal
line_chart and set per-series aggregate + window, or use the first-class
aggregate_band widget type for min/avg/max band charts backed by one shared
source query.
Config shape summary:
titleis required.default_dbsets the database used when a series does not override it.groups[]defines dashboard tabs/sections in display order.groups[].widgets[]references widget ids from the top-levelwidgetsmap.widgets.<id>.typecurrently supportsnumber,numbers,line_chart,aggregate_band, andevent_log.- Each widget must define at least one series.
- A metric series must define either
metricormeasurement+field. - An event series must define
event_name_pattern(wildcard patterns supported with*). line_chartandaggregate_bandwidgets require valid duration strings forlookbackandinterval.event_logwidgets require a valid duration string forlookback.line_chartandaggregate_bandwidgets must not contain duplicate effective labels. A duplicate label is any repeated explicitlabel, or repeated fallback label derived frommetricormeasurement.field.- Aggregate series must set
aggregateandwindowtogether. - Supported aggregate names come from
GET /api/v1/aggregatesand currently includeavg,count,max,median,min,p50,p95,p99,sum,trimmed_avg, andtrimmed_average. aggregate_bandwidgets either define explicitmin/avg/maxseries roles, or use the single-series shortcut that expands into those three roles automatically.
Series support:
dbordatabaseoverridesdefault_dbfor a single series.aggregateandwindowlet chart series query bucketed aggregates instead of raw points.transform.factor,transform.offset,transform.unit,transform.decimals, andtransform.formatare display-only changes applied in the browser.thresholds.directionmust beaboveorbelowwhen warning or critical thresholds are set.- Thresholds only affect number and numbers widgets. Chart widgets render the transformed values but do not apply severity coloring.
event_limitsets the maximum number of recent events to display in an event_log widget (default: 10).
Event Widgets
The event_log widget type displays recent events from the event stream. Events
are queried using wildcardable event name patterns.
Example event_log widget:
{
"type": "event_log",
"title": "Slow Disk Writes",
"refresh_sec": 10,
"lookback": "6h",
"series": [
{
"event_name_pattern": "disk.sd_write_probe.slow",
"event_limit": 10,
"db": "metrics"
}
]
}
Rows display event name, timestamp, and event value. If the event payload
contains latency_ms, it is shown as the value; otherwise, the top-level
numeric value field is used if present.
Editor
/dashboard/edit loads the current server-side dashboard.json, lets you edit
it in-place, previews the selected group in the browser, and saves the result
back through the dashboard config API.
The editor layout has four working areas:
- Dashboard metadata for title, default database, and the Validate, Refresh Preview, Revert, and Save actions.
- A Groups pane for adding, renaming, reordering, and deleting groups.
- A Widgets pane for adding a new widget to the selected group or attaching an existing shared widget to that group.
- A Widget Editor pane for editing the selected widget and its series.
Important editor behavior:
- Widgets are reusable across groups. Adding an existing widget links the same widget id into another group instead of cloning it.
- The usage badge shows whether the selected widget is shared by multiple groups.
- The group preview is live but debounced; most edits trigger a refresh of the currently selected group preview after a short delay.
Refresh Previewforces an immediate preview rebuild.- The preview uses the configured widget
lookbackandinterval. The live dashboard's local lookback picker is not part of the editor preview. - There is no browser-side autosave.
Revertrestores the last config loaded from the server, or the last successfully saved config. Savesends the normalized draft toPUT /api/dashboard-config. When the server writes a backup of the previous file, the editor displays that backup path in the status row.Validatesends the draft toPOST /api/dashboard-config/validatewithout writing the file.
Widget editor details:
- Widget ids are slugged to lowercase underscore-separated identifiers and kept unique when renamed or created.
- New widgets default to type
numbers, inherit the global refresh cadence, and start with one series. - Line-chart and aggregate-band widgets expose
lookbackandintervalfields in the editor. - Series rows can be reordered, duplicated, and deleted.
- Series can point at a metric directly or use
measurement+field. - The editor loads the live database list, metric catalog, and supported aggregate names from the NanoTDB API to populate database selects, metric suggestions, and aggregate pickers.
- Empty transform and threshold objects are removed before save, so the emitted JSON stays compact.
Web Config
The dashboard-related settings live under [web] in engine.toml:
enabledenables or disables the web handlers.base_pathsets the dashboard route prefix, default/dashboard.explore_pathsets the manual explorer route prefix, default/explore.engine_pathsets the engine explorer route prefix, default/engine.titlesets the browser page title.refresh_secondssets the default UI refresh cadence.dashboard_configpoints at the dashboard JSON file.web_rootpoints at a filesystem directory that overrides the embedded UI bundle.api_base_urlsets the absolute API base the browser should call when the UI is hosted separately.
Example:
[web]
enabled = true
base_path = "/dashboard"
explore_path = "/explore"
engine_path = "/engine"
title = "NanoTDB Dashboard"
refresh_seconds = 10
dashboard_config = "dashboard.json"
web_root = "ui"
api_base_url = ""
Editable UI Assets
To export the embedded UI bundle for editing:
./nanotdb --export-web-assets ./ui
Then set [web].web_root to that directory. NanoTDB will serve these files from
disk instead of the embedded bundle:
dashboard.htmleditor.htmlindex.htmlengine.htmldashboard_assets/assets/engine_assets/common_assets/
This lets you edit HTML, CSS, and JavaScript without rebuilding the Go binary.
If you host the exported UI separately from the NanoTDB process, set [web].api_base_url
so the browser pages call the NanoTDB API at the correct origin.
API Endpoints Used By The UI
GET /api/dashboard-configPOST /api/dashboard-config/validatePUT /api/dashboard-configGET /api/v1/aggregatesGET /api/v1/databasesGET /api/v1/metrics?db=<name>GET /api/v1/queryGET /api/v1/query_rangeGET /api/engine/overviewGET /api/engine/database?db=<name>GET /api/engine/files?db=<name>GET /api/engine/runtime
Browser Smoke Test
For a browser-level regression check of the dashboard and editor:
npm install
npx playwright install
npm run test:web-smoke
The smoke script starts a temporary NanoTDB server, stubs the chart library and metric API responses in-browser, verifies dashboard widget refresh failures are surfaced inline, and checks that editor validation rejects duplicate line-chart labels.
Sample Rollup Fixture
To run NanoTDB against the rollup-enabled fixture and keep appending fresh points every 10 seconds:
./scripts/run_sample_rollup_server.sh
Defaults:
- root dir:
test-data/full-cycle-check - config:
test-data/full-cycle-check/engine.toml - dashboard config:
test-data/full-cycle-check/dashboard.json - ingest interval:
10seconds - base URL:
http://127.0.0.1:8428 - metrics per tick:
10(temp.synthetic00..temp.synthetic09) - source DB:
source
Optional arguments:
./scripts/run_sample_rollup_server.sh <root-dir> <config-path> <interval-seconds> <base-url> <metric-count> <source-db>
This keeps the server in the foreground and prints an ingest tick log. Stop with
Ctrl+C.