Track the source-of-truth code, skills, bootstrap rules, docs, and packaging files before adding new features. Exclude local vault config, generated skill mirrors, temporary exports, virtual environments, and business data files from version control. Validation: inspected staged file list and git ignored status before commit.
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>` |