@carllee1983/dbcli 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,17 @@ All notable changes to dbcli are documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.18.0] - 2026-05-11
9
+
10
+ ### Added
11
+
12
+ - **Interactive HTML Dashboards**: `query`, `q`, and `export` can now render results as fully interactive, standalone HTML reports.
13
+ - New `--ui` flag to open dashboards directly in the system browser.
14
+ - New `html` format for `stdout` and file-based report generation.
15
+ - Snippet `visual:` block in frontmatter for KPI and chart configuration (Line, Bar, Area, Pie, Scatter).
16
+ - Secure payload injection with automatic HTML escaping and blacklist redaction.
17
+ - Bundled React + Recharts + Tailwind UI template for zero-dependency portability.
18
+
8
19
  ## [1.17.0] - 2026-05-10
9
20
 
10
21
  ### Added
package/README.md CHANGED
@@ -133,6 +133,25 @@ The planner is cache-first (no network); add `--probe` to refresh the
133
133
  underlying inspect context. Use `--list` to see all goals,
134
134
  `--format markdown` for human reading, or `--for-agent` for compact JSON.
135
135
 
136
+ ### Interactive HTML Dashboards
137
+
138
+ ```bash
139
+ # Open results in browser
140
+ dbcli query "SELECT * FROM orders" --ui
141
+
142
+ # Run a saved snippet with visualization metadata
143
+ dbcli q @analytics/revenue --ui
144
+
145
+ # Export query results as a standalone HTML file
146
+ dbcli export "SELECT * FROM users" --format html --output report.html
147
+ ```
148
+
149
+ `dbcli` can render query results as fully interactive, standalone HTML dashboards. These reports are powered by React + Recharts and are zero-dependency — the entire application and data are inlined into a single HTML file.
150
+
151
+ - **`--ui` flag**: Automatically generates a temporary report and opens it in your default browser.
152
+ - **`visual:` block**: Snippet frontmatter can define KPIs and charts (Line, Bar, Area, Pie, Scatter) to drive the dashboard.
153
+ - **Security**: Result sets are redacted by the blacklist before injection, and data is safely escaped for HTML.
154
+
136
155
  ### Recovery & Guided Remediation
137
156
 
