469 lines
12 KiB
Markdown
469 lines
12 KiB
Markdown
---
|
|
name: wiki-dashboard
|
|
description: >
|
|
Create dynamic, queryable dashboard views of the Obsidian vault using Obsidian Bases or Dataview.
|
|
Use this skill when the user says "create a dashboard", "vault dashboard", "show all X as a table",
|
|
"dynamic view", "query my vault", "build a content index", "show me all concepts/entities/projects",
|
|
or wants a structured, auto-updating view of their wiki content.
|
|
Bases is native to Obsidian 1.8+ (no plugin needed). Dataview requires the community plugin.
|
|
---
|
|
|
|
# Wiki Dashboard — Dynamic Vault Views
|
|
|
|
Two tools available: **Obsidian Bases** (native, GUI-driven, no plugin) and **Dataview** (community plugin, SQL-like, more powerful). Check which the user has and prefer Bases unless they ask for Dataview or need GROUP BY / computed columns.
|
|
|
|
## Before You Start
|
|
|
|
1. **Resolve config** — follow the Config Resolution Protocol in `llm-wiki/SKILL.md` (walk up CWD for `.env` → `~/.obsidian-wiki/config` → prompt setup). This gives `OBSIDIAN_VAULT_PATH`.
|
|
2. Read `$OBSIDIAN_VAULT_PATH/index.md` to understand what categories and pages exist.
|
|
3. Ask the user what they want to view if not specified — folder, tag, category, date range?
|
|
4. Ask if they have Dataview installed if you're unsure which tool to use.
|
|
|
|
---
|
|
|
|
## Option A — Obsidian Bases (`.base` files)
|
|
|
|
Bases are YAML files that define live views over vault notes. Native to Obsidian 1.8+, no plugin needed.
|
|
|
|
### Official canonical schema
|
|
|
|
Top-level keys:
|
|
|
|
```yaml
|
|
filters: # Global filter applied to all views (expression strings under and/or/not)
|
|
formulas: # Named computed properties — referenced as formula.<name>
|
|
properties: # Display config per property — sets displayName for column headers
|
|
summaries: # Aggregation formulas (e.g. mean, sum)
|
|
views: # Array of view definitions (required)
|
|
```
|
|
|
|
Each item in `views:`:
|
|
|
|
```yaml
|
|
views:
|
|
- type: table # table | list | cards | map
|
|
name: "View Name" # display label
|
|
limit: 50 # optional max rows
|
|
order: # column display order (list of property/formula names)
|
|
- file.name
|
|
- note.updated
|
|
groupBy: # grouping — goes INSIDE the view, NOT at top level
|
|
property: note.tags
|
|
direction: ASC # ASC | DESC
|
|
filters: # view-specific filter (merges with global filters)
|
|
and:
|
|
- 'note.status != "done"'
|
|
summaries:
|
|
formula.myFormula: Average
|
|
```
|
|
|
|
### Filter syntax — CRITICAL
|
|
|
|
**Filters use expression strings, not typed objects.** Always wrap in `and:`, `or:`, or `not:` — a bare list causes a "may only have one of and/or/not keys" parse error.
|
|
|
|
```yaml
|
|
# CORRECT
|
|
filters:
|
|
and:
|
|
- file.inFolder("concepts")
|
|
|
|
# WRONG — typed objects (parse error)
|
|
filters:
|
|
- type: folder
|
|
folder: concepts
|
|
```
|
|
|
|
Filters support nesting:
|
|
```yaml
|
|
filters:
|
|
or:
|
|
- file.hasTag("book")
|
|
- and:
|
|
- file.inFolder("concepts")
|
|
- file.hasTag("research")
|
|
- not:
|
|
- file.hasTag("archived")
|
|
```
|
|
|
|
### Property name conventions
|
|
|
|
Different contexts use different naming — confirmed from Obsidian's auto-reformat behaviour:
|
|
|
|
| Context | Frontmatter field `tags` | File name | Formula |
|
|
|---|---|---|---|
|
|
| `properties:` keys | `note.tags` | `file.name` | `formula.<name>` |
|
|
| `order:` values | `tags` (bare) | `file.name` | `formula.<name>` |
|
|
| `groupBy.property:` | `tags` (bare) | `file.name` | — |
|
|
| `filters:` expressions | `file.hasTag(...)` / `note.tags` | `file.name` | `formula.<name>` |
|
|
| `formulas:` expressions | `note.tags`, `note.updated` | `file.name` | — |
|
|
|
|
### Basic table — folder filter
|
|
|
|
```yaml
|
|
filters:
|
|
and:
|
|
- file.inFolder("concepts")
|
|
properties:
|
|
file.name:
|
|
displayName: Page
|
|
note.tags:
|
|
displayName: Tags
|
|
note.summary:
|
|
displayName: Summary
|
|
note.updated:
|
|
displayName: Updated
|
|
views:
|
|
- type: table
|
|
name: Table
|
|
order:
|
|
- file.name
|
|
- tags
|
|
- summary
|
|
- updated
|
|
```
|
|
|
|
### Cards view — folder filter
|
|
|
|
```yaml
|
|
filters:
|
|
and:
|
|
- file.inFolder("entities")
|
|
properties:
|
|
file.name:
|
|
displayName: Entity
|
|
note.title:
|
|
displayName: Full Name
|
|
note.tags:
|
|
displayName: Tags
|
|
note.summary:
|
|
displayName: Summary
|
|
views:
|
|
- type: cards
|
|
name: Cards
|
|
order:
|
|
- file.name
|
|
- title
|
|
- tags
|
|
- summary
|
|
```
|
|
|
|
### Group by property — groupBy goes INSIDE the view
|
|
|
|
When `groupBy` is set, **omit that property from `order:`** — it becomes the group header row and adding it as a column too causes duplication.
|
|
|
|
```yaml
|
|
filters:
|
|
and:
|
|
- file.inFolder("concepts")
|
|
properties:
|
|
file.name:
|
|
displayName: Concept
|
|
note.summary:
|
|
displayName: Summary
|
|
note.updated:
|
|
displayName: Updated
|
|
views:
|
|
- type: table
|
|
name: By Domain
|
|
groupBy:
|
|
property: tags # bare property name, no note. prefix
|
|
direction: ASC
|
|
order:
|
|
- file.name # do NOT include tags here — already the group header
|
|
- summary
|
|
- updated
|
|
```
|
|
|
|
### Tag filter
|
|
|
|
```yaml
|
|
filters:
|
|
and:
|
|
- file.hasTag("machine-learning")
|
|
properties:
|
|
file.name:
|
|
displayName: Page
|
|
note.category:
|
|
displayName: Category
|
|
note.summary:
|
|
displayName: Summary
|
|
views:
|
|
- type: table
|
|
name: Table
|
|
order:
|
|
- file.name
|
|
- category
|
|
- summary
|
|
```
|
|
|
|
### Multi-filter (folder AND tag)
|
|
|
|
```yaml
|
|
filters:
|
|
and:
|
|
- file.inFolder("projects")
|
|
- file.hasTag("active")
|
|
properties:
|
|
file.name:
|
|
displayName: Project
|
|
note.summary:
|
|
displayName: Summary
|
|
note.updated:
|
|
displayName: Last Updated
|
|
views:
|
|
- type: cards
|
|
name: Cards
|
|
order:
|
|
- file.name
|
|
- summary
|
|
- updated
|
|
```
|
|
|
|
### OR filter (two folders)
|
|
|
|
```yaml
|
|
filters:
|
|
or:
|
|
- file.inFolder("concepts")
|
|
- file.inFolder("entities")
|
|
properties:
|
|
file.name:
|
|
displayName: Page
|
|
note.category:
|
|
displayName: Category
|
|
note.updated:
|
|
displayName: Updated
|
|
views:
|
|
- type: table
|
|
name: Table
|
|
order:
|
|
- file.name
|
|
- category
|
|
- updated
|
|
```
|
|
|
|
### Computed column via formulas
|
|
|
|
```yaml
|
|
filters:
|
|
and:
|
|
- file.inFolder("concepts")
|
|
formulas:
|
|
days_stale: "floor((now() - note.updated) / 86400000)"
|
|
properties:
|
|
file.name:
|
|
displayName: Page
|
|
note.updated:
|
|
displayName: Updated
|
|
formula.days_stale:
|
|
displayName: Days Stale
|
|
views:
|
|
- type: table
|
|
name: Stale
|
|
order:
|
|
- file.name
|
|
- updated
|
|
- formula.days_stale
|
|
```
|
|
|
|
### Filter expression reference
|
|
|
|
| Expression | What it does |
|
|
|---|---|
|
|
| `file.inFolder("path")` | Pages in that folder |
|
|
| `file.hasTag("tag")` | Pages with that tag (no `#` prefix) |
|
|
| `file.hasLink("Note Name")` | Pages linking to a note |
|
|
| `file.name == "note-name"` | Exact filename match |
|
|
| `file.ext == "md"` | Filter by extension |
|
|
| `note.propertyName` | Any frontmatter property |
|
|
| `formula.formulaName` | A named formula result |
|
|
| `now()` | Current timestamp in ms |
|
|
|
|
> **On Obsidian UI-generated format:** When Obsidian's GUI writes or reformats a `.base` file it may output a simplified shorthand with top-level `columns:`, `sort:`, and `view:` keys instead of the canonical schema. That format also works — Obsidian accepts both. Manually authored files should use the canonical schema above.
|
|
|
|
---
|
|
|
|
## Option B — Dataview (community plugin)
|
|
|
|
Dataview uses a SQL-like query language inside ` ```dataview ``` ` code blocks in any note. More powerful than Bases for computed columns, GROUP BY, and cross-folder queries.
|
|
|
|
### Basic table — folder
|
|
|
|
````markdown
|
|
```dataview
|
|
TABLE
|
|
tags AS "Tags",
|
|
summary AS "Summary",
|
|
file.mtime AS "Last Modified"
|
|
FROM "concepts"
|
|
SORT file.mtime DESC
|
|
```
|
|
````
|
|
|
|
### Table with clickable links (TABLE WITHOUT ID)
|
|
|
|
````markdown
|
|
```dataview
|
|
TABLE WITHOUT ID
|
|
file.link AS "Entity",
|
|
tags AS "Tags",
|
|
summary AS "Summary"
|
|
FROM "entities"
|
|
SORT file.name ASC
|
|
```
|
|
````
|
|
|
|
### GROUP BY — use `rows.` prefix after grouping
|
|
|
|
After `GROUP BY`, individual file properties must be prefixed with `rows.` — otherwise the column is empty or errors.
|
|
|
|
````markdown
|
|
```dataview
|
|
TABLE WITHOUT ID
|
|
rows.file.link AS "Concept",
|
|
rows.summary AS "Summary"
|
|
FROM "concepts"
|
|
GROUP BY tags[0] AS "Domain"
|
|
```
|
|
````
|
|
|
|
### Stale pages — use `file.mtime` for date math
|
|
|
|
Avoid `choice(updated, date(updated), file.mtime)` — mixed date formats in `updated` frontmatter cause arithmetic errors. `file.mtime` is always a valid DateTime.
|
|
|
|
````markdown
|
|
```dataview
|
|
TABLE WITHOUT ID
|
|
file.link AS "Page",
|
|
category AS "Type",
|
|
file.mtime AS "Last Modified",
|
|
(date(today) - file.mtime).days + " days" AS "Age"
|
|
FROM "concepts" OR "entities" OR "projects"
|
|
WHERE file.name != file.folder
|
|
WHERE (date(today) - file.mtime).days > 30
|
|
SORT (date(today) - file.mtime).days DESC
|
|
```
|
|
````
|
|
|
|
### Multi-folder query
|
|
|
|
````markdown
|
|
```dataview
|
|
TABLE
|
|
summary AS "Summary",
|
|
file.mtime AS "Last Modified"
|
|
FROM "projects"
|
|
WHERE file.name != file.folder
|
|
SORT file.mtime DESC
|
|
```
|
|
````
|
|
|
|
### Dataview reference
|
|
|
|
| Clause | Usage |
|
|
|---|---|
|
|
| `FROM "folder"` | All notes in folder |
|
|
| `FROM #tag` | All notes with tag |
|
|
| `FROM "a" OR "b"` | Union of two folders |
|
|
| `WHERE file.name != file.folder` | Exclude folder index pages |
|
|
| `GROUP BY field AS "Label"` | Group rows — use `rows.` for properties after this |
|
|
| `SORT field DESC` | Sort direction |
|
|
| `file.link` | Clickable wikilink |
|
|
| `file.mtime` | Last modified time (always valid DateTime) |
|
|
| `(date(today) - file.mtime).days` | Days since last modification |
|
|
|
|
---
|
|
|
|
## Step 3: Write the File
|
|
|
|
**Bases:** Target path `$OBSIDIAN_VAULT_PATH/_meta/<dashboard-name>.base`
|
|
|
|
**Dataview:** Write queries directly into any `.md` note. A dedicated dashboard note at `$OBSIDIAN_VAULT_PATH/_meta/dashboard.md` works well for multi-section views.
|
|
|
|
Slug examples:
|
|
- "All concepts" → `_meta/concepts-index.base`
|
|
- "Recent ingests" → `_meta/recent-ingests.base`
|
|
- "Project overview" → `_meta/projects-overview.base`
|
|
- "Stale pages" → `_meta/stale-pages.base`
|
|
- "Full dashboard" → `_meta/dashboard.md`
|
|
|
|
Create `_meta/` if it doesn't exist yet.
|
|
|
|
## Step 4: Embed Bases (optional)
|
|
|
|
To embed a `.base` inside a note:
|
|
|
|
```markdown
|
|
## Entities
|
|
![[_meta/entities-tracker.base]]
|
|
```
|
|
|
|
Ask before modifying an existing note.
|
|
|
|
## Step 5: Update Tracking
|
|
|
|
Append to `$OBSIDIAN_VAULT_PATH/log.md`:
|
|
```
|
|
- [TIMESTAMP] WIKI_DASHBOARD name="<slug>" tool=bases|dataview view=<type> filter="<description>"
|
|
```
|
|
|
|
No manifest or index update needed — dashboards are live queries, not static pages.
|
|
|
|
## Common Dashboard Recipes
|
|
|
|
| Dashboard | Best tool | What it shows |
|
|
|---|---|---|
|
|
| **Content index** | Bases or Dataview | All pages grouped by category, sorted by updated |
|
|
| **Entity tracker** | Bases (cards) | Entity pages as a visual card gallery |
|
|
| **Concepts by domain** | Dataview | Concepts grouped by first tag using GROUP BY |
|
|
| **Ingestion log** | Either | Pages sorted by `created` date |
|
|
| **Stale content** | Dataview | Pages not touched in 30+ days with day count |
|
|
| **Project overview** | Either | Project pages with last-sync date |
|
|
| **Research tracker** | Dataview | Synthesis pages tagged `research` |
|
|
|
|
## Quality Checklist
|
|
|
|
- [ ] Bases: filters use expression strings under `and:`/`or:`/`not:`, never typed objects
|
|
- [ ] Bases: `groupBy` goes inside the view definition — not as a top-level key
|
|
- [ ] Bases: column headers set via `properties: <name>: displayName: "..."`, not `columns: [{title}]`
|
|
- [ ] Bases: `formulas:` used for computed columns, referenced as `formula.<name>` in order/properties
|
|
- [ ] Dataview: GROUP BY queries use `rows.property` not bare `property`
|
|
- [ ] Dataview: date arithmetic uses `file.mtime`, not `choice(updated, ...)`
|
|
- [ ] File written to `_meta/` with a descriptive slug
|
|
- [ ] `log.md` updated
|
|
- [ ] User told how to embed Bases (`![[_meta/<name>.base]]`) or open the dashboard note
|
|
|
|
## QMD Refresh After Vault Writes
|
|
|
|
QMD is a search index, not the source of truth. If `$QMD_WIKI_COLLECTION` is empty or unset, skip this step. Run it only after this skill has written or rewritten vault markdown. If QMD refresh fails, do not roll back the vault changes; report the QMD status separately.
|
|
|
|
Use `$QMD_CLI` if set; otherwise use `qmd`.
|
|
|
|
```bash
|
|
${QMD_CLI:-qmd} update
|
|
```
|
|
|
|
If the output says vectors are needed or embeddings may be stale, run:
|
|
|
|
```bash
|
|
${QMD_CLI:-qmd} embed
|
|
```
|
|
|
|
Verify the collection with either:
|
|
|
|
```bash
|
|
${QMD_CLI:-qmd} ls "$QMD_WIKI_COLLECTION"
|
|
```
|
|
|
|
or, when a specific page path is known:
|
|
|
|
```bash
|
|
${QMD_CLI:-qmd} get "qmd://$QMD_WIKI_COLLECTION/<page>.md" -l 5
|
|
```
|
|
|
|
Record one of:
|
|
- `QMD refreshed: update + embed + verified`
|
|
- `QMD refreshed: update only + verified`
|
|
- `QMD skipped: QMD_WIKI_COLLECTION unset`
|
|
- `QMD skipped: qmd CLI unavailable`
|
|
- `QMD failed: <short error summary>` |