Add Kopia backup stack with Backblaze B2 offsite target.

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