138
157
  ```bash
package/README.zh-TW.md CHANGED
@@ -118,6 +118,25 @@ dbcli query '{"status":"active"}' --collection users --use atlas
118
118
 
119
119
  對 MongoDB 而言,`list` 與 `query` 會使用該連線設定中的資料庫;`query` 也必須指定 `--collection <名稱>`。
120
120
 
121
+ ### 互動式 HTML 儀表板 (Interactive HTML Dashboards)
122
+
123
+ ```bash
124
+ # 在瀏覽器中開啟查詢結果
125
+ dbcli query "SELECT * FROM orders" --ui
126
+
127
+ # 執行具備視覺化中繼資料的片段
128
+ dbcli q @analytics/revenue --ui
129
+
130
+ # 將結果匯出為獨立的 HTML 檔案
131
+ dbcli export "SELECT * FROM users" --format html --output report.html
132
+ ```
133
+
134
+ `dbcli` 可將查詢結果算繪為完全互動、獨立的 HTML 儀表板。這些報表由 React + Recharts 驅動且具備「零依賴」特性 — 整個應用程式與資料皆被內嵌於單一 HTML 檔案中。
135
+
136
+ - **`--ui` 旗標**:自動產生暫時性報表並在預設瀏覽器中開啟。
137
+ - **`visual:` 區塊**:可在查詢片段的 frontmatter 中定義 KPI 與圖表 (Line, Bar, Area, Pie, Scatter)。
138
+ - **安全性**:資料在注入前會先經過黑名單過濾,且針對 HTML 進行了安全跳脫處理。
139
+
121
140
  ---
122
141
 
123
142
  ## 多重連線支援 (v2)
package/assets/SKILL.md CHANGED
@@ -188,13 +188,13 @@ Full flags and edge cases: see [reference.md](reference.md) `init` section.
188
188
  | `use` | n/a | Show/switch default named connection (v2 only). |
189
189
  | `list` | query-only+ | Tables (SQL), collections (MongoDB), keys (Redis), or indices (Elasticsearch). |
190
190
  | `schema` | query-only+ | SQL: per-table or full scan into `.dbcli/schemas/`. MongoDB: sampled. ES: flattened mapping. Redis: per-key only (type/TTL/size). Supports `--recovery`. |
191
- | `query` | query-only+ | SQL, Mongo JSON (`--collection`), Redis command, or ES DSL/Lucene (`--collection`). Supports `--recovery`. |
191
+ | `query` | query-only+ | SQL, Mongo JSON (`--collection`), Redis command, or ES DSL/Lucene (`--collection`). `--format table\|json\|csv\|html`, `--ui` to open the interactive dashboard in a browser. Supports `--recovery`. |
192
192
  | `plan` | n/a | Static SQL risk analyzer (`--format text\|json`); classifies a statement without connecting to the database. |
193
- | `q` | query-only+ | Run a saved snippet by `@name` with `--param k=v`. SQL / Elasticsearch DSL / read-only Redis bodies; blacklist enforced. Supports `--recovery`. |
193
+ | `q` | query-only+ | Run a saved snippet by `@name` with `--param k=v`. SQL / Elasticsearch DSL / read-only Redis bodies; blacklist enforced. `--format table\|json\|csv\|html`, `--ui` to open the interactive dashboard. Supports `--recovery`. |
194
194
  | `queries` | n/a | Manage saved snippets: `list` / `show` / `search` / `suggest` / `new` / `edit` / `check` / `delete` / `rename` / `copy` / `import` / `export`. |
195
195
  | `insert` / `update` | read-write+ | SQL or MongoDB only. JSON `--data` / `--set`; `--where` required on `update`; `--dry-run` first. Redis writes go through `query`. Supports `--recovery`. |
196
196
  | `delete` | data-admin+ | SQL or MongoDB only. `--where` required; `--dry-run` first. Supports `--recovery`. |
197
- | `export` | query-only+ | SQL or MongoDB only. Query → CSV/JSON(L) file or stdout. Supports `--recovery`. |
197
+ | `export` | query-only+ | SQL or MongoDB only. Query → `--format json\|jsonl\|csv\|html` file or stdout. `html` emits a standalone interactive dashboard. Supports `--recovery`. |
198
198
  | `blacklist` | n/a | `list` / `table` / `column` subcommands redact sensitive data from query results. |
199
199
  | `check` | query-only+ | SQL only (best on MySQL/MariaDB). |
200
200
  | `diff` | query-only+ | SQL only. Save/compare schema snapshots. |
@@ -285,8 +285,9 @@ Manage local snippets with `queries new | edit | delete | rename | copy | import
285
285
  local layer for editing.
286
286
 
287
287
  Each `.sql` file may declare YAML frontmatter inside `-- ---` blocks
288
- (name, description, engine, params, tags). See `dbcli queries show @<name> --format json`
289
- for the machine-readable contract.
288
+ (name, description, engine, params, tags, optional `intent`, optional `visual`).
289
+ The `visual:` block drives the interactive dashboard (see "Interactive HTML dashboard"
290
+ below). See `dbcli queries show @<name> --format json` for the machine-readable contract.
290
291
 
291
292
  ### Engine-specific bodies
292
293
 
@@ -321,6 +322,58 @@ Engine variants are picked automatically based on the active connection.
321
322
  Override any of them by placing a same-named file under `.dbcli-shared/queries/`
322
323
  or `.dbcli/queries/`.
323
324
 
