Add docs-review task with last-reviewed frontmatter tracking (#129)

## Summary
- New `docs-review` mise task replaces `docs-review-random` — sorts docs by `last-reviewed` frontmatter field (never-reviewed first, then oldest)
- Updated review-documentation how-to to explain the new workflow and how to mark cards as reviewed
- Updated ai-assistance-guide task table to reference `docs-review`

## Test plan
- [x] `mise run docs-review` runs and shows staleness table + most stale doc
- [x] `mise run docs-review -- --limit 5` respects the limit flag
- [x] All pre-commit checks pass (links, index, filenames)

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/129
This commit is contained in:
Erich Blume 2026-02-09 07:29:45 -08:00
commit 9e361cf38f
6 changed files with 214 additions and 132 deletions

View file

@ -0,0 +1 @@
Add `docs-review` mise task that sorts docs by `last-reviewed` frontmatter date, prioritizing never-reviewed cards. Updated the review-documentation how-to to match.

View file

@ -1,5 +1,6 @@
---
title: Architecture
last-reviewed: 2026-02-09
tags:
- explanation
- architecture
@ -37,48 +38,33 @@ Two always-on devices form the infrastructure backbone:
## Network Layer
[[tailscale]] provides the network fabric:
[[tailscale]] provides the network fabric. All devices join a single tailnet (`tail8d86e.ts.net`) connected via WireGuard tunnels — no port forwarding or public IPs on homelab devices. ACLs control which devices and services can talk to each other, and MagicDNS provides `*.tail8d86e.ts.net` hostnames.
- All devices on tailnet `tail8d86e.ts.net`
- ACLs control access between devices and services
- MagicDNS provides `*.tail8d86e.ts.net` hostnames
- No port forwarding or public IPs on homelab devices
- Selected services exposed publicly via [[flyio-proxy]] (Fly.io → Tailscale tunnel)
## Routing Layer
## Service Routing
Three layers of reverse proxying expose services at different scopes:
Three DNS domains route to services:
| Domain | Mechanism | Reachable from |
|--------|-----------|----------------|
| `*.eblu.me` | [[flyio-proxy]] (Fly.io → Tailscale tunnel) | Public internet |
| `*.ops.eblu.me` | Caddy reverse proxy on indri | k8s pods, containers, tailnet clients |
| Domain | Proxy | Reachable from |
|--------|-------|----------------|
| `*.tail8d86e.ts.net` | Tailscale MagicDNS | Tailnet clients only |
| `*.ops.eblu.me` | [[caddy]] on indri | k8s pods, containers, tailnet clients |
| `*.eblu.me` | [[flyio-proxy]] on Fly.io | Public internet |
See [[routing]] for details on when to use which.
**Tailscale** is the base layer — every service gets a MagicDNS hostname. The [[tailscale-operator]] gives Kubernetes services their own Tailscale Ingress endpoints.
**[[caddy]]** runs natively on [[indri]] and provides a unified `*.ops.eblu.me` wildcard with TLS (Let's Encrypt via DNS-01/Gandi). It proxies to both local services (Forgejo, Zot, Jellyfin) and Kubernetes services (via their Tailscale Ingress endpoints). Access is restricted by Tailscale ACLs — only `tag:homelab` and `autogroup:admin` can reach Caddy.
**[[flyio-proxy]]** runs on Fly.io for select services that need public internet access. Traffic hits Fly.io's Anycast edge, terminates TLS, and tunnels back to the homelab over Tailscale. Only services explicitly tagged `tag:flyio-target` are reachable — a compromised proxy cannot route to arbitrary services on the tailnet.
See [[routing]] for the full service URL table and port map.
## Compute Layer
Services run in two places:
Services run in two places on [[indri]]:
### Native on Indri (Ansible)
**Native (Ansible)** — services that need host-level access run directly on macOS, managed via Ansible roles in `ansible/roles/`. See [[indri]] for the full list.
Some services run directly on macOS:
- [[forgejo]] - Git forge (needs filesystem access)
- [[zot]] - Container registry (k8s depends on it)
- [[jellyfin]] - Media server (needs VideoToolbox hardware transcoding)
- [[borgmatic]] - Backups (needs host filesystem access)
Managed via Ansible roles in `ansible/roles/`.
### Kubernetes (ArgoCD)
Most services run in minikube on indri:
- [[grafana]], [[prometheus]], [[loki]] - Observability
- [[miniflux]], [[navidrome]], [[kiwix]] - Applications
- [[postgresql]] - Shared database (CloudNativePG)
Managed via ArgoCD from `argocd/manifests/`.
**Kubernetes (ArgoCD)** — most services run in minikube, managed via ArgoCD from `argocd/manifests/`. See [[apps]] for the application registry.
## Data Flow
@ -120,9 +106,10 @@ Managed via ArgoCD from `argocd/manifests/`.
└─────────────┘
```
[[alloy]] runs in two places:
[[alloy]] runs in three places:
- On indri: collects host metrics and logs
- In k8s: collects pod logs and service probes
- On [[flyio-proxy]]: tails nginx access logs and derives request metrics
See [[observability]] for details.

View file

@ -10,15 +10,36 @@ tags:
How to periodically review and maintain the BlumeOps knowledge base.
## Quick Random Review
## Review by Staleness
Select a random documentation card for review:
Show docs sorted by when they were last reviewed (most stale first):
```bash
mise run docs-review-random
mise run docs-review
```
This displays a random card with a review checklist to guide your assessment.
This reads the `last-reviewed` frontmatter field from each card. Cards without the field are treated as never-reviewed and appear at the top. The script shows a staleness table and then displays the most stale card with a review checklist.
To show more entries in the table:
```bash
mise run docs-review -- --limit 30
```
### Marking a Card as Reviewed
After reviewing a card, add or update the `last-reviewed` field in its frontmatter:
```yaml
---
title: Some Card
last-reviewed: 2026-02-09
tags:
- reference
---
```
Commit this change alongside any fixes you make during the review.
## Review Checklist
@ -72,14 +93,6 @@ cd pulumi/gandi && pulumi preview
If changes are pending, investigate whether docs or infrastructure is stale.
## When to Review
Consider running `mise run docs-review-random` during:
- Start of work sessions (quick maintenance)
- After major infrastructure changes (verify docs reflect reality)
- When learning the system (random exploration)
## Making Changes
If a card needs updates:

View file

@ -99,7 +99,7 @@ BlumeOps operations are driven by mise tasks. Run `mise tasks` to list all avail
| `docs-check-filenames` | Check for duplicate doc filenames |
| `docs-review-stale` | Report docs by last-modified date, highlight stale ones |
| `docs-review-tags` | Print frontmatter tag inventory across all docs |
| `docs-review-random` | Select a random doc card for review |
| `docs-review` | Review the most stale doc by last-reviewed date |
| `indri-runner-logs` | View Forgejo workflow logs from local runner |
For ArgoCD operations, use the `argocd` CLI directly:

169
mise-tasks/docs-review Executable file
View file

@ -0,0 +1,169 @@
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12"
# dependencies = ["pyyaml>=6.0", "rich>=13.0.0", "typer>=0.9.0"]
# ///
#MISE description="Review the most stale documentation card by last-reviewed date"
"""Review the most stale documentation card by last-reviewed date.
Scans all markdown files in docs/ (excluding changelog.d/) and sorts them
by the ``last-reviewed`` frontmatter field. Docs without the field are
treated as never-reviewed and float to the top. Displays a staleness
table and then shows the most stale doc with a review checklist.
After reviewing, update the card's frontmatter:
last-reviewed: YYYY-MM-DD
Usage: mise run docs-review [-- --limit 10]
"""
import sys
from datetime import date
from pathlib import Path
from typing import Annotated
import typer
import yaml
from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
from rich.table import Table
DOCS_DIR = Path(__file__).parent.parent / "docs"
def extract_frontmatter(file_path: Path) -> dict | None:
"""Extract YAML frontmatter from a markdown file."""
content = file_path.read_text()
if not content.startswith("---"):
return None
end_idx = content.find("---", 3)
if end_idx == -1:
return None
frontmatter_text = content[3:end_idx].strip()
try:
return yaml.safe_load(frontmatter_text) or {}
except yaml.YAMLError:
return None
def get_last_reviewed(frontmatter: dict) -> date | None:
"""Extract last-reviewed date from frontmatter."""
raw = frontmatter.get("last-reviewed")
if raw is None:
return None
if isinstance(raw, date):
return raw
try:
return date.fromisoformat(str(raw))
except ValueError:
return None
def main(
limit: Annotated[int, typer.Option(help="Number of docs to show in the table")] = 15,
) -> None:
console = Console()
today = date.today()
entries: list[tuple[str, date | None, Path]] = []
for md_file in sorted(DOCS_DIR.rglob("*.md")):
if "changelog.d" in md_file.parts:
continue
frontmatter = extract_frontmatter(md_file)
last_reviewed = get_last_reviewed(frontmatter) if frontmatter else None
rel_path = str(md_file.relative_to(DOCS_DIR))
entries.append((rel_path, last_reviewed, md_file))
# Sort: never-reviewed first (None), then oldest reviewed
entries.sort(key=lambda e: (e[1] is not None, e[1] or date.min))
never_reviewed = sum(1 for e in entries if e[1] is None)
# --- Staleness table ---
console.print()
console.print(Panel(
f"[bold]{len(entries)}[/bold] total docs, "
f"[bold red]{never_reviewed}[/bold red] never reviewed",
title="[bold]Documentation Review Queue[/bold]",
border_style="cyan",
))
console.print()
table = Table(show_header=True, header_style="bold")
table.add_column("#", justify="right")
table.add_column("File")
table.add_column("Last Reviewed", justify="right")
table.add_column("Age (days)", justify="right")
for i, (rel_path, last_reviewed, _) in enumerate(entries[:limit], 1):
if last_reviewed is None:
table.add_row(
str(i),
f"[red]{rel_path}[/red]",
"[red]never[/red]",
"[red]—[/red]",
)
else:
age = (today - last_reviewed).days
style = "yellow" if age > 90 else ""
age_str = f"[{style}]{age}[/{style}]" if style else str(age)
path_str = f"[{style}]{rel_path}[/{style}]" if style else rel_path
date_str = f"[{style}]{last_reviewed}[/{style}]" if style else str(last_reviewed)
table.add_row(str(i), path_str, date_str, age_str)
remaining = len(entries) - limit
if remaining > 0:
table.add_row("", f"[dim]… {remaining} more[/dim]", "", "")
console.print(table)
console.print()
# --- Show the most stale doc ---
if not entries:
console.print("[bold red]No documentation files found![/bold red]")
raise typer.Exit(code=1)
top_path, top_reviewed, top_file = entries[0]
console.print(Panel(
f"[bold cyan]{top_path}[/bold cyan]\n"
+ (f"[dim]Last reviewed: {top_reviewed}[/dim]" if top_reviewed else "[dim red]Never reviewed[/dim red]"),
title="[bold]Up For Review[/bold]",
border_style="green",
))
console.print()
content = top_file.read_text()
console.print(Markdown(content))
console.print()
console.print(Panel(
"[bold]Review Checklist:[/bold]\n\n"
"• Is the information accurate and up-to-date?\n"
"• Are there broken or missing wiki-links?\n"
"• Should this card link to other related cards?\n"
"• Is the card too large and should be split?\n"
"• Is the card too small and should be merged?\n"
"• Does the frontmatter (tags, title) make sense?\n"
"• Is the card in the correct category (reference/how-to/etc)?\n\n"
"[bold]Verify Deployed State:[/bold]\n\n"
"• If ArgoCD app: is it synced? (argocd app get <app>)\n"
"• If Ansible role: does it apply idempotently? (--check --diff)\n"
"• If Pulumi: is there drift? (pulumi preview)\n\n"
"[bold]After Review:[/bold]\n\n"
"• Update the card's frontmatter: [cyan]last-reviewed: "
+ str(today)
+ "[/cyan]\n"
"• Commit the change (along with any fixes)",
title="[bold yellow]Review Guidance[/bold yellow]",
border_style="yellow",
))
if __name__ == "__main__":
typer.run(main)

View file

@ -1,88 +0,0 @@
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12"
# dependencies = ["rich>=13.0.0"]
# ///
#MISE description="Select a random documentation card for review"
"""Select a random documentation card for review.
This script scans all markdown files in the docs/ directory (excluding
changelog.d/), selects one at random, and displays it for review.
Useful for periodic knowledge base maintenance and verification.
Usage: mise run docs-review-random
"""
import random
import sys
from pathlib import Path
from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
DOCS_DIR = Path(__file__).parent.parent / "docs"
def get_all_docs() -> list[Path]:
"""Get all documentation markdown files, excluding changelog.d."""
docs = []
for md_file in DOCS_DIR.rglob("*.md"):
# Skip changelog fragments
if "changelog.d" in md_file.parts:
continue
docs.append(md_file)
return docs
def main() -> int:
console = Console()
docs = get_all_docs()
if not docs:
console.print("[bold red]No documentation files found![/bold red]")
return 1
# Select a random document
selected = random.choice(docs)
rel_path = selected.relative_to(DOCS_DIR)
# Display header
console.print()
console.print(Panel(
f"[bold cyan]{rel_path}[/bold cyan]\n"
f"[dim]{len(docs)} total docs in knowledge base[/dim]",
title="[bold]Random Documentation Card[/bold]",
border_style="cyan",
))
console.print()
# Display the file content
content = selected.read_text()
console.print(Markdown(content))
# Review checklist
console.print()
console.print(Panel(
"[bold]Review Checklist:[/bold]\n\n"
"• Is the information accurate and up-to-date?\n"
"• Are there broken or missing wiki-links?\n"
"• Should this card link to other related cards?\n"
"• Is the card too large and should be split?\n"
"• Is the card too small and should be merged?\n"
"• Does the frontmatter (tags, title) make sense?\n"
"• Is the card in the correct category (reference/how-to/etc)?\n\n"
"[bold]Verify Deployed State:[/bold]\n\n"
"• If ArgoCD app: is it synced? (argocd app get <app>)\n"
"• If Ansible role: does it apply idempotently? (--check --diff)\n"
"• If Pulumi: is there drift? (pulumi preview)",
title="[bold yellow]Review Guidance[/bold yellow]",
border_style="yellow",
))
return 0
if __name__ == "__main__":
sys.exit(main())