{"id":49697242,"url":"https://github.com/adiakys/otlpdashboard","last_synced_at":"2026-05-23T23:08:17.982Z","repository":{"id":356158146,"uuid":"1227993474","full_name":"Adiakys/OtlpDashboard","owner":"Adiakys","description":"Self-hosted OTLP receiver and dashboard for traces, logs, and metrics. .NET 10 + Nuxt 4 SPA, with SQLite, PostgreSQL, or SQL Server storage.","archived":false,"fork":false,"pushed_at":"2026-05-19T19:19:54.000Z","size":1348,"stargazers_count":9,"open_issues_count":1,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-19T22:55:18.264Z","etag":null,"topics":["dashboard","logging","monitoring","monitoring-tool","observability","opentelemetry","otlp","traces"],"latest_commit_sha":null,"homepage":"https://adiakys.github.io/OtlpDashboard/","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Adiakys.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-03T12:58:24.000Z","updated_at":"2026-05-19T19:20:11.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/Adiakys/OtlpDashboard","commit_stats":null,"previous_names":["adiakys/otlpdashboard"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/Adiakys/OtlpDashboard","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Adiakys%2FOtlpDashboard","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Adiakys%2FOtlpDashboard/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Adiakys%2FOtlpDashboard/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Adiakys%2FOtlpDashboard/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Adiakys","download_url":"https://codeload.github.com/Adiakys/OtlpDashboard/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Adiakys%2FOtlpDashboard/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33415052,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-23T22:14:44.296Z","status":"ssl_error","status_checked_at":"2026-05-23T22:14:43.778Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dashboard","logging","monitoring","monitoring-tool","observability","opentelemetry","otlp","traces"],"created_at":"2026-05-08T02:27:29.519Z","updated_at":"2026-05-23T23:08:17.975Z","avatar_url":"https://github.com/Adiakys.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# OpenTelemetry Dashboard\n\n\u003e \"Grafana was overwhelming, while Aspire was lacking.\"\n\u003e\n\u003e This dashboard is the \"just right\" middle ground: a lightweight, self-hosted OTLP receiver and viewer designed for those who need professional observability without the setup fatigue.\n\n![OpenTelemetry Dashboard — dashboard, traces, and logs views](docs/screenshots/hero.webp)\n\nA self-hosted OTLP receiver and viewer for traces, logs, and metrics.\n.NET 10 + EF Core backend, Vue 3 / Nuxt 4 SPA. Stores telemetry in SQLite,\nPostgreSQL, or SQL Server. Speaks OTLP gRPC on `:4317` and OTLP\nHTTP/Protobuf on `:4318` (the same port also serves the SPA).\n\n**Live demo:** [adiakys.github.io/OtlpDashboard](https://adiakys.github.io/OtlpDashboard/) — static build with mock telemetry, no backend required. Login accepts any password (e.g. `demo`).\n\n\u003e The demo runs against an in-browser **mock** backend (a tiny TS shim\n\u003e that intercepts every API call and returns generated DTOs) — not the\n\u003e real .NET host. It exists to let people click around without us\n\u003e hosting anything; behaviour can drift from the real server in subtle\n\u003e ways (some flows are stubbed, some validation paths return generated\n\u003e data instead of real errors). For a faithful preview run the docker\n\u003e compose stack below.\n\n---\n\n## Quick start\n\n### Docker — full demo stack\n\nThe bundled `docker-compose.yml` starts the dashboard against Postgres plus a\nsmall demo workload (`sample-server` + `sample-client` + the OpenTelemetry\nCollector) that pushes traces, logs, and metrics so the dashboard isn't empty\non first boot:\n\n```bash\ndocker compose up --build\n# OTLP gRPC : localhost:4317\n# OTLP HTTP : localhost:4318\n# SPA       : http://localhost:4318\n```\n\nSources for the demo workload live under [`demo/`](demo/) — sample app\n(Server / Client), the built-in widget-library pack\n(`demo/packs/default/`, distributed as a separate repo and consumed via\ngit submodule), the demo-stack starter dashboard\n(`demo/packs/default-dashboard/`), and the Collector config. To run only the dashboard against an external Postgres / SQL Server,\nstrip the demo services from your override file and point\n`Dashboard__Storage__Provider` + `ConnectionStrings__*` at your DB.\n\n### Local dev (no Docker)\n\n```bash\n# Backend\ndotnet run --project src/OpenTelemetryDashboard.Host\n\n# Frontend (separate terminal, hot-reload, proxies /api to :4318)\ncd web \u0026\u0026 pnpm install \u0026\u0026 pnpm dev\n```\n\nThe backend runs migrations on every boot, so the SQLite file appears at\n`./src/OpenTelemetryDashboard.Host/data/telemetry.db` on first run.\n\n---\n\n## Configuration\n\nAll settings live under `Dashboard:*` in `appsettings.json` and can be\noverridden by environment variables using double underscores\n(e.g. `Dashboard__BrowserToken`).\n\n### Auth tokens\n\nTwo static bearer tokens gate the surface:\n\n| Variable                       | Protects                                          |\n|--------------------------------|---------------------------------------------------|\n| `DASHBOARD__BROWSERTOKEN`      | The SPA + read API (`/api/v1/...`) + MCP (`/mcp`) |\n| `DASHBOARD__OTLP__APIKEY`      | OTLP ingestion (gRPC + HTTP)                      |\n\n**Posture by environment:**\n\n- **Development** — empty tokens degrade to allow-all so the existing\n  integration tests and local dev keep running without setup.\n- **Production** — missing tokens **fail closed**: the host refuses to\n  start. Either set both tokens, or opt in to public access explicitly\n  with `DASHBOARD__AUTH__ALLOWANONYMOUS=true` (surfaced as `Degraded`\n  on `/healthz` so SREs see the posture).\n\nThe SPA's `/login` form exchanges the password for an HttpOnly +\nSameSite=Strict session cookie via `POST /api/v1/auth/login`; JS never\nsees the token after the exchange. `POST /api/v1/auth/logout` clears\nthe cookie. OTLP clients send the API key as\n`Authorization: Bearer \u003ctoken\u003e` or as the `x-otlp-api-key` header.\n\n### Retention (max days)\n\nBackground job that drops rows older than the configured age. Defaults\ntarget a \"debug + recent forensics\" workload — logs hold a month so\nlast-week incidents stay investigable; traces and metrics rotate\nweekly because high-cardinality spans and split-by metric attributes\ninflate fast.\n\n```jsonc\n\"Dashboard\": {\n  \"TelemetryLimits\": {\n    \"MaxLogDays\": 30,    // default\n    \"MaxTraceDays\": 7,   // default\n    \"MaxMetricDays\": 7,  // default\n    \"SweepIntervalMinutes\": 60\n  }\n}\n```\n\nOr via env vars:\n\n```bash\nDashboard__TelemetryLimits__MaxLogDays=30\nDashboard__TelemetryLimits__MaxTraceDays=7\nDashboard__TelemetryLimits__MaxMetricDays=7\nDashboard__TelemetryLimits__SweepIntervalMinutes=60\n```\n\nA value of `0` disables retention for that signal (records are kept\nindefinitely) — `/healthz` then reports `Degraded` so an unbounded\nwindow can't hide silently. The sweep runs each `SweepIntervalMinutes`\nper signal independently — a failure on one kind doesn't stop the\nothers.\n\n### Storage provider\n\n```bash\nDashboard__Storage__Provider=Sqlite        # default\nConnectionStrings__Sqlite=\"Data Source=/app/data/telemetry.db\"\n\nDashboard__Storage__Provider=PostgreSql\nConnectionStrings__PostgreSql=\"Host=...;Database=telemetry;Username=...;Password=...\"\n\nDashboard__Storage__Provider=SqlServer\nConnectionStrings__SqlServer=\"Server=...;Database=telemetry;User Id=...;Password=...;TrustServerCertificate=True\"\n```\n\n---\n\n## MCP server (read-only)\n\nAn optional [Model Context Protocol](https://modelcontextprotocol.io) server\nexposes the same data as the read API (logs, traces, metrics) so an LLM\nclient — Claude Code, Claude Desktop, MCP Inspector, etc. — can query the\ndashboard directly. **Disabled by default**; set the flag to mount it:\n\n```bash\nDashboard__Mcp__Enabled=true\n```\n\nMounted at `/mcp` on the same port as the SPA (`:4318`) over Streamable HTTP,\ngated by the same browser bearer token (`DASHBOARD__BROWSERTOKEN`) that\nprotects `/api/v1/*`. Tools exposed:\n\n| Tool                  | Mirrors                                |\n|-----------------------|----------------------------------------|\n| `query_logs`          | `GET /api/v1/logs`                     |\n| `list_log_services`   | `GET /api/v1/logs/services`            |\n| `query_traces`        | `GET /api/v1/traces`                   |\n| `get_trace`           | `GET /api/v1/traces/{traceId}`         |\n| `list_trace_services` | `GET /api/v1/traces/services`          |\n| `list_metrics`        | `GET /api/v1/metrics`                  |\n| `query_metric_points` | `GET /api/v1/metrics/points`           |\n| `list_metric_services`| `GET /api/v1/metrics/services`         |\n\nWire it into Claude Code by dropping a `.mcp.json` at your project root:\n\n```json\n{\n  \"mcpServers\": {\n    \"otel-dashboard\": {\n      \"type\": \"http\",\n      \"url\": \"http://localhost:4318/mcp\",\n      \"headers\": { \"Authorization\": \"Bearer ${DASHBOARD_BROWSER_TOKEN}\" }\n    }\n  }\n}\n```\n\nBuilt on the official `ModelContextProtocol.AspNetCore` SDK\n(jointly maintained by Anthropic and Microsoft).\n\n---\n\n## Widgets\n\nDashboards are made of widgets dropped onto a 12-column grid. Three sources:\n\n- **`std`** — built into the bundle (Stat, Line, Sparkline, Gauge, Bar gauge,\n  Pie, Heatmap, Recent traces, Top traces, Logs stream, Text).\n- **`custom`** — preset of a builtin saved by the user, persisted in the DB.\n- **`\u003clibrary\u003e`** — read-only widgets shipped by an installed library\n  (filesystem or installed from a Git repo).\n\nThe picker dialog (`+ Add widget` while editing a dashboard) shows all\nthree groups in the same modal. The search box filters by name,\ndescription, *or* source — type `std`, `custom`, or a library id to filter\na whole bucket.\n\n### Creating a custom widget\n\n1. Open a dashboard, click **Edit**, add a builtin widget.\n2. Configure it (metric binding, thresholds, range, …).\n3. In the config drawer click **Save as widget**, give it a name, icon, and\n   default size.\n\nIt now appears in the picker under **My widgets**, with inline edit and\ndelete buttons that show on hover.\n\nNotes:\n- The seed config is captured *at save time* — editing the template later\n  changes only metadata (name, description, icon, default size). The seed\n  config is immutable; clone + delete to change it.\n- Existing dashboard instances of a deleted custom widget render a\n  placeholder, not a crash.\n\n### Packs (widget libraries + dashboards)\n\nEverything that can ship with the dashboard — widget libraries, built-in\ndashboards, and any future asset type — is distributed as a **pack**: a\nsingle directory with `pack.json` at its root. The dashboard scans every\nentry in `Dashboard:Packs:Paths` (in order) and surfaces every pack's\ncontents in the picker (libraries) and dashboard list (built-ins).\n\nThe shipped image already configures **two paths** in scan order:\n\n1. `/app/data/packs` — runtime-managed (volume, git installs,\n   drag-and-drop)\n2. `/app/builtin-packs` — baked into the image layer (no volume\n   shadowing on rebuild)\n\nDerived images don't need to set any environment variable — just `COPY`\ninto the second path:\n\n```dockerfile\nFROM opentelemetrydashboard:latest\nCOPY my-pack/ /app/builtin-packs/my-pack/\n```\n\nWhen two paths expose packs with the same `pack.json#id`, the first in\nscan order wins and the rest are skipped with a warning — so a runtime\ninstall can override a baked-in default by sharing its id.\n\nTwo sample packs live under `demo/packs/`:\n- `default/` (a submodule pointing at `Adiakys/OtlpDashboard-default-pack`)\n  ships the .NET and PostgreSQL widget libraries.\n- `default-dashboard/` ships the starter dashboard pre-bound to the\n  demo compose stack.\n\nBoth are bind-mounted by `docker-compose.yml`, so `docker compose up --build`\nshows libraries + starter dashboard out of the box.\n\nInstall one of two ways:\n\n1. **Drop a folder.** Copy the pack into the packs path (volume-mount,\n   Ansible, etc.). Click the refresh icon in the widget picker (or\n   `POST /api/v1/packs/reload`) to re-scan.\n2. **Install from a Git repo.** Click the git-branch icon in the picker\n   header (or `POST /api/v1/packs/install` with `{ url, ref, path? }`).\n   The server runs a shallow clone via LibGit2Sharp, parses `pack.json`,\n   resolves HEAD to a commit SHA, and atomically moves the directory\n   into the runtime-managed root. Allowed hosts are\n   `Dashboard:Packs:AllowedGitHosts` (default `github.com, gitlab.com`).\n   Use a tag for stable pinning; branches work but get a UI warning.\n   The optional `path` parameter re-roots the install on a sub-directory,\n   useful for monorepos that ship multiple packs side by side. Updates:\n   the \"Update\" button on git-installed packs re-pulls the same ref\n   (`fetch \u0026\u0026 reset --hard`).\n\n#### Pack layout\n\n```\nmy-pack/\n├── pack.json\n├── README.md            (optional — surfaced in the UI)\n├── LICENSE              (optional)\n├── libraries/\n│   ├── core/\n│   │   ├── manifest.json\n│   │   └── widgets/\n│   │       ├── sla-tracker/widget.json\n│   │       └── trace-heatmap/widget.json\n│   └── extras/\n│       ├── manifest.json\n│       └── widgets/…\n└── dashboards/\n    ├── default.json\n    └── ops-overview.json\n```\n\n`pack.json`:\n\n```json\n{\n  \"id\": \"team-otel-pack\",\n  \"name\": \"Team OTel Pack\",\n  \"version\": \"1.2.0\",\n  \"author\": \"platform@example.com\",\n  \"license\": \"MIT\",\n  \"description\": \"Curated widgets and dashboards for service ownership reviews\",\n  \"homepage\": \"https://github.com/example/team-otel-pack\",\n  \"libraries\": [\n    { \"id\": \"core\",   \"path\": \"libraries/core\" },\n    { \"id\": \"extras\", \"path\": \"libraries/extras\" }\n  ],\n  \"dashboards\": [\n    { \"id\": \"default\",      \"path\": \"dashboards/default.json\", \"builtin\": true },\n    { \"id\": \"ops-overview\", \"path\": \"dashboards/ops-overview.json\" }\n  ]\n}\n```\n\n`id` must match the directory name. Each library entry's\n`manifest.json` carries only `id`, `name`, `description?`, `icon?` —\nshipping metadata (version, author, license) lives at pack level.\nDashboards flagged `builtin: true` are seeded into the dashboard store\non first boot; the rest are installable templates.\n\n#### Writing a widget\n\nEach `widgets/\u003ckindId\u003e/widget.json` declares one widget. Two engines:\n\n**`preset`** — wraps a builtin with a precooked config:\n\n```json\n{\n  \"name\": \"SLA Tracker\",\n  \"description\": \"p99 latency with SLO thresholds\",\n  \"icon\": \"i-ph-target\",\n  \"defaultSize\": { \"w\": 4, \"h\": 3 },\n  \"engine\": \"preset\",\n  \"baseKind\": \"metric-stat\",\n  \"defaultConfig\": {\n    \"calc\": \"last\",\n    \"unitKind\": \"ms\",\n    \"decimals\": 1,\n    \"thresholds\": [\n      { \"value\": 0,   \"color\": \"#7AAA7A\" },\n      { \"value\": 200, \"color\": \"#D9B566\" },\n      { \"value\": 500, \"color\": \"#E27A3F\" }\n    ]\n  }\n}\n```\n\n`baseKind` is one of: `metric-stat`, `metric-line`, `metric-sparkline`,\n`metric-gauge`, `metric-bar-gauge`, `metric-pie`, `metric-heatmap`,\n`recent-traces`, `top-traces`, `logs-stream`, `text`. The shape of `defaultConfig`\nmatches the corresponding form in the SPA — copy from a working instance\n(`Save as widget` produces a valid one).\n\n**`spec`** — sandboxed HTML/SVG/CSS template with named metric bindings.\nUse this for card-style UIs that the standard chart widgets can't\nexpress: a database illustration whose fill level tracks a load\nmetric, a grid of service tiles whose colour follows a per-service\nstatus metric, gauges with custom artwork, etc.\n\n```json\n{\n  \"name\": \"Database card\",\n  \"icon\": \"i-ph-database\",\n  \"defaultSize\": { \"w\": 4, \"h\": 4 },\n  \"engine\": \"spec\",\n  \"spec\": {\n    \"template\": \"\u003cdiv class='db'\u003e\\n  \u003csvg\u003e...\u003c/svg\u003e\\n  \u003cdiv class='stats'\u003e\\n    \u003cstrong class='{{ thresholdClass load.value load.thresholds }}'\u003e{{ format load.value 'percent' 0 }}\u003c/strong\u003e\\n    \u003cspan\u003eQPS {{ format qps.value 'ops' 1 }}\u003c/span\u003e\\n  \u003c/div\u003e\\n\u003c/div\u003e\",\n    \"styles\": \".db { display: flex; gap: 1rem; ... } .db .vellum-th-bad { color: var(--color-rust-600); }\",\n    \"dataBindings\": [\n      { \"name\": \"load\", \"type\": \"metric\", \"calc\": \"last\", \"unitKind\": \"percent\",\n        \"thresholds\": [{ \"value\": 0, \"color\": \"#7AAA7A\" }, { \"value\": 70, \"color\": \"#D9B566\" }, { \"value\": 90, \"color\": \"#E27A3F\" }] },\n      { \"name\": \"qps\",  \"type\": \"metric\", \"calc\": \"mean\", \"unitKind\": \"ops\" }\n    ]\n  }\n}\n```\n\nThe user installing the library picks one instrument per binding\n(`load`, `qps`, …) via the config drawer; the template/styles are\nimmutable and ship with the library.\n\n**Template syntax** (Mustache-light, no JS evaluation):\n\n| Construct | Meaning |\n|---|---|\n| `{{ name }}` / `{{ name.path }}` | Interpolate a value (HTML-escaped). |\n| `{{ helper arg1 arg2 }}` | Call a whitelisted helper. |\n| `{{#if expr}}…{{/if}}` | Render block when truthy. |\n| `{{#each list as item}}…{{/each}}` | Loop, exposes `item` and `_index`. |\n\n**Helpers available**: `format(value, unitKind, decimals?)`,\n`percent(value, min, max)`, `thresholdColor(value, thresholds)`,\n`thresholdClass(value, thresholds)` (returns `vellum-th-ok` / `-warn`\n/ `-bad`), `dateAgo(timestamp)`, `pluralize(n, singular, plural)`,\n`default(...values)`, comparators `eq`/`neq`/`lt`/`lte`/`gt`/`gte`\n(usable inside `{{#if}}`).\n\n**Binding shapes** the template scope sees:\n\n- `metric` (default — `splitBy` not set): `{ value, unit, unitKind, thresholds }`.\n- `metric` with `splitBy: \"service.name\"`: array of `{ key, value, attrs, thresholds }` — iterable with `{{#each}}`.\n- `metric-series`: array of raw `{ time, value, attributes }` rows.\n\n**Sandboxing**:\n\n- The Mustache renderer never `eval`s — only whitelisted helpers run.\n- DOMPurify (lazy-loaded ~50 KB gzip) sanitises the rendered HTML:\n  no `\u003cscript\u003e`, no `on*=`, no `javascript:` URLs, no `\u003ciframe\u003e`.\n- CSS is auto-prefixed with the widget's instance scope so styles\n  can't leak; `@import`, `@font-face`, `expression()`, and IE-era\n  binding tricks are stripped before scoping.\n\nField rules enforced by the loader (`engine: spec`):\n\n- `name` ≤ 64 chars.\n- `description` ≤ 280 chars.\n- `icon` matches `^i-(ph|lucide)-[a-z0-9-]+$`.\n- `defaultSize.w` ∈ [1, 12], `defaultSize.h` ∈ [1, 24].\n- `defaultConfig` ≤ 64 KiB; `spec` (the whole template+styles+bindings\n  envelope) ≤ 256 KiB.\n\nInvalid widgets are skipped (logged) and don't break the rest of the\nlibrary.\n\nThe bundled `demo/packs/default/libraries/postgres/` library ships\n`postgres-server-card` and `db-scoreboard` — two `engine: spec` widgets\nthat double as live examples for authors of the `spec` engine.\n\n---\n\n## Built-in dashboards\n\nDashboards ship inside packs — list them in `pack.json#dashboards` with\n`builtin: true` and the `BuiltinDashboardSeeder` folds each into the\ndashboard store on the first boot after install. The format matches the\none the SPA emits via \"Export JSON\".\n\n### File format\n\nSame envelope produced by the SPA's export, plus an optional top-level\n`id`:\n\n```json\n{\n  \"version\": 1,\n  \"id\": \"00000000-0000-0000-0000-000000000001\",\n  \"name\": \"Production Overview\",\n  \"widgets\": [\n    { \"id\": \"...\", \"kind\": \"std:metric-stat\", \"x\": 0, \"y\": 0, \"w\": 4, \"h\": 3, \"config\": { /* opaque */ } }\n  ]\n}\n```\n\n### Id resolution (precedence)\n\n1. **Explicit `id`** in the JSON wins, must parse as a Guid.\n2. **`default.json`** filename → `00000000-0000-0000-0000-000000000001`\n   (the well-known default dashboard id). Convenient for distributing a\n   non-empty default.\n3. **Any other filename** → SHA-256 of the filename, truncated to 16\n   bytes (RFC 9562 v8). Stable across deployments — same filename\n   always produces the same dashboard id.\n\n### Idempotency\n\nThe seeder runs every boot but uses **skip-if-exists** semantics: an id\nalready in the store is left alone. The single special case is the\ndefault dashboard — when `default.json` is present and the existing\ndefault row is still pristine (no widgets, RowVersion 0), the seeder\nupserts it once. Any user edit (saved widgets or RowVersion ≥ 1) makes\nthe seeder back off.\n\nTo re-apply a built-in file, delete the dashboard via the UI first.\n\nA demo lives at `demo/packs/default-dashboard/dashboards/default.json`\n(referenced from `default-dashboard/pack.json` with `builtin: true`),\nmounted by `docker-compose.yml` as `/app/builtin-packs/default-dashboard`.\n\n---\n\n## Endpoints\n\n| Method | Path                                  | Purpose                       |\n|--------|---------------------------------------|-------------------------------|\n| GET    | `/healthz`                            | Liveness + readiness probe (anonymous; checks DB, sink, retention, auth posture) |\n| POST   | `/v1/{traces,logs,metrics}`           | OTLP HTTP/Protobuf ingestion  |\n| POST   | `/api/v1/auth/login`, `/logout`       | Session-cookie login / logout (anonymous) |\n| GET    | `/api/v1/info`                        | App name (anonymous); version + retention + storage provider when authenticated |\n| GET    | `/api/v1/traces`, `/logs`, `/metrics` | Query API (paginated). `/metrics/points` requires `from`/`to` |\n| GET/POST/PUT/DELETE | `/api/v1/dashboards`     | Dashboards CRUD. `DELETE` requires `?rowVersion=N` (optimistic concurrency) |\n| GET/POST/PUT/DELETE | `/api/v1/widgets/definitions` | Custom widgets CRUD. `DELETE` requires `?rowVersion=N` |\n| GET    | `/api/v1/widgets/libraries`           | Flat library list for the picker |\n| GET    | `/api/v1/packs`                       | Installed packs (libraries + dashboards) |\n| POST   | `/api/v1/packs/reload`                | Re-scan the packs paths       |\n| POST   | `/api/v1/packs/install`               | Clone from git (`{url,ref,path?}`) |\n| POST   | `/api/v1/packs/{id}/update`           | Re-pull a git-installed pack  |\n| DELETE | `/api/v1/packs/{id}`                  | Uninstall a pack              |\n| POST/GET/DELETE | `/mcp`                       | MCP server (when `Dashboard:Mcp:Enabled`) |\n| (boot) | seed dashboards from packs            | `builtin: true` dashboards loaded after migrations, idempotent |\n\nErrors return RFC 7807 `application/problem+json` with a `traceId`\nextension for log correlation. Per-IP rate limits apply to OTLP ingest\n(200 req/s default), the read API (60 req/s), mutations (10 req/s),\nand pack install (1 concurrent globally) — tunable under\n`Dashboard:RateLimits:*`.\n\n---\n\n## Acknowledgements\n\nClaude Code did a lot of the typing on this project. The direction, design,\ntesting, and debugging stayed on me.\n\n---\n\n## License\n\n[GPLv3](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadiakys%2Fotlpdashboard","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fadiakys%2Fotlpdashboard","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadiakys%2Fotlpdashboard/lists"}