325
+ ## Interactive HTML dashboard
326
+
327
+ `query`, `q`, and `export` can render results as a standalone, self-contained HTML
328
+ report powered by a bundled React + Recharts template (`assets/ui-template.html`,
329
+ injected via a hardened `window.__DBCLI_PAYLOAD__ = {...}` block — `<` is escaped
330
+ to neutralise `</script>` payloads).
331
+
332
+ ```bash
333
+ # Open in browser (writes to a temp file, then `open`/`xdg-open`/`start`)
334
+ dbcli query "SELECT day, dau FROM dau_daily" --ui
335
+ dbcli q @analytics/revenue --param days=30 --ui
336
+
337
+ # Pipe HTML to stdout (CI artifacts, email, static hosting)
338
+ dbcli query "SELECT * FROM orders" --format html > orders.html
339
+
340
+ # Export to a file (interchangeable with json/jsonl/csv)
341
+ dbcli export "SELECT * FROM orders" --format html --output orders.html
342
+ ```
343
+
344
+ `--ui` implies `--format html` and opens the file; `--format html` alone prints to
345
+ stdout. Blacklist redaction is applied **before** rendering — the dashboard never
346
+ sees masked columns.
347
+
348
+ ### Snippet `visual:` block
349
+
350
+ To get KPIs and charts (rather than just a sortable table), add a `visual:` block
351
+ to the snippet's frontmatter. Column names must exist in the result row.
352
+
353
+ ```sql
354
+ -- ---
355
+ -- name: Revenue Trend
356
+ -- engine: postgres
357
+ -- params:
358
+ -- days: { type: int, default: 30 }
359
+ -- visual:
360
+ -- title: Revenue (last :days days)
361
+ -- kpis:
362
+ -- - { label: Total Revenue, value_column: total_revenue, format: currency }
363
+ -- - { label: Orders, value_column: order_count, format: number }
364
+ -- - { label: Conversion, value_column: conv_rate, format: percent }
365
+ -- charts:
366
+ -- - { type: line, title: Daily Revenue, x: day, y: [revenue] }
367
+ -- - { type: bar, title: By Channel, x: channel, y: [revenue, refunds] }
368
+ -- ---
369
+ SELECT ...
370
+ ```
371
+
372
+ - `kpis[].format`: `currency` / `number` / `percent` (omit for raw value).
373
+ - `charts[].type`: `line` / `bar` / `area` / `pie` / `scatter`.
374
+ - Raw `query` invocations (no snippet) render a sortable/filterable table only —
375
+ there is no `visual:` to attach.
376
+
324
377
  ## Common workflows
325
378
 
326
379
  - **Debug odd state:** `schema` → `check` → `query` with tight `WHERE` → follow FKs from schema JSON. Evidence over theory.
@@ -135,9 +135,13 @@ dbcli query "DEL stale:key" # requires data-admin+
135
135
  # Elasticsearch: DSL body or Lucene q-string
136
136
  dbcli query '{"query":{"match":{"status":"active"}}}' --collection orders
137
137
  dbcli query 'status:active AND amount:>100' --index orders --limit 50
