Widgets, grid and filters
The /dashboards page itself is a self-contained HTML+JS page in the style of the admin panel's other pages (embeddings/import) — no external JS framework, no charting library, staying within the engine's zero-dependency policy.
Widget types
Five types: line, bar, pie, table, kpi. A widget's grid position and size (X/Y/W/H) are in grid cells, not pixels — the frontend decides the actual cell size. A widget's Config maps the query result onto the visualization: label_field/value_field/series_field.
Every widget has exactly one query source — a chart (see "Datasets and charts"), a No-Code spec, or raw SQL text.
Global filters
A dashboard can define a set of global filter selectors shown at the top: type date_range, select, or text, bound to a table.column reference. The column is validated per widget at render time, not when the filter is created — different widgets on the same dashboard may query different tables, and a filter simply won't apply where there's no matching column (expected behavior for a multi-widget dashboard, not a silent bug). As noted on the "No-Code query builder" page, filters only graft onto widgets with a structured source (a chart or a No-Code spec) — they don't apply to raw-SQL widgets.
Storage
Dashboards and widgets are stored in data/dashboards.json — a single JSON file, atomic rename-through-tmp writes, in-memory maps guarded by an RWMutex — the same pattern as public share links and RBAC users. Deleting a dashboard cascades to delete its widgets.