Ctrl K
ComunidadGuías…

DESIGN.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Dashglass — Design Document

Status: draft, pre-implementation.

## 1. Vision

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.

## 2. Non-goals

- 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.

## 3. Tech stack

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 |

## 4. Architecture overview

```
                    ┌───────────────────────────────┐
   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:

1. **Serve the SPA** (embedded, one binary).
2. **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
   lets `codemirror-promql` plug in directly.
3. **Manage dashboard files**: read/validate/write the dashboards directory,
   watch it with fsnotify and push changes to the UI over SSE — `git pull`
   in the directory updates open dashboards live. That is the GitOps loop.

## 5. Dashboard data model

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):

```yaml
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 on `kind`.

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).

### Git-friendly serialization rules

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.

## 6. Repository layout (pnpm monorepo)

```
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.

## 7. Plugin architecture — the four registries

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.

### 7.1 The DataSource contract

The cornerstone of backend interchangeability. One TypeScript interface,
shaped after the Prometheus HTTP API:

```ts
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_metric` endpoints. 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(...)`).

### 7.2 The management API contract

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.

### 7.3 Capabilities

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.

### 7.4 Configuration & authentication

**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:

1. **Local users** — username + argon2id hash + role in `auth.yaml`.
   Managed from the admin UI (and a `dashglass user` break-glass CLI +
   `DASHGLASS_BOOTSTRAP_ADMIN_PASSWORD` for 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.
2. **Trusted reverse proxy** — `user`/`groups` headers from Authelia,
   Authentik, oauth2-proxy…; no Dashglass session, headers are evaluated
   per request.
3. **Native OIDC** — authorization-code flow, groups claim; lands on the
   same session cookie as local login.
4. **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.**

1. **`runtime` embeds its own metrics query orchestrator**, self-contained
   behind the `PrometheusDataSource` interface: per-panel polling aligned on
   the query `step`, abort when a panel leaves the viewport, sharing of
   identical queries across panels. It is the functional equivalent of
   panel's `promql-data.ts` listener-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.
2. **Each host app keeps its own stack for management CRUD** (dashboards,
   settings, users):
   - the OSS `apps/dashglass` uses **TanStack Query** — no global store to
     maintain for an app this size;
   - `bleemeo-panel` **keeps Redux/RTK unchanged**. When it embeds the
     dashboard engine, it mounts `<DashboardRenderer>` with a
     `PrometheusDataSource` implementation backed by `preview_query`, next to
     its Redux store, with no conflict. Its `promql-data.ts` eventually
     becomes replaceable by the runtime orchestrator.

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.

## 9. SaaS convergence path

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:

1. Dashglass ships standalone with the four registries in place (even while
   only built-ins exist).
2. Panel converts its widget-dispatch ternary and `navigationPages` array
   into registries (an internal refactor, no behavior change).
3. Panel embeds the Dashglass dashboard engine for its dashboard pages, with a
   Bleemeo `PrometheusDataSource` and its proprietary panel kinds.
4. Model convergence: a **bidirectional converter** between the Bleemeo
   widget model (widgets as separate API resources, grid stored per widget,
   `DashboardLayoutJSON` export 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.

## 10. Features

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.

### v1

- Panel kinds: **timeseries**, **stat** (single value + sparkline),
  **gauge**, **table**.
- PromQL editor with highlighting, linting and live autocompletion
  (CodeMirror + `codemirror-promql` wired to the data source).
- Dashboard **variables** (`$instance`, `$job`) fed by `label_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).

### v2+

- 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 log` when 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).

## 11. Build order

1. `packages/model` — schema, serializers, migrations, round-trip tests.
   The file format is the product; it comes first.
2. Go backend — Prometheus proxy, file CRUD + validation, fsnotify watcher +
   SSE, capabilities.
3. `packages/runtime` — the four registries, grid rendering, panel chrome,
   time-range/variables context, metrics orchestrator.
4. `packages/panels-builtin` (reusing panel's ECharts utilities —
   `chart-options.ts`, `line-chart-tools.ts` — as a starting point) +
   PromQL editor.
5. `apps/dashglass` assembled end to end; then, only after stabilization, the
   "panel embeds the engine" proof of concept.

## 12. Open questions

- ~~**License**~~ — **decided (2026-07): Apache-2.0**, adoption is the
  priority. `LICENSE` at 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.

## 13. Feature status

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) | 🚧 |