138
+
139
+ # Interactive HTML dashboard (see "Interactive HTML dashboard" below)
140
+ dbcli query "SELECT day, dau FROM dau_daily" --ui # open in browser
141
+ dbcli query "SELECT * FROM orders" --format html > orders.html # pipe to stdout
138
142
  ```
139
143
 
140
- **Options:** `--format <table|json|csv>`, `--limit <number>`, `--no-limit`, `--collection <name>` (MongoDB / Elasticsearch), `--index <name>` (Elasticsearch alias for `--collection`)
144
+ **Options:** `--format <table|json|csv|html>`, `--ui` (open the dashboard in the system browser; implies `--format html`), `--limit <number>`, `--no-limit`, `--collection <name>` (MongoDB / Elasticsearch), `--index <name>` (Elasticsearch alias for `--collection`), `--recovery`
141
145
  **Permission:** query-only+ (Redis: per-command; Elasticsearch: per HTTP method/path)
142
146
 
143
147
  > **MongoDB notes:**
@@ -156,6 +160,31 @@ dbcli query 'status:active AND amount:>100' --index orders --limit 50
156
160
  > - Hits are flattened: each result row contains `_id` plus dotted-path fields from `_source`. Pass `--format json` to keep nested structures readable.
157
161
  > - Query-only mode caps at 1000 hits; `--no-limit` is internally capped at 10 000 (use saved searches / `search_after` for deeper pagination).
158
162
 
163
+ ### plan
164
+
165
+ Static SQL risk analyzer. Classifies a statement into the same permission tiers
166
+ used by `query` (`query-only` / `read-write` / `data-admin` / `admin`) and lists
167
+ the underlying signals (DML / DDL / multi-statement / unsafe constructs) without
168
+ ever connecting to the database.
169
+
170
+ ```bash
171
+ dbcli plan "SELECT * FROM users"
172
+ dbcli plan "UPDATE users SET name='x'" # human-readable text classification
173
+ dbcli plan "DROP TABLE users" --format json # machine-readable risk report
174
+ ```
175
+
176
+ **Options:** `--format <text|json>` (default `text`).
177
+ **Permission:** n/a (offline analyzer; no connection opened).
178
+
179
+ Use cases:
180
+ - Agents that want to decide whether to call `query` vs `insert` / `update` /
181
+ `delete` before sending SQL.
182
+ - Pre-flight safety check before binding parameters into a saved snippet.
183
+ - Lint hook for code review pipelines that store SQL in source.
184
+
185
+ `plan` does not enforce blacklist or auto-`LIMIT`; those still apply when the
186
+ SQL is actually executed via `query` / `q`.
187
+
159
188
  ### q
160
189
 
161
190
  Run a saved query snippet by `@name`. Snippets are parameterised SELECT/WITH statements resolved from three layers, with **local > shared > builtin** precedence (a local file always shadows shared and builtin variants of the same key):
@@ -172,15 +201,19 @@ dbcli q @dau --param days=30 --format json # override a param
172
201
  dbcli q @analytics/revenue --param-file params.json
173
202
  dbcli q @dau --dry-run # show final SQL + bind values
174
203
  dbcli q @dau --no-limit # disable size guard wrap
204
+ dbcli q @analytics/revenue --param days=30 --ui # open dashboard
205
+ dbcli q @analytics/revenue --param days=30 --format html > report.html
175
206
  ```
176
207
 
177
208
  **Options:**
178
- - `--format <table|json|csv>` — output format (default: `table`)
209
+ - `--format <table|json|csv|html>` — output format (default: `table`)
210
+ - `--ui` — open the rendered HTML dashboard in the system browser (implies `--format html`; writes to a temp file then invokes `open` / `xdg-open` / `start`)
179
211
  - `--param <key=value>` — pass a parameter (repeatable)
180
212
  - `--param-file <path>` — JSON object whose keys are param names
181
213
  - `--no-limit` — skip the `SELECT * FROM (…) AS _dbcli_guard LIMIT 1000` wrap
182
214
  - `--dry-run` — print the bound SQL + values without executing
183
215
  - `--use <name>` — pick a v2 named connection
216
+ - `--recovery` — emit a `RecoveryEnvelope` on failure (see `recover`)
184
217
 
185
218
  **Permission:** query-only+
186
219
 
