# Panel Desk > Paste an exported Grafana dashboard JSON model and work it four ways in one sitting: a > production review of the board, a query audit of every panel target, a portability pass for > importing it into another environment, and a cost pass for what it charges you to keep open. > A free in-browser reader parses the model first and writes a patched `dashboard.json`. URL: https://panel-desk.skillsafe.ai/ API docs: https://panel-desk.skillsafe.ai/api.html Token panel: https://panel-desk.skillsafe.ai/tokens.html ## What the work object is One **exported Grafana dashboard JSON model** — the document behind *Dashboard settings → JSON Model*, an *Export for sharing externally* file, or the `dashboard` object out of a dashboard API response. Panel Desk unwraps the last two automatically. Nothing else is required: the panels, the targets, the templating list and the time settings are all inside that one document. ## What runs without an account, and free The whole reader. It is deterministic JavaScript in the visitor's browser — no model, no account, no upload: - The model is parsed and, when it is a sharing wrapper or an API response, unwrapped. - Every panel is walked, **including the panels nested inside collapsed rows**, with its type, id, title, datasource, unit, threshold count, `gridPos`, `repeat`, `maxDataPoints` and target list. - Every target expression is collected and checked against a fixed catalogue of PromQL defects: a literal range selector where `$__rate_interval` belongs, `rate()` over a metric that is not a counter, an unbounded selector, an aggregation with no `by`/`without`, a group-by naming a high-cardinality label, a quantile that is not taken over a rate of buckets, subqueries, `topk` inside a panel, and regex matchers that match everything. - Every template variable is listed with whether it is **actually referenced** anywhere. - Every literal datasource uid is found and named with the panel and target that pins it. This is the single most common reason an exported dashboard will not import into another environment. - The 24-column layout grid is reconstructed as a wireframe, with ragged rows, overlaps and gaps. - The query budget is computed: `targets x (60 / refresh seconds)` per minute and per day, from the dashboard's own `refresh` and `time.from`, with the repeat multiplier called out separately as a factor the unrepeated floor does not include. - Credential-shaped values are found — a `Bearer` prefix, a `secureJsonData` block, a long token-shaped string — because a dashboard JSON is routinely pasted into tickets and committed. - A **patched `dashboard.json`** is written for the repairs that have exactly one correct answer: datasource pins replaced with a `${DS_PROMETHEUS}` variable (and the variable added when absent), root `id` nulled, sharing wrapper and snapshot data stripped, legacy per-panel `alert` objects deleted, duplicate panel ids renumbered, `graph` migrated to `timeseries` and `singlestat` to `stat`, bare-string datasources converted to the object form, credentials redacted, and a fixed `timezone` set back to `browser`. Everything ambiguous is listed as deliberately left alone, with the reason. - Exports: the read as Markdown, a panel inventory CSV, a query audit CSV, a flags CSV and the patched JSON. Each flag carries a `DS-nnn` id. Those ids are handed to the model as facts it must reconcile one by one, and the reconciliation is shown in the result — an unaccounted flag is displayed as unaccounted. ## The four lanes (metered, signed-in users only) One system prompt, one model (`gpt-terra`, currently `gpt-5.6-terra`), routed on an explicit `task` field. Each lane returns the same outer envelope plus its own block. | `task` | Question | Verdicts | Lane block | | --- | --- | --- | --- | | `review` | Is this board production-ready as a board? | `ship` / `rework` / `rebuild` | `blocking` | | `queries` | Is each target expression correct, and what does it cost? | `sound` / `tune` / `unsafe` | `queries` | | `portable` | Will this export import somewhere else and come up working? | `portable` / `needs-work` / `pinned` | `provision` | | `cost` | What does this dashboard charge you to keep open? | `lean` / `trim` / `expensive` | `budget` | Shared envelope: `lane`, `dashboard_name`, `verdict`, `headline`, `overview`, `checks[]` (`{name, status, note}` with status `pass|warn|fail|n/a`), `findings[]` (`{id, severity, title, where, why, fix, snippet}` with severity `critical|high|medium|low` and ids `PD-001`…), `coverage_check[]` (`{flag, handled, note}` — one entry per `DS-nnn` flag), and `next_steps[]`. Each lane returns exactly **ten named checks**, in a fixed order, so a check the model drops is rendered as missing rather than shortening the table. Handoff is a button: each result carries its own conclusions into the next lane's `carryover` and switches lanes without a reload. `review → queries → cost → portable → review`. ## What it will not do - It does not connect to your Grafana, your Prometheus or your Mimir. Everything is the document you pasted; every number is arithmetic over that document, never a measurement of your stack. - It does not run the queries. Cardinality statements are about the *shape* of an expression, not a series count read from a TSDB. - It does not invent panels, targets, variables or metric names. The prompt forbids it and the reconciliation makes an unsupported claim visible. - It does not price in currency unless you state a rate — the cost lane's unit is query volume. ## Sources Panel Desk is a derived work built on four agent skills, one per lane: - `@wshobson/grafana-dashboards` — build and maintain production Grafana dashboards (the `review` lane) - `@grafana/prometheus-cardinality-troubleshooter` — diagnose slow queries, OOMs and active-series bills (the `queries` lane) - `@grafana/datasource-provisioning` — datasource and dashboard provisioning as code (the `portable` lane) - `@grafana/cost-management` — cost monitoring, attribution and optimization (the `cost` lane) Grafana, Prometheus, Loki and Mimir are trademarks of their respective owners. Panel Desk is not affiliated with or endorsed by Grafana Labs. ## Driving it from code Base URL `https://api.skillsafe.ai/v1/app-api`. The request body for `/estimate`, `/run` and `/run-stream` **is the input object itself** — there is no `input` wrapper and no app-slug header; the slug is carried by the token. `/me` and `/estimate` work with a guest token; `/run` needs a personal token because the app is metered and does not sponsor guests. Pass an `Idempotency-Key` derived from `(task, dashboard, attempt)` so a retry cannot double-bill. Full worked examples in eight languages are on `/api.html`.