#!/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/", "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** | | ---
Reference (only open if we're doing it) ### Stack / files {task["stack"]} ### If we do it — rough steps {steps} ### Done when {done}
""" 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()