@@ -201,6 +234,13 @@ Each `.sql` file is plain SQL with optional YAML frontmatter inside a leading `-
201
234
  -- description: lookback window in days
202
235
  -- enum: [7, 30, 90]
203
236
  -- tags: [analytics]
237
+ -- intent: perf.slow-query # optional; consumed by `queries suggest`
238
+ -- visual: # optional; consumed by `--ui` / `--format html`
239
+ -- title: Daily Active Users
240
+ -- kpis:
241
+ -- - { label: DAU, value_column: dau, format: number }
242
+ -- charts:
243
+ -- - { type: line, title: DAU trend, x: day, y: [dau] }
204
244
  -- ---
205
245
  SELECT COUNT(DISTINCT user_id) AS dau
206
246
  FROM events
@@ -209,6 +249,8 @@ WHERE created_at > NOW() - (:days || ' days')::interval;
209
249
 
210
250
  Param placeholders use `:name`. They are rewritten to `$1, $2, …` (Postgres) or `?, ?, …` (MySQL) at execution time and passed as bind values — string interpolation is never used.
211
251
 
252
+ The `visual:` block is documented in detail under [Interactive HTML dashboard](#interactive-html-dashboard) below. Unknown / malformed fields are silently dropped at parse time; the snippet still runs and the dashboard falls back to a sortable table.
253
+
212
254
  #### Param type coercion
213
255
 
214
256
  | Declared `type` | Accepts |
@@ -299,6 +341,10 @@ dbcli queries list --tag analytics --engine postgres --format json
299
341
  dbcli queries list --source local # only personal overrides
300
342
  dbcli queries show @dau # frontmatter + SQL
301
343
  dbcli queries show @dau --format json # MCP-shaped contract
344
+ dbcli queries search slow query # fuzzy-ranked keyword search across snippets
345
+ dbcli queries search cache --engine postgres --source builtin --limit 5
346
+ dbcli queries suggest perf # browse snippets by intent prefix (v1.11+)
347
+ dbcli queries suggest perf.cache-hit --format json
302
348
 
303
349
  # Authoring
304
350
  dbcli queries new @new/sample # scaffold under .dbcli-shared/queries/
@@ -319,8 +365,10 @@ dbcli queries export @dau --output dau.sql # write snippet body to a fi
319
365
  dbcli queries export @diag/connections --engine postgres # pick a variant when multiple engines exist
320
366
  ```
321
367
 
322
- **`list` options:** `--format <table|json|csv>`, `--tag <tag>`, `--engine <postgres|mysql>`, `--source <local|shared>`
368
+ **`list` options:** `--format <table|json|csv>`, `--tag <tag>`, `--engine <postgres|mysql|redis|elasticsearch|all>`, `--source <local|shared|builtin|all>`
323
369
  **`show` options:** `--format <table|json|csv>`
370
+ **`search` options:** `--format <table|json>`, `--engine <postgres|mysql|redis|elasticsearch|all>`, `--source <local|shared|builtin|all>`, `--limit <n>` (default 10), `--include-internal` (show fuzzy ranking score). Keyword(s) are fuzzy-matched against name, description, tags, intent.
371
+ **`suggest` options:** `--format <table|json>`, `--engine <postgres|mysql|redis|elasticsearch|all>`, `--source <local|shared|builtin|all>`. Intent prefix-matched against the snippet's `intent` frontmatter field. Common intents: `perf.slow-query`, `perf.cache-hit`, `capacity.size`, `safety.connections`, `monitor.cluster-health`.
324
372
  **`new` options:** `--local`, `--edit`
325
373
  **`edit` options:** `--shared`
326
374
  **`check` options:** `--strict`, `--format <table|json|csv>`
@@ -378,11 +426,15 @@ Export query results to file or stdout.
378
426
  dbcli export "SELECT * FROM users" --format csv --output users.csv
379
427
  dbcli export "SELECT * FROM users" --format csv --output users.csv --force # Skip overwrite confirmation
380
428
  dbcli export "SELECT * FROM users" --format json | jq '.[]'
429
+ dbcli export "SELECT * FROM users" --format jsonl --output users.ndjson
430
+ dbcli export "SELECT * FROM orders" --format html --output orders.html # standalone dashboard
381
431
  ```
382
432
 
383
- **Options:** `--format <json|csv>` (required), `--output <path>`, `--force`
433
+ **Options:** `--format <json|jsonl|csv|html>` (required), `--output <path>`, `--force`, `--recovery`
384
434
  **Permission:** query-only+
385
435
 
436
+ The `html` format emits the same self-contained dashboard as `query --ui` (see [Interactive HTML dashboard](#interactive-html-dashboard)). Because `export` runs raw SQL (no snippet metadata), the HTML report is always rendered as a sortable / filterable table — no KPIs or charts. Use `dbcli q @<name> --format html` (or `--ui`) for the charted view.
437
+
386
438
  ### blacklist
387
439
 
388
440
  Manage sensitive data blacklist to prevent AI access to restricted tables/columns.
@@ -850,6 +902,32 @@ dbcli migrate drop-enum status --execute --force
850
902
 
851
903
  **AI agent note:** Always use dry-run first (no `--execute`) to preview generated SQL. Only add `--execute` after confirming the SQL is correct. For DROP operations, both `--execute` and `--force` are required.
852
904
 
905
+ ### skill
906
+
907
+ Emit `SKILL.md` (and the companion `reference.md`) to stdout, a file, or one of
908
+ four AI-agent platform directories. The skill is the source of truth that lets
909
+ Claude Code / Gemini / Copilot / Cursor know how to drive dbcli safely.
910
+
911
+ ```bash
912
+ dbcli skill # print SKILL.md to stdout
913
+ dbcli skill --output ./SKILL.md # write to a file (no platform install)
914
+ dbcli skill --install claude # install to ~/.claude/skills/dbcli/
915
+ dbcli skill --install gemini # install to ~/.gemini/skills/dbcli/
916
+ dbcli skill --install copilot # install to .github/skills/dbcli/ (repo-local)
917
+ dbcli skill --install cursor # install to .cursor/skills/dbcli/ (repo-local)
918
+ ```
919
+
920
+ **Options:**
921
+ - `--install <platform>` — `claude` | `gemini` | `copilot` | `cursor`. Writes `SKILL.md` plus `reference.md` next to it so the agent gets progressive disclosure.
922
+ - `--output <path>` — write `SKILL.md` to a file instead of stdout. Does not install `reference.md`.
923
+
924
+ **Notes:**
925
+ - Both files come straight from `assets/SKILL.md` + `assets/reference.md` inside the dbcli package — no runtime rendering. Keep these in sync when shipping a release.
926
+ - `claude` / `gemini` install paths are user-global; `copilot` / `cursor` are repo-local under `.github/` / `.cursor/`.
927
+ - Re-running `--install` overwrites the existing skill atomically; no prompt.
928
+
929
+ **Permission:** n/a.
930
+
853
931
  ### skill tasks (Agent Task Packs)
854
932
 
855
933
  ```bash
@@ -877,6 +955,108 @@ Task storage layers:
877
955
  Higher tiers override lower tiers by task name. Task name is derived from the
878
956
  file path under the tier root (e.g. `diag/inspect.md` → `diag/inspect`).
879
957
 
958
+ ## Interactive HTML dashboard
959
+
960
+ `query`, `q`, and `export` can render results as a single, fully self-contained
961
+ HTML file backed by a bundled React + Recharts template. The template lives at
962
+ `assets/ui-template.html` and is installed alongside the binary; no external
963
+ network, CDN, or runtime is required to view the report.
964
+
965
+ ### Entry points
966
+
967
+ | Command form | Behaviour |
968
+ |--------------|-----------|
969
+ | `dbcli query "<sql>" --ui` | Render to a temp file under `$TMPDIR/dbcli-query-<ts>.html` and open with `open` / `xdg-open` / `start`. |
970
+ | `dbcli q @<name> --ui` | Same, with snippet metadata (`name`, `description`, `visual:` block). |
971
+ | `dbcli query "<sql>" --format html` | Print HTML to stdout (pipe, redirect, attach). |
972
+ | `dbcli q @<name> --format html` | Same, snippet-aware. |
973
+ | `dbcli export "<sql>" --format html --output report.html` | Write HTML to an explicit path; respects `--force` / overwrite confirmation. |
974
+
975
+ `--ui` is a convenience flag — it implies `--format html` and then opens the
976
+ file. `--ui` and `--format` are mutually compatible; passing both is allowed and
977
+ behaves as `--ui`.
978
+
979
+ ### Data injection contract
980
+
981
+ The template ships with a single placeholder, `/*DBCLI_PAYLOAD*/`, which dbcli
982
+ replaces with:
983
+
984
+ ```js
985
+ window.__DBCLI_PAYLOAD__ = { "meta": {...}, "rows": [...] };
986
+ ```
987
+
988
+ Hardening rules applied before injection:
989
+
990
+ - Payload is `JSON.stringify(...)`-encoded.
991
+ - Every `<` is replaced with `<` so a malicious row containing `</script>`
992
+ cannot terminate the inline script tag.
993
+ - Blacklist redaction (`dbcli blacklist`) runs **before** the formatter — masked
994
+ columns never reach the dashboard.
995
+ - The replacement uses a function callback (`html.replace(..., () => injection)`)
996
+ so `$&`-style backreferences in the payload are not interpreted.
997
+
998
+ ### `meta` shape
999
+
1000
+ `meta` is the `SavedQueryMeta` object (see `dbcli queries show @<name> --format json`):
1001
+
1002
+ ```jsonc
1003
+ {
1004
+ "name": "Revenue Trend", // display title
1005
+ "key": "@analytics/revenue", // snippet key, or "raw-sql" / "export"
1006
+ "description": "...", // free text (SQL preview for raw query)
1007
+ "params": [...], // ParamSpec[]
1008
+ "tags": ["analytics"],
1009
+ "intent": "perf.slow-query",
1010
+ "visual": { ... } // optional, see below
1011
+ }
1012
+ ```
1013
+
1014
+ For raw `query` / `export` invocations, `meta.params` is `[]` and
1015
+ `meta.visual` is absent — the dashboard renders a sortable / filterable table.
1016
+
1017
+ ### `visual:` block (snippet frontmatter)
1018
+
1019
+ ```yaml
1020
+ visual:
1021
+ title: Revenue (last :days days) # optional override of meta.name
1022
+ kpis:
1023
+ - label: Total Revenue
1024
+ value_column: total_revenue # must exist in result rows
1025
+ format: currency # currency | number | percent (optional)
1026
+ - label: Orders
1027
+ value_column: order_count
1028
+ format: number
1029
+ charts:
1030
+ - type: line # line | bar | area | pie | scatter
1031
+ title: Daily Revenue
1032
+ x: day # column for X axis
1033
+ y: [revenue] # 1..N columns for series
1034
+ - type: bar
1035
+ title: By Channel
1036
+ x: channel
1037
+ y: [revenue, refunds]
1038
+ ```
1039
+
1040
+ Parser behaviour (`src/core/saved-queries/parser.ts::normaliseVisual`):
1041
+
1042
+ - The block is **optional**. Missing → table-only render.
1043
+ - Items missing required fields (`kpi.label` + `kpi.value_column`, or
1044
+ `chart.type` + `chart.x` + `chart.y[]`) are silently dropped.
1045
+ - Unknown `format` / `type` values are forwarded as strings; the dashboard
1046
+ decides how to render them (unknown chart types fall back gracefully).
1047
+ - The snippet still executes as a normal SQL/DSL query — `visual:` only affects
1048
+ the HTML renderer.
1049
+
1050
+ ### Limitations
1051
+
1052
+ - The dashboard is read-only; there is no in-page editor or re-run button.
1053
+ - Raw `query` / `export` HTML output never shows KPIs or charts (no snippet
1054
+ metadata is available). Use `dbcli q @<name>` for the charted view.
1055
+ - Engine support follows the underlying command: SQL, MongoDB (`--collection`),
1056
+ Redis, and Elasticsearch (`--collection`) all render through the same template.
1057
+ - Very wide / very long result sets render as a single client-side table; for
1058
+ >10k rows prefer `--format csv` / `--format jsonl` and a downstream tool.
1059
+
880
1060
  ## MongoDB Support
881
1061
 
882
1062
  MongoDB connections use a JSON-based query model instead of SQL. Treat MongoDB support as a narrower document-database path, not as a full SQL feature equivalent.