Protects /config, selective labdata, and nightly DB dumps; documents homelab improvement plan briefs.
628 lines
24 KiB
Python
628 lines
24 KiB
Python
#!/usr/bin/env python3
|
|
"""Regenerate interview-first task briefs from structured data."""
|
|
|
|
from pathlib import Path
|
|
|
|
TASKS = [
|
|
{
|
|
"id": "TB-001",
|
|
"slug": "backup-stack",
|
|
"title": "Backup stack",
|
|
"sentence": "Set up automatic nightly backups of configs and databases, with a copy off the server.",
|
|
"why": "Right now nothing backs up /config or your databases — ZFS snapshots don't help if the whole machine dies.",
|
|
"questions": [
|
|
"Does losing valhalla overnight scare you, or is 'I'll deal with it' okay for now?",
|
|
"Do you already have offsite storage (Backblaze, Wasabi, another machine)?",
|
|
"Kopia (has a UI) vs Restic (simpler CLI) — want a recommendation or don't care?",
|
|
],
|
|
"stack": "`backup` new stack",
|
|
"steps": [
|
|
"Pick Kopia or Restic",
|
|
"New stacks/backup/ with nightly schedule + ntfy on failure",
|
|
"Script dumps for all Postgres/MariaDB/CouchDB",
|
|
"One restore test so we know it works",
|
|
],
|
|
"done": [
|
|
"Nightly backup runs without you",
|
|
"At least one restore tested",
|
|
"Failures ping ntfy",
|
|
],
|
|
},
|
|
{
|
|
"id": "TB-002",
|
|
"slug": "jd-caddy-block",
|
|
"title": "jd.ginnoir.com",
|
|
"sentence": "Add a LAN-only web address for JDownloader's UI.",
|
|
"why": "It's documented but never added to the Caddyfile — you use port 8998 instead.",
|
|
"questions": [
|
|
"Are you actually using the romhacks/JDownloader pipeline, or is this premature?",
|
|
"Is LAN-only enough, or do you need it over Tailscale too (already covered by internal_only)?",
|
|
],
|
|
"stack": "Caddyfile",
|
|
"steps": [
|
|
"Add jd.ginnoir.com block with internal_only",
|
|
"gen-bookmarks.ps1 + push",
|
|
],
|
|
"done": ["jd.ginnoir.com works on LAN", "403 from outside"],
|
|
},
|
|
{
|
|
"id": "TB-003",
|
|
"slug": "recyclarr",
|
|
"title": "Recyclarr",
|
|
"sentence": "One container that keeps Sonarr/Radarr/Bazarr quality settings in sync with TRaSH guides.",
|
|
"why": "Your *arr configs probably drift over time; this automates the boring sync.",
|
|
"questions": [
|
|
"Have you hand-tuned quality profiles you'd hate to have overwritten?",
|
|
"Want this, or are your *arr settings 'set and forget' already?",
|
|
],
|
|
"stack": "stacks/media/",
|
|
"steps": [
|
|
"Add recyclarr container on media network",
|
|
"Point at sonarr/radarr/bazarr/prowlarr APIs",
|
|
"Run first sync, spot-check Sonarr",
|
|
],
|
|
"done": ["Recyclarr syncs without errors", "Profiles visible in Sonarr or Radarr"],
|
|
},
|
|
{
|
|
"id": "TB-004",
|
|
"slug": "pin-infra-tags",
|
|
"title": "Pin infra image tags",
|
|
"sentence": "Stop Postgres, MariaDB, Caddy, etc. from silently updating to a broken :latest.",
|
|
"why": "Watchtower auto-updates containers; one bad DB image pull is painful.",
|
|
"questions": [
|
|
"Have you been burned by a surprise update before?",
|
|
"Okay to manually bump pinned versions every few months?",
|
|
],
|
|
"stack": "All compose files",
|
|
"steps": [
|
|
"Audit :latest on infra images",
|
|
"Pin versions + watchtower.enable=false on those",
|
|
"Document policy in CLAUDE.md",
|
|
],
|
|
"done": ["No :latest on DB/Caddy/Vault/CouchDB/MinIO", "Stacks still deploy clean"],
|
|
},
|
|
{
|
|
"id": "TB-033",
|
|
"slug": "public-surface-audit",
|
|
"title": "Public surface audit",
|
|
"sentence": "A short conversation + checklist: what's reachable from the internet, and should it be?",
|
|
"why": "Some things are public on purpose (Foundry, famapp); some might surprise you (Obsidian).",
|
|
"questions": [
|
|
"Want to walk through the list together in chat instead of reading a table?",
|
|
"Any service you *know* should be public that we'd flag as wrong?",
|
|
],
|
|
"stack": "Caddyfile review only",
|
|
"steps": [
|
|
"Walk each public domain in conversation",
|
|
"Mark: keep public / lock down / drop",
|
|
"Save decisions here and in TB-005/TB-006 as needed",
|
|
],
|
|
"done": ["Every public site has your yes/no", "Action items linked to other TBs"],
|
|
},
|
|
{
|
|
"id": "TB-005",
|
|
"slug": "obsidian-hardening",
|
|
"title": "Obsidian / CouchDB hardening",
|
|
"sentence": "Your whole PKM vault syncs over a public URL — let's decide how exposed you're okay with.",
|
|
"why": "obsidian.ginnoir.com has no LAN-only or SSO gate; LiveSync needs remote access from phones.",
|
|
"questions": [
|
|
"Do you sync Obsidian from your phone without Tailscale always on?",
|
|
"Would 'Tailscale only' work, or do you need public access?",
|
|
"Authentik login in front — okay if we test LiveSync still works?",
|
|
],
|
|
"stack": "Caddyfile, stacks/notes/",
|
|
"steps": [
|
|
"Pick model: Tailscale-only, Authentik, or hybrid",
|
|
"Implement + test sync from each device",
|
|
"Rotate CouchDB password",
|
|
],
|
|
"done": ["You can still sync where you need to", "Admin/_utils not wide open"],
|
|
},
|
|
{
|
|
"id": "TB-006",
|
|
"slug": "authentik-forward-auth",
|
|
"title": "Authentik on admin UIs",
|
|
"sentence": "Require Authentik login before Portainer, code-server, registry, etc.",
|
|
"why": "LAN-only IP checks aren't the same as identity — Authentik is already running for famapp.",
|
|
"questions": [
|
|
"Which admin sites do you actually use? (No point gating ones you never open)",
|
|
"Portainer git webhooks must keep working — okay to test carefully?",
|
|
"Do this for everything internal, or just the scary ones (Portainer, code, vault)?",
|
|
],
|
|
"stack": "Caddyfile + Authentik UI",
|
|
"steps": [
|
|
"Authentik proxy provider + outpost",
|
|
"Caddy forward_auth snippet",
|
|
"Tier 1: portainer, code, registry-ui, vault, minio console",
|
|
],
|
|
"done": ["Tier 1 sites require login", "Portainer webhooks still work"],
|
|
},
|
|
{
|
|
"id": "TB-007",
|
|
"slug": "romhacks-handoff",
|
|
"title": "Romhacks → RomM handoff",
|
|
"sentence": "Finish the pipeline so downloaded ROM hacks land in RomM with cover art.",
|
|
"why": "Orchestrator + JDownloader exist; moving files into the library isn't built yet.",
|
|
"questions": [
|
|
"Is the Discord/JDownloader pipeline armed, or still theoretical?",
|
|
"Worth finishing before arming, or drop the whole romhacks stack?",
|
|
"How much automation vs 'I'll move files manually'?",
|
|
],
|
|
"stack": "stacks/romhacks/orchestrator/",
|
|
"steps": [
|
|
"Complete handoff.py — incoming → Emulation/roms/<platform>",
|
|
"RomM custom cover API",
|
|
"Test with one staged folder",
|
|
],
|
|
"done": ["Test ROM in RomM after handoff", "DRY_RUN still safe"],
|
|
},
|
|
{
|
|
"id": "TB-008",
|
|
"slug": "retire-deluge",
|
|
"title": "Retire Deluge",
|
|
"sentence": "Remove Deluge if qBittorrent is doing all the torrent work.",
|
|
"why": "You run Deluge, qBit, and NZBGet — two torrent clients may be leftover cruft.",
|
|
"questions": [
|
|
"When did you last use Deluge?",
|
|
"Are Sonarr/Radarr pointed at qBit only? (We can check together)",
|
|
],
|
|
"stack": "stacks/media/, Caddyfile",
|
|
"steps": [
|
|
"Confirm zero Deluge torrents + no *arr dependency",
|
|
"Remove service + Caddy block",
|
|
],
|
|
"done": ["Deluge gone", "qBit/NZBGet still fine"],
|
|
},
|
|
{
|
|
"id": "TB-009",
|
|
"slug": "bookstack-decision",
|
|
"title": "BookStack vs Obsidian",
|
|
"sentence": "Decide if docs.ginnoir.com (BookStack) stays or goes — Obsidian is already your main PKM.",
|
|
"why": "Two wiki systems = drift and an extra MariaDB to feed.",
|
|
"questions": [
|
|
"Do you use BookStack at all? When did you last open it?",
|
|
"If we export and remove it, anything lost?",
|
|
"Or keep it for a specific use case — what?",
|
|
],
|
|
"stack": "stacks/dev/",
|
|
"steps": [
|
|
"Your call: retire, keep with boundary, or migrate to Wiki.js (TB-032)",
|
|
"If retire: export → remove containers",
|
|
],
|
|
"done": ["Decision recorded", "If retired: BookStack containers gone"],
|
|
},
|
|
{
|
|
"id": "TB-010",
|
|
"slug": "dev-stack-env-migration",
|
|
"title": "dev stack.env migration",
|
|
"sentence": "Make the dev stack use stack.env like everything else — no Portainer UI env vars.",
|
|
"why": "Every other stack commits secrets to stack.env; dev still needs UI mirroring (footgun).",
|
|
"questions": [
|
|
"Have you been bitten by dev env drift before?",
|
|
"Worth an afternoon, or low priority housekeeping?",
|
|
],
|
|
"stack": "stacks/dev/",
|
|
"steps": [
|
|
"Move all ${VAR} into stack.env",
|
|
"Deploy, verify Gitea/Plane/BookStack/code-server",
|
|
"Clear Portainer UI env vars",
|
|
],
|
|
"done": ["Dev stack runs with env_file only"],
|
|
},
|
|
{
|
|
"id": "TB-011",
|
|
"slug": "reap-orphan-volumes",
|
|
"title": "Reap orphan volumes",
|
|
"sentence": "Delete old pre-split Docker named volumes that bind mounts replaced.",
|
|
"why": "htpc-download-box_* and valhalla-lab_* volumes waste disk and confuse what's canonical.",
|
|
"questions": [
|
|
"Want me to SSH and list them with you before deleting anything?",
|
|
"Backups done first? (TB-001)",
|
|
],
|
|
"stack": "SSH on valhalla only",
|
|
"steps": [
|
|
"docker volume ls — confirm orphans",
|
|
"Spot-check bind mounts healthy",
|
|
"docker volume rm",
|
|
],
|
|
"done": ["Orphan volumes gone", "All containers still healthy"],
|
|
},
|
|
{
|
|
"id": "TB-012",
|
|
"slug": "maintainerr",
|
|
"title": "Maintainerr",
|
|
"sentence": "Auto-clean stale Overseerr requests so the queue doesn't fill with junk.",
|
|
"why": "requests.ginnoir.com is public; old unfulfilled requests pile up.",
|
|
"questions": [
|
|
"Is your Overseerr queue messy now, or is this preventive?",
|
|
"How aggressive on age — 30 days? 90?",
|
|
],
|
|
"stack": "stacks/media/",
|
|
"steps": [
|
|
"Add maintainerr on media network",
|
|
"Connect Overseerr + Sonarr/Radarr APIs",
|
|
"Conservative cleanup rule first",
|
|
],
|
|
"done": ["Maintainerr connected", "One rule configured"],
|
|
},
|
|
{
|
|
"id": "TB-013",
|
|
"slug": "code-server-mount",
|
|
"title": "Narrow code-server mount",
|
|
"sentence": "Stop code-server from mounting your entire home directory.",
|
|
"why": "code.ginnoir.com currently exposes /home/ginnoir — SSH keys, everything.",
|
|
"questions": [
|
|
"What folders do you actually edit in code-server?",
|
|
"homelabstack + a projects dir enough?",
|
|
],
|
|
"stack": "stacks/dev/docker-compose.yml",
|
|
"steps": [
|
|
"Change mount to scoped path",
|
|
"Symlink homelabstack if needed",
|
|
],
|
|
"done": [".ssh and unrelated home dirs not visible in IDE"],
|
|
},
|
|
{
|
|
"id": "TB-014",
|
|
"slug": "registry-auth",
|
|
"title": "Registry authentication",
|
|
"sentence": "Put a password on your private Docker registry.",
|
|
"why": "registry.ginnoir.com is LAN-only but anyone on LAN/tailnet can push/pull.",
|
|
"questions": [
|
|
"Do you push images to it regularly, or is it barely used?",
|
|
"Will famapp CI or github-runner need login updated?",
|
|
],
|
|
"stack": "stacks/dev/",
|
|
"steps": [
|
|
"htpasswd + registry auth env",
|
|
"Test docker login/pull/push",
|
|
],
|
|
"done": ["Unauthenticated pull fails", "You can pull with creds"],
|
|
},
|
|
{
|
|
"id": "TB-016",
|
|
"slug": "document-plex",
|
|
"title": "Document Plex",
|
|
"sentence": "Write down where Plex lives and how it connects to Tautulli — it's not in this repo.",
|
|
"why": "Tautulli reads /storage1/PMS logs but Plex isn't in compose; easy to forget how it's managed.",
|
|
"questions": [
|
|
"Is Plex on valhalla itself, or another box?",
|
|
"Do you want it in compose eventually, or happy as host-native?",
|
|
],
|
|
"stack": "Docs only",
|
|
"steps": [
|
|
"You tell me: install path, systemd?, library paths",
|
|
"Add short section to CLAUDE.md or EXTERNAL-SERVICES.md",
|
|
],
|
|
"done": ["Plex documented with paths"],
|
|
},
|
|
{
|
|
"id": "TB-019",
|
|
"slug": "immich",
|
|
"title": "Immich (photos)",
|
|
"sentence": "Self-hosted Google Photos — family photo backup with a phone app.",
|
|
"why": "Natural fit next to famapp; you don't have photo backup today.",
|
|
"questions": [
|
|
"Is phone photo backup something you actively want?",
|
|
"Public with Authentik, or LAN/tailnet only?",
|
|
"Storage growth on ZFS — okay?",
|
|
],
|
|
"stack": "New photos stack",
|
|
"steps": [
|
|
"New stacks/photos/",
|
|
"photos.ginnoir.com + optional Authentik",
|
|
"Test upload from phone",
|
|
],
|
|
"done": ["Mobile upload works", "In backup scope"],
|
|
},
|
|
{
|
|
"id": "TB-020",
|
|
"slug": "mealie",
|
|
"title": "Mealie (recipes)",
|
|
"sentence": "Recipe manager with meal plans and shopping lists.",
|
|
"why": "Pairs with famapp/family theme; lightweight stack.",
|
|
"questions": [
|
|
"Would you actually use this, or is it 'nice in theory'?",
|
|
"Shared with family or just you?",
|
|
],
|
|
"stack": "New kitchen stack or famapp",
|
|
"steps": ["New stack", "recipes.ginnoir.com", "Import one recipe as test"],
|
|
"done": ["You'd use it — or we drop it"],
|
|
},
|
|
{
|
|
"id": "TB-021",
|
|
"slug": "paperless-ngx",
|
|
"title": "Paperless-ngx",
|
|
"sentence": "Drop PDFs in a folder → OCR → searchable archive.",
|
|
"why": "ownCloud stores files; this adds document search and tagging.",
|
|
"questions": [
|
|
"Do you scan receipts/documents often enough to matter?",
|
|
"Or is ownCloud + Obsidian enough?",
|
|
],
|
|
"stack": "New docs stack",
|
|
"steps": ["New stack", "consume folder on ZFS", "Test one PDF OCR"],
|
|
"done": ["Search finds text in test doc — or we skip"],
|
|
},
|
|
{
|
|
"id": "TB-022",
|
|
"slug": "readarr",
|
|
"title": "Readarr (ebooks)",
|
|
"sentence": "Automate ebook downloads like Sonarr does for TV.",
|
|
"why": "You have /storage1/Books in ownCloud; no *arr for books yet.",
|
|
"questions": [
|
|
"Do you acquire ebooks automatically, or buy/download manually?",
|
|
"Audiobooks too? (Different tool — Audiobookshelf)",
|
|
],
|
|
"stack": "stacks/media/",
|
|
"steps": [
|
|
"Add readarr + Caddy internal block",
|
|
"Hook Prowlarr + download client",
|
|
],
|
|
"done": ["Test grab lands in Books — or drop"],
|
|
},
|
|
{
|
|
"id": "TB-023",
|
|
"slug": "autobrr",
|
|
"title": "Autobrr",
|
|
"sentence": "Monitor private tracker IRC/RSS and auto-push to qBittorrent.",
|
|
"why": "For active private tracker use — overkill if you're mostly public indexers via Prowlarr.",
|
|
"questions": [
|
|
"Are you on private trackers that need announce monitoring?",
|
|
"Or skip — Prowlarr enough?",
|
|
],
|
|
"stack": "stacks/media/",
|
|
"steps": ["Add autobrr", "Connect qBit", "One filter if proceeding"],
|
|
"done": ["Worth it for your tracker habits — or dropped"],
|
|
},
|
|
{
|
|
"id": "TB-024",
|
|
"slug": "syncthing",
|
|
"title": "Syncthing",
|
|
"sentence": "Continuous folder sync between devices — ROM saves, dotfiles, etc.",
|
|
"why": "Different from ownCloud's model; good for device-to-device without 'upload to server'.",
|
|
"questions": [
|
|
"What folders would you sync? (Be specific — not all of /storage1)",
|
|
"Conflict risk with RomM/ownCloud on same paths?",
|
|
],
|
|
"stack": "New sync stack",
|
|
"steps": ["New stack", "Pair two devices", "One test folder"],
|
|
"done": ["Sync works on chosen folder — or drop"],
|
|
},
|
|
{
|
|
"id": "TB-025",
|
|
"slug": "metrics-beszel",
|
|
"title": "Metrics (Beszel/Netdata)",
|
|
"sentence": "See CPU/RAM/disk graphs — Uptime Kuma only tells you up/down.",
|
|
"why": "Homelab has no metrics dashboard today.",
|
|
"questions": [
|
|
"Do you ever wonder 'why is the server slow' without an answer?",
|
|
"Beszel (minimal) vs Netdata (detailed) — or skip?",
|
|
],
|
|
"stack": "stacks/monitoring/",
|
|
"steps": ["Add chosen tool", "metrics.ginnoir.com internal"],
|
|
"done": ["Dashboard shows host + container usage"],
|
|
},
|
|
{
|
|
"id": "TB-026",
|
|
"slug": "igir-container",
|
|
"title": "IGIR container",
|
|
"sentence": "Schedule your existing ROM cleanup scripts instead of SSH ad-hoc.",
|
|
"why": "igir-*.sh scripts exist; romhacks handoff will add more ROMs to maintain.",
|
|
"questions": [
|
|
"Do you run IGIR often enough to automate?",
|
|
"Or manual is fine?",
|
|
],
|
|
"stack": "stacks/roms/",
|
|
"steps": [
|
|
"Container wrapping existing scripts",
|
|
"Weekly dry-run report → ntfy",
|
|
],
|
|
"done": ["Scheduled dry-run runs — or drop"],
|
|
},
|
|
{
|
|
"id": "TB-015",
|
|
"slug": "vault-secrets-integration",
|
|
"title": "Use Vault for real",
|
|
"sentence": "Vault runs but secrets live in stack.env — integrate Vault for CI/rotation without changing git-canonical deploy.",
|
|
"why": "Optional complexity; only worth it if you want rotation or CI secret centralization.",
|
|
"questions": [
|
|
"What problem would Vault solve that stack.env doesn't?",
|
|
"If 'nothing really' — drop this.",
|
|
],
|
|
"stack": "portainer-compose.yml, scripts",
|
|
"steps": [
|
|
"Define Vault vs stack.env boundary",
|
|
"Migrate one token (e.g. github-runner)",
|
|
],
|
|
"done": ["Clear use case — or dropped"],
|
|
},
|
|
{
|
|
"id": "TB-017",
|
|
"slug": "minio-consolidation",
|
|
"title": "One MinIO instead of three",
|
|
"sentence": "resume, famapp, and Plane each run their own MinIO — could be one server, three buckets.",
|
|
"why": "Less ops; slightly higher blast radius if MinIO dies.",
|
|
"questions": [
|
|
"Do the three MinIO instances cause you pain today?",
|
|
"If no — drop; isolation is fine.",
|
|
],
|
|
"stack": "resume, famapp, dev",
|
|
"steps": ["Decision", "If yes: migrate buckets, update env vars"],
|
|
"done": ["Consolidated or explicitly kept separate"],
|
|
},
|
|
{
|
|
"id": "TB-018",
|
|
"slug": "plane-evaluation",
|
|
"title": "Is Plane worth it?",
|
|
"sentence": "Plane is 7 containers for project management — do you use it?",
|
|
"why": "Gitea has issues; Homarr is your dashboard; Plane may be shelfware.",
|
|
"questions": [
|
|
"When did you last open plane.ginnoir.com?",
|
|
"Gitea issues enough, or need a kanban?",
|
|
],
|
|
"stack": "stacks/dev/",
|
|
"steps": ["Your usage answer", "Keep, or remove and reclaim resources"],
|
|
"done": ["Decision recorded"],
|
|
},
|
|
{
|
|
"id": "TB-027",
|
|
"slug": "jellyfin",
|
|
"title": "Jellyfin instead of Plex",
|
|
"sentence": "Self-host streaming in Docker instead of host-native Plex.",
|
|
"why": "Only if you want to move off Plex licensing / centralize in compose.",
|
|
"questions": [
|
|
"Happy with Plex today?",
|
|
"Family/clients okay switching apps?",
|
|
"Hardware transcoding needed?",
|
|
],
|
|
"stack": "media or new streaming",
|
|
"steps": ["Decision", "If yes: jellyfin + GPU passthrough eval"],
|
|
"done": ["Stay on Plex or migrate plan documented"],
|
|
},
|
|
{
|
|
"id": "TB-028",
|
|
"slug": "home-assistant",
|
|
"title": "Home Assistant",
|
|
"sentence": "Home automation hub — only makes sense if you have smart devices to control.",
|
|
"why": "Zero home automation in the stack today.",
|
|
"questions": [
|
|
"How many smart devices do you have?",
|
|
"Zigbee/Z-Wave USB stick on valhalla?",
|
|
"If <5 devices — probably skip.",
|
|
],
|
|
"stack": "New home stack",
|
|
"steps": ["Go/no-go", "If go: HA + USB passthrough"],
|
|
"done": ["Go/no-go with reason"],
|
|
},
|
|
{
|
|
"id": "TB-029",
|
|
"slug": "firefly-iii",
|
|
"title": "Firefly III (finance)",
|
|
"sentence": "Self-hosted budgeting and transaction tracking.",
|
|
"why": "Niche personal finance; sensitive data.",
|
|
"questions": [
|
|
"Do you want to self-host finances, or use a SaaS/bank app?",
|
|
"If yes — internal-only + Authentik mandatory.",
|
|
],
|
|
"stack": "New finance stack",
|
|
"steps": ["New stack", "internal-only Caddy"],
|
|
"done": ["Using it — or dropped"],
|
|
},
|
|
{
|
|
"id": "TB-030",
|
|
"slug": "adguard-home",
|
|
"title": "AdGuard Home",
|
|
"sentence": "Network-wide ad blocking and local DNS for *.ginnoir.com.",
|
|
"why": "Stops LAN hairpin through Cloudflare; blocks ads if router DNS points here.",
|
|
"questions": [
|
|
"Does your router already do ad blocking / local DNS?",
|
|
"Willing to point DHCP DNS at valhalla?",
|
|
],
|
|
"stack": "New network stack or mgmt",
|
|
"steps": ["Check router first", "If gap: AdGuard + DNS rewrites"],
|
|
"done": ["Router sufficient — or AdGuard deployed"],
|
|
},
|
|
{
|
|
"id": "TB-031",
|
|
"slug": "pingvin-share",
|
|
"title": "Pingvin Share",
|
|
"sentence": "Temporary password-protected file links — lighter than ownCloud shares.",
|
|
"why": "One-off 'here's a file' drops; ownCloud is heavier.",
|
|
"questions": [
|
|
"Do you send ad-hoc files to people often?",
|
|
"Or rare enough to skip?",
|
|
],
|
|
"stack": "stacks/share/",
|
|
"steps": ["Add to share stack", "drop.ginnoir.com or similar"],
|
|
"done": ["Test share link — or drop"],
|
|
},
|
|
{
|
|
"id": "TB-032",
|
|
"slug": "wikijs",
|
|
"title": "Wiki.js",
|
|
"sentence": "Modern wiki — only relevant if TB-009 says 'keep a structured wiki' but not BookStack.",
|
|
"why": "Depends on TB-009 decision; don't start here first.",
|
|
"questions": [
|
|
"Did TB-009 land on 'keep wiki'?",
|
|
"If BookStack retired or Obsidian-only — skip this entirely.",
|
|
],
|
|
"stack": "stacks/dev/",
|
|
"steps": ["Only if TB-009 = Option C", "Import + replace BookStack"],
|
|
"done": ["TB-009 dependency satisfied — or N/A"],
|
|
},
|
|
]
|
|
|
|
|
|
def render(task: dict) -> str:
|
|
qs = "\n".join(f"{i}. {q}" for i, q in enumerate(task["questions"], 1))
|
|
steps = "\n".join(f"{i}. {s}" for i, s in enumerate(task["steps"], 1))
|
|
done = "\n".join(f"- [ ] {d}" for d in task["done"])
|
|
return f"""# {task["id"]} — {task["title"]}
|
|
|
|
**Status:** not started
|
|
**Your call:** _(unset — talk first)_
|
|
|
|
---
|
|
|
|
## Talk first
|
|
|
|
> **Stop.** Don't read Reference until we've talked.
|
|
> In chat, say **"let's do {task["id"]}"** or **"{task["title"].lower()}"**.
|
|
|
|
**In one sentence:** {task["sentence"]}
|
|
|
|
**Why it came up:** {task["why"]}
|
|
|
|
### Questions
|
|
|
|
{qs}
|
|
|
|
### Your options
|
|
|
|
| Option | Meaning |
|
|
|--------|---------|
|
|
| **Do it** | We're doing this — I'll implement or walk you through it |
|
|
| **Later** | Keep on the list, not now |
|
|
| **Drop** | Remove from plan — totally fine |
|
|
| **Tweak** | Change scope; tell me how |
|
|
|
|
### What we decided
|
|
|
|
| | |
|
|
|---|---|
|
|
| **Decision** | |
|
|
| **Notes** | |
|
|
| **Date** | |
|
|
|
|
---
|
|
|
|
<details>
|
|
<summary>Reference (only open if we're doing it)</summary>
|
|
|
|
### Stack / files
|
|
|
|
{task["stack"]}
|
|
|
|
### If we do it — rough steps
|
|
|
|
{steps}
|
|
|
|
### Done when
|
|
|
|
{done}
|
|
|
|
</details>
|
|
"""
|
|
|
|
|
|
def main() -> None:
|
|
out_dir = Path(__file__).parent / "tasks"
|
|
for task in TASKS:
|
|
path = out_dir / f"{task['id']}-{task['slug']}.md"
|
|
path.write_text(render(task), encoding="utf-8", newline="\n")
|
|
print(f"wrote {path.name}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|