Status: draft, pre-implementation.
Dashglass is a lightweight, Prometheus-native dashboarding tool: the dashboard part of Grafana — and only that — with first-class GitOps support.
- Prometheus only as a data source (at first).
- Dashboards are edited in the UI but stored as files (JSON, YAML or TOML, user's choice) in a directory, so they can be versioned, reviewed and deployed through git.
- Ships as a single Go binary embedding the SPA — no database, no migration scripts, no plugin marketplace. Deliberately lighter than Grafana.
A second, longer-term goal shapes the architecture from day one: the Bleemeo SaaS panel should eventually be able to embed this UI and extend it with proprietary plugins (open-core model). The open-source app and the SaaS panel would share the same dashboard engine, the same panel plugins and the same API contract, differing only in the plugins they register and the backend they talk to.
- Alerting, notification channels, incident management.
- Non-Prometheus data sources (SQL, Loki, traces…) — the data source layer is an interface, but only Prometheus is implemented.
- Fine-grained permissions beyond the built-in three-role model (§7.4): no per-dashboard/folder ACLs, no teams, no organizations/multi-tenancy, no SCIM, no audit log — that is the SaaS backend's concern. The OSS binary ships users, three fixed roles and identity-provider mapping, because self-hosted users rightfully expect that much; the line is drawn at fine-grained and multi-tenant.
- Runtime-loadable third-party plugins. Plugins are composed at build time; the registry design keeps runtime loading possible later.
Aligned with bleemeo-panel so components, patterns and developer habits
transfer both ways:
| Concern | Choice | Rationale |
|---|---|---|
| Build/runtime | Vite, React 19, TypeScript | Same as panel |
| UI kit | Chakra UI v3 | Same as panel |
| Charts | Apache ECharts (canvas) | Panel already uses ECharts for every serious widget (line, gauge, heatmap, bar) with shared utilities we can reuse. Canvas handles the point volumes a Prometheus dashboard produces; Recharts (SVG, one DOM node per point) does not. Native zoom/brush, gauges, heatmaps. |
| PromQL editing | @prometheus-io/codemirror-promql |
Official Prometheus package: highlighting + linting + autocompletion backed by the live Prometheus API (metric names, labels, label values). Already used by panel. |
| Dashboard grid | react-grid-layout v2 |
Already used by panel (WidgetGrid) |
| Schema/validation | Zod | Canonical dashboard schema, file validation on load, JSON Schema generation for editor autocompletion in GitOps workflows |
| Server state (OSS app) | TanStack Query | See §8 |
| Backend | Go, single binary, SPA embedded via embed.FS |
Consistent with the Bleemeo ecosystem; one artifact to deploy |
┌───────────────────────────────┐
browser ────────►│ Go backend (single binary) │
│ │
│ • serves the embedded SPA │
│ • Prometheus proxy │──────► Prometheus
│ (native /api/v1/* paths) │
│ • dashboard file CRUD │──────► dashboards/ dir
│ • fsnotify watcher → SSE │ (git repo)
│ • /api/capabilities │
└───────────────────────────────┘
The backend exists because the browser can neither write files nor query Prometheus across origins with server-held credentials. It has three jobs:
- Serve the SPA (embedded, one binary).
- Proxy Prometheus, exposing the native Prometheus HTTP API paths
(
/api/v1/query_range,/api/v1/query,/api/v1/labels,/api/v1/label/<name>/values,/api/v1/metadata). Prometheus-side authentication is configured server-side; credentials never reach the browser. Mirroring the native API keeps the frontend client trivial and letscodemirror-promqlplug in directly. - Manage dashboard files: read/validate/write the dashboards directory,
watch it with fsnotify and push changes to the UI over SSE —
git pullin the directory updates open dashboards live. That is the GitOps loop.
We adopt the Perses dashboard model (CNCF) rather than inventing one. It
is well designed, validation-friendly, and — decisively — plugin-oriented
in the format itself: panels, queries, data sources and variables are all
kind/spec pairs, so proprietary panel kinds serialize in the same file
format with no schema change.
Shape (Perses-derived, apiVersion-versioned for future migrations):
kind: Dashboard
metadata:
name: node-overview
spec:
display: { name: "Node overview" }
duration: 1h
refreshInterval: 30s
variables:
- kind: ListVariable
spec:
name: instance
plugin:
kind: PrometheusLabelValuesVariable
spec: { labelName: instance }
panels:
cpu:
kind: Panel
spec:
display: { name: "CPU usage" }
plugin:
kind: TimeSeriesChart
spec: { legend: { position: bottom } }
queries:
- kind: TimeSeriesQuery
spec:
plugin:
kind: PrometheusTimeSeriesQuery
spec:
query: 'rate(node_cpu_seconds_total{instance=~"$instance"}[5m])'
seriesNameFormat: "{{cpu}} {{mode}}"
layouts:
- kind: Grid
spec:
items:
- { x: 0, y: 0, width: 12, height: 8,
content: { $ref: "#/spec/panels/cpu" } }Key properties we keep from Perses:
- Panels and layouts are separate: panels are a map, layouts reference
them by
$ref. Moving a panel is a layout-only diff. - Everything extensible is
kind/spec— the plugin registry (§7) is a direct lookup onkind.
The model lives in packages/model as Zod schemas + TypeScript types, with
serializers and migrations. The Go backend validates against the same schema
(generated JSON Schema).
This is where Grafana fails and Dashglass must shine. Rules for every writer (UI saves included):
- Deterministic output: stable key order, stable list order. Editing a panel title must produce a one-line diff.
- No volatile fields: no timestamps, no regenerated IDs, no editor metadata in the file.
- Defaults are omitted, not expanded.
- Format by extension:
.json/.yaml/.toml, one internal model, three encoders. YAML is the default (most readable in review). - Versioned schema (
apiVersion) with explicit migrations. - Optional provisioned/read-only mode: dashboards owned by git; the UI allows experimenting but offers "export patch" instead of saving.
packages/
model/ # Perses-derived types, Zod schemas, serializers
# (json/yaml/toml), schema migrations, round-trip tests
runtime/ # plugin registries, dashboard rendering engine
# (grid, panel chrome, time-range & variables context,
# metrics query orchestrator), DataSource interface
ui/ # Chakra theme + shared primitives
panels-builtin/ # TimeSeriesChart, StatChart, GaugeChart, Table (ECharts)
datasource-prometheus/ # PrometheusDataSource impl targeting the native API
apps/
dashglass/ # OSS app shell: assembles runtime + builtins +
# local-backend datasource + basic auth provider
backend/ # Go server
The SaaS panel is a separate (private) consumer of the published packages: it registers the same built-ins plus proprietary plugins. Composition happens at build time (the Grafana OSS/Enterprise, GitLab CE/EE model) — no module federation, no runtime plugin loading, no version matrix.
Exploration of bleemeo-panel identified exactly what is hard-coded today
and must be a registry in Dashglass from the first commit:
| Extension point | Today in bleemeo-panel | In Dashglass |
|---|---|---|
| Panel types | Hard-coded ternary chain on a numeric enum (WidgetCard.tsx) |
registerPanel({ kind, component, editor, defaults, migrate }) |
| Navigation & routes | Static navigationPages array + inline <Routes> (App.tsx), gated by feature flags |
registerPage({ nav, route, element }), filtered by capabilities |
| Data source | Metrics engine wired to /v1/widget/preview_query/ (promql-data.ts) |
PrometheusDataSource interface (§7.1) |
| Auth | Implicit Django session cookie + CSRF + X-Bleemeo-Account header (api.ts) |
AuthProvider interface: getUser(), decorateRequest(), login()/logout() |
A kind found in a dashboard file resolves through the panel registry to a
component; unknown kinds render a graceful "unknown panel" placeholder (so an
OSS install can open a SaaS-exported dashboard without crashing).
Existing Bleemeo widget kinds map cleanly: LINE/STACKED → TimeSeriesChart,
NUMBER → StatChart, GAUGE → GaugeChart; STATUS, SNMP_STATUS and LOGS
remain proprietary kinds — which is precisely the validation of the plugin
model.
The cornerstone of backend interchangeability. One TypeScript interface, shaped after the Prometheus HTTP API:
interface PrometheusDataSource {
rangeQuery(promql: string, start: Time, end: Time, step: Duration): Promise<PromResponse>
instantQuery(promql: string, time?: Time): Promise<PromResponse>
labelNames(match?: string[], range?: TimeRange): Promise<string[]>
labelValues(label: string, match?: string[], range?: TimeRange): Promise<string[]>
metricMetadata(): Promise<MetricMetadata>
}- OSS implementation: calls the Go backend's Prometheus proxy (native paths) — near-trivial.
- SaaS implementation: calls Bleemeo's
preview_query/promql_for_metricendpoints. Their responses are already Prometheus-shaped ({ status, data: { resultType, result } }), so the adapter is thin.
The same interface feeds codemirror-promql completions and the variables
system (label_values(...)).
A small OpenAPI-specified "Dashglass API" both backends implement:
- Dashboard CRUD (list/get/save/delete, plus watch events over SSE).
- The Prometheus proxy (§4).
/api/capabilities(§7.3).- Current-user info.
The Go backend implements it natively; the Bleemeo API implements it behind a thin façade. The UI only ever speaks this contract.
At startup the UI asks the backend what it supports: multi-tenancy, billing,
agents, provisioned/read-only mode, active plugin list… The OSS backend
returns the minimal profile; the SaaS returns the full one. This generalizes
the panel's existing hasAgent/hasK8S/hasMonitor feature-flag pattern
into a server-driven mechanism — the same UI, fed differently, instead of a
fork.
Configuration. The binary reads an optional YAML file (-config dashglass.yaml) with ${ENV_VAR} expansion for secrets; precedence is
flags > environment > file > defaults, and every flag keeps working
without a file. Two kinds of state, two files:
dashglass.yaml— static configuration written by a human (listen, Prometheus URL, dashboards dir, auth providers). Anything defined here is authoritative: the admin UI shows it read-only.auth.yaml— server-managed state written atomically by the backend (local users, role mappings edited from the UI, session secret). Never hand-edited; the "provisioned or managed" split mirrors the dashboards readonly mode.
Authentication. Three fixed roles — viewer (read dashboards, query
proxy), editor (+ write dashboards), admin (+ admin surface). One
effective role per user, no cumulation. -readonly stays orthogonal and
wins over roles for dashboard writes. Identity comes from, in delivery
order:
- Local users — username + argon2id hash + role in
auth.yaml. Managed from the admin UI (and adashglass userbreak-glass CLI +DASHGLASS_BOOTSTRAP_ADMIN_PASSWORDfor automated deployments). Bootstrap without CLI: while auth is disabled everyone is an anonymous admin, so the admin UI is reachable and "create the first user" enables authentication. - Trusted reverse proxy —
user/groupsheaders from Authelia, Authentik, oauth2-proxy…; no Dashglass session, headers are evaluated per request. - Native OIDC — authorization-code flow, groups claim; lands on the same session cookie as local login.
- Native LDAP — deferred until demand shows up; most homelab LDAP deployments already front it with an OIDC provider.
External identities carry groups; groups map to roles with one shared
structure per provider (role_mapping: {admin: [...], editor: [...], viewer: ["*"]}): the highest matching role wins, "*" matches any
authenticated user, and no match means 403 — being authenticated in a
shared IdP grants nothing until a mapping says so. Local users have no
groups; their role is direct.
Sessions are signed cookies (HttpOnly, SameSite=Lax, HMAC secret
generated into auth.yaml), plus a per-user session version bumped on
password/role change so revocation is immediate despite stateless
cookies. Login is throttled per IP. capabilities.auth advertises the
mode so the same UI serves the SaaS through its own AuthProvider
(§7 table) — this section is the OSS implementation of that contract,
not a second system.
bleemeo-panel is deeply RTK Query (Redux); the natural choice for a small
OSS app is TanStack Query. Picking either for the shared packages would
leak an implementation choice into the public contract: RTK Query would force
every host to mount a Redux store with our reducers/middleware; exposed
TanStack hooks would force a QueryClient on the panel.
Rule: the shared packages expose neither Redux nor TanStack Query in their public API.
runtimeembeds its own metrics query orchestrator, self-contained behind thePrometheusDataSourceinterface: per-panel polling aligned on the querystep, abort when a panel leaves the viewport, sharing of identical queries across panels. It is the functional equivalent of panel'spromql-data.tslistener-middleware engine, but encapsulated with no external store dependency (this is also how Perses makes its engine embeddable). Panels are pure display components receiving data as props from the panel chrome. The orchestrator's needs (step-aligned polling, visibility-based abort, streaming later) exceed TanStack's request/cache model anyway, so it is dedicated code with no dependency.- Each host app keeps its own stack for management CRUD (dashboards,
settings, users):
- the OSS
apps/dashglassuses TanStack Query — no global store to maintain for an app this size; bleemeo-panelkeeps Redux/RTK unchanged. When it embeds the dashboard engine, it mounts<DashboardRenderer>with aPrometheusDataSourceimplementation backed bypreview_query, next to its Redux store, with no conflict. Itspromql-data.tseventually becomes replaceable by the runtime orchestrator.
- the OSS
Nuance: the rule constrains the public API. Internally, runtime could
use a private, non-exposed QueryClient as an implementation detail — it
coexists fine with Redux in a host — but the metrics orchestrator is
dedicated code regardless, to keep the package lean.
The panel is already PromQL-native end to end (widgets carry a
promql_query; the metrics endpoint returns Prometheus-shaped responses), so
convergence is an adapter problem, not a rewrite. Strangler-fig, not big
bang:
- Dashglass ships standalone with the four registries in place (even while only built-ins exist).
- Panel converts its widget-dispatch ternary and
navigationPagesarray into registries (an internal refactor, no behavior change). - Panel embeds the Dashglass dashboard engine for its dashboard pages, with a
Bleemeo
PrometheusDataSourceand its proprietary panel kinds. - Model convergence: a bidirectional converter between the Bleemeo
widget model (widgets as separate API resources, grid stored per widget,
DashboardLayoutJSONexport format) and the Dashglass/Perses document model. Long-term, the SaaS can store/expose dashboards in the Dashglass format, enabling "export your SaaS dashboards to git" (GitOps as a SaaS feature) and a two-way migration bridge between OSS and SaaS users.
Versioning discipline: once panel consumes the packages, the plugin API is a
contract. Keep it marked experimental until Dashglass v1 ships, and only
stabilize it after porting one real panel widget onto it — that exercise
reveals what the contract is missing.
The original feature plan (v1, then v2+), kept for the rationale behind each choice. What actually exists is tracked in §13 — that matrix is authoritative; all of v1 and most of v2+ below has shipped, so read this section as intent, not status.
- Panel kinds: timeseries, stat (single value + sparkline), gauge, table.
- PromQL editor with highlighting, linting and live autocompletion
(CodeMirror +
codemirror-promqlwired to the data source). - Dashboard variables (
$instance,$job) fed bylabel_values()— in the schema from day one even if the editor UI comes later. - Time range picker, auto-refresh, zoom/brush on charts.
- Legend templating (
{{instance}} — {{mountpoint}}) and unit formatting (bytes, percent, durations) — small details that carry 90% of perceived quality. - Explore mode: a free PromQL + graph page to iterate on a query before adding it to a panel.
- File watch + live reload (the GitOps loop).
- Heatmap and bar panels.
- Grafana dashboard importer (even partial: timeseries/stat/gauge) — the adoption killer feature; nobody recreates 40 dashboards by hand.
- Dashboard history via
git logwhen the dashboards directory is a git repo (read-only integration; git itself is the version store). - Provisioned/read-only mode (the originally-sketched "export patch" was dropped — provisioned instances are edited in git).
- Data links / drill-down: panel links carrying the current variables and
time range (
$instance,$__from/$__to) to another dashboard or an external tool. - Repeating panels by variable: one panel definition rendered once per value of a variable (panel repeat done; row/section repeat is the follow-up).
- Annotations (planned — not yet built).
packages/model— schema, serializers, migrations, round-trip tests. The file format is the product; it comes first.- Go backend — Prometheus proxy, file CRUD + validation, fsnotify watcher + SSE, capabilities.
packages/runtime— the four registries, grid rendering, panel chrome, time-range/variables context, metrics orchestrator.packages/panels-builtin(reusing panel's ECharts utilities —chart-options.ts,line-chart-tools.ts— as a starting point) + PromQL editor.apps/dashglassassembled end to end; then, only after stabilization, the "panel embeds the engine" proof of concept.
License— decided (2026-07): Apache-2.0, adoption is the priority.LICENSEat the repo root; every package declares"license": "Apache-2.0".- Project naming: working name "Dashglass" (repo
dashglass); check for trademark/name collisions before going public. - Prior art: Perses (CNCF) shares the pitch and provided the data model; differentiation is editing UX, git-friendly multi-format serialization, and the Bleemeo convergence story.
Canonical status of every feature — the single source of truth the README
and CLAUDE.md defer to. Update this table when a feature lands or a
non-goal changes. Legend: ✅ done · 🚧 planned · ⛔ non-goal (§2).
| Area | Feature | Status |
|---|---|---|
| Panels | Time series, stat, gauge, table, bar, heatmap, status grid, markdown, image, clock | ✅ |
| Panels | Legend templating ({{label}}) and unit formatting |
✅ |
| Dashboards | PromQL editor (autocomplete + lint) | ✅ |
| Dashboards | Variables, incl. chaining (dependent lists) | ✅ |
| Dashboards | Time range picker, auto-refresh, zoom/brush | ✅ |
| Dashboards | Explore mode | ✅ |
| Dashboards | Kiosk / TV mode, light & dark themes | ✅ |
| Dashboards | Panel repeat by variable | ✅ |
| Dashboards | Data links / drill-down (variable + $__from/$__to) |
✅ |
| Dashboards | Row/section repeat | 🚧 |
| Dashboards | Annotations | 🚧 |
| Storage | Deterministic JSON/YAML/TOML files, one-field/one-line diff | ✅ |
| Storage | File watch → SSE live reload | ✅ |
| Storage | Git history (log/diff/restore), commit-on-save, remote sync | ✅ |
| Storage | Provisioned / -readonly mode |
✅ |
| Data | Prometheus via read-only proxy | ✅ |
| Data | Datasource auth: basic/bearer, custom headers, custom CA, mTLS | ✅ |
| Data | Non-Prometheus sources (SQL, Loki, traces) | ⛔ |
| Import | Grafana JSON and grafana.com import | ✅ |
| Auth | Local users + 3 roles + admin UI | ✅ |
| Auth | Reverse-proxy headers, OIDC (auth-code + PKCE, groups→role) | ✅ |
| Auth | LDAP | 🚧 |
| Auth | Per-dashboard ACLs, teams, multi-tenancy | ⛔ |
| Ops | Single Go binary (embedded SPA), Docker packaging | ✅ |
| Ops | Release pipeline (CalVer tags, prebuilt binaries, GHCR images) | ✅ |
| Ops | Runtime admin config (datasource, git remote) | ✅ |
| Alerting | Alerting, notifications, incident management | ⛔ |
| Plugins | Runtime-loadable third-party plugins (build-time only) | ⛔ |
| SaaS | bleemeo-panel embedding POC (§9) | 🚧 |