Commit 75dffb

2026-06-15 10:02:38 Damien: Flesh out all pages — add Home Assistant, LAN Machines, GC Jobs, Known Issues; expand Infrastructure, Docker Swarm, Network/Media/Documents/Traefik pages
Docker Swarm.md ..
@@ 2,7 2,7 @@
← [[Home]]
- Four-node Swarm cluster managed from PCT 108 (network, 192.168.2.82). PCT 109 is **not** a Swarm node.
+ Five-node Swarm cluster managed from PCT 108 (network, 192.168.2.82). PCT 109 (ai) is **not** a Swarm node — all its services are standalone Docker Compose.
| Node | Hostname | IP | Role |
|------|----------|----|------|
@@ 25,15 25,16 @@
pct exec 108 -- docker service ls
# Inspect a service
- pct exec 108 -- docker service ps <service> # failed replica history
+ pct exec 108 -- docker service ps <service> # failed replica history
pct exec 108 -- docker service logs <service> --tail 50
- # Deploy / update
+ # Deploy / redeploy
pct exec 108 -- docker stack deploy -c <compose> <stack>
- pct exec 108 -- docker service update --force <service> # restart + fresh overlay IP
+ pct exec 108 -- docker service update --force <service> # restart + fresh overlay IP
# Check containers on a specific node
pct exec 102 -- docker ps -a --filter name=<name>
+ pct exec 104 -- docker ps
```
---
@@ 45,12 46,12 @@
/mnt/tank/appdata/portainer/compose/<id>/docker-compose.yml
```
- **Important:** Portainer stores stack env vars in its internal DB (`portainer.db`) — there are no `.env` files on disk. When redeploying via CLI, export vars manually:
+ **Portainer env var gotcha:** Portainer stores stack env vars in its internal DB (`portainer.db`) — there are no `.env` files on disk. When redeploying via CLI, export vars manually:
```bash
pct exec 108 -- bash -c 'export VAR=val && docker stack deploy -c <compose> <stack>'
```
- ### Portainer Stack Directory → Stack Mapping
+ ### Portainer Directory → Stack Mapping
| Dir ID | Stack |
|--------|-------|
@@ 80,30 81,67 @@
labels:
- "traefik.enable=true"
- "traefik.http.routers.myservice.rule=Host(`myservice.carr-family.org`)"
+ - "traefik.http.services.myservice.loadbalancer.server.port=8080"
```
---
+ ## Cross-Provider Middleware Reference
+
+ Middlewares defined in `routes.yml` (file provider) must be referenced with `@file` suffix in Swarm labels:
+ ```yaml
+ - "traefik.http.routers.myservice.middlewares=lan-only@file"
+ ```
+ Plain names default to `@swarm` and return 404.
+
+ ---
+
## Overlay Network Health Check
- Services on PCT 102 can develop stale VXLAN attachments after redeployments, causing 504s even though the service shows `1/1`.
+ Services on PCT 102 can develop stale VXLAN attachments after redeployments, causing 504s even though the service shows `1/1` and the container is healthy.
+ **Diagnose:**
```bash
TRAEFIK=$(pct exec 108 -- docker ps -q --filter name=traefik)
IP=$(pct exec 102 -- docker inspect <container-name> --format '{{(index .NetworkSettings.Networks "traefik-public").IPAddress}}')
pct exec 108 -- docker exec $TRAEFIK ping -c 2 $IP
- # Fix:
+ ```
+
+ **Fix:**
+ ```bash
pct exec 108 -- docker service update --force <service-name>
```
+ All 11 media-core services were force-updated 2026-05-13 to clear a full-stack stale attachment event.
+
---
- ## Watchtower
+ ## Standalone Containers (not Swarm-managed)
+
+ Several services run as `docker compose` or `docker run` directly on specific nodes, outside Swarm. They need static routes in Traefik's `routes.yml` since Swarm label discovery doesn't apply.
+
+ | Container | Node | IP:Port | Route |
+ |-----------|------|---------|-------|
+ | `qbittorrent-mam` | PCT 101 | 192.168.2.190:8080 | static route |
+ | `qbittorrent-vpn` | PCT 101 | 192.168.2.190:8081 | static route |
+ | `gluetun-proton` | PCT 101 | — | VPN gateway only |
+ | `qui` | PCT 101 | 192.168.2.190:7476 | static route |
+ | `jellyfin` | PCT 101 | 192.168.2.191:8096 | static route |
+ | `otterwiki` | PCT 104 | 192.168.2.105:8081 | static route |
+ | `litellm` + `postgres` | PCT 109 | 192.168.2.83:4000 | static route |
+ | `n8n` + `qdrant` | PCT 109 | 192.168.2.83:5678 | static route |
+ | `openclaw` | PCT 109 | 192.168.2.83:18789 | static route |
+ | `odysseus` | PCT 109 | 192.168.2.83:7000 | static route |
+ | `firecrawl` | PCT 109 | 192.168.2.83:3002 | no route |
+ | `whisper` | PCT 109 | 192.168.2.83:8001 | no route |
+ | `proton-bridge` | PCT 109 | 192.168.2.83:1025/1143 | no route |
+ | Pterodactyl Panel | PCT 300 | 192.168.2.136:80 | static route (lan-only) |
- Automatic image updates daily at 04:00 AM. See [[Network Stack]] for full config.
+ ---
- **Excluding a container** (required for `network_mode: "service:..."` deps):
- ```yaml
- labels:
- - "com.centurylinklabs.watchtower.enable=false"
- ```
+ ## General Compose Notes
+
+ - All services use `PUID=1000 PGID=1000` and `TZ=America/Toronto` unless noted
+ - `gai_conf_ipv4` is an external Docker config used by Prowlarr to force IPv4 DNS
+ - Before adding bind mounts: `mkdir -p /tank/appdata/<app>/<dir> && chown 1000:1000 ...` on the host first
+ - PCT 108 cannot read `/tank/appdata/<app>/` files directly from other nodes' compose dirs — always pipe compose files in via stdin when deploying from a compose file that lives on another node
Documents Stack.md ..
@@ 2,7 2,7 @@
← [[Home]]
- Services running on PCT 104 (documents, 192.168.2.105). Docker storage on ZFS (`/tank/docker`).
+ Services running on PCT 104 (documents, 192.168.2.105). Docker storage on ZFS (`/tank/docker`). Only `/tank/media` and `/tank/appdata/nextcloud` are bind-mounted — not the full tank.
---
@@ 24,9 24,9 @@
Photo management at `photos.carr-family.org`. Services: immich-server, machine-learning, postgres/pgvecto.rs (port 5435), valkey.
- GPU: uses `-cuda` image (`ghcr.io/immich-app/immich-machine-learning:release-cuda`) with `DEVICE=cuda` + `NVIDIA_VISIBLE_DEVICES=all`.
+ GPU: uses `-cuda` image (`ghcr.io/immich-app/immich-machine-learning:release-cuda`) with `DEVICE=cuda` + `NVIDIA_VISIBLE_DEVICES=all`. `nvidia-container-toolkit` installed in PCT 104; Docker default runtime is `nvidia`.
- **Redeploy** (env vars not in .env — export in shell):
+ **Redeploy** (env vars stored in Portainer DB — export in shell):
```bash
pct exec 108 -- bash -c 'export DB_PASSWORD=T5dBbAvgHWceH7jhsxjism4b2Cre9NA DB_USERNAME=immich DB_DATABASE_NAME=immich && docker stack deploy -c /mnt/tank/appdata/portainer/compose/10/docker-compose.yml documents-immich'
```
@@ 37,8 37,9 @@
File cloud at `cloud.carr-family.org`. Services: app (port 8085), cron, postgres:16 (port 5432).
- - Nextcloud data migrated to ZFS 2026-05-25 — stored at `/mnt/tank/appdata/nextcloud/` inside PCT 104
+ - Nextcloud data on ZFS at `/mnt/tank/appdata/nextcloud/` (migrated 2026-05-25)
- `PUID/PGID: 33:33`
+ - Obsidian vault lives inside the Nextcloud data tree: `/tank/appdata/nextcloud/data/data/nextcloud/files/obsidian` (bind-mounted into PCT 107 and PCT 109)
---
@@ 51,21 52,25 @@
pct exec 108 -- bash -c 'export POSTGRES_DB=paperless POSTGRES_USER=paperless POSTGRES_PASSWORD=eWJarhDasRNBu0LfBwuI6VPOXwnRCUy PAPERLESS_SECRET_KEY=J60Om5l2dsL1pUSWz3AQhqizRFhlii8dRzsynYrAfzbBsV316S PAPERLESS_TIME_ZONE=America/Chicago PAPERLESS_URL=https://paperless.carr-family.org "PAPERLESS_CSRF_TRUSTED_ORIGINS=https://paperless.carr-family.org" && docker stack deploy -c /mnt/tank/appdata/portainer/compose/4/docker-compose.yml documents-paperless'
```
- **Postgres password gotcha** — `POSTGRES_PASSWORD` only applies on first-init. If the DB is recreated with existing data, a password mismatch causes `password authentication failed` crash-loop. Fix via local socket (trust auth):
+ ### Paperless Gotchas
+
+ **Postgres password gotcha** — `POSTGRES_PASSWORD` only applies on first-init. If the DB container is recreated (e.g. Watchtower update) with existing data, the stored password is unchanged but a mismatch causes `password authentication failed` crash-loop. Fix via local socket (trust auth bypasses password check):
```bash
DB=$(pct exec 104 -- docker ps --format '{{.Names}}' | grep 'paperless_db\.' | head -1)
pct exec 104 -- docker exec $DB psql -U paperless -d paperless -c "ALTER USER paperless PASSWORD 'newpassword'"
pct exec 108 -- docker service update --force documents-paperless_webserver
```
+ > Note (2026-05-29): DB passwords and Paperless secret key regenerated after Portainer lost stack env vars. Data intact; sessions invalidated.
+
### paperless-gpt
- LLM-powered title/tag suggestions + OCR at `paperless-gpt.carr-family.org` (port 8080).
+ LLM-powered title/tag suggestions + OCR at `paperless-gpt.carr-family.org` (port 8080). Image: `icereed/paperless-gpt:latest`.
- - General LLM: `gpt-4o-mini` via LiteLLM (`http://192.168.2.83:4000`)
- - OCR LLM: `llava:7b` via LiteLLM → Ollama on `192.168.2.11`
- - Tag a document `paperless-gpt-ocr` to trigger OCR processing
- - API token: `71c8ba36985118d419cf9a1f5f8497290d816e10`
+ - **General LLM:** `gpt-4o-mini` via LiteLLM (`http://192.168.2.83:4000`)
+ - **OCR LLM:** `llava:7b` via LiteLLM → Ollama on `192.168.2.11` (RTX 3060). `llama3.2-vision:11b` failed — Ollama 0.30.7 on .11 doesn't support `mllama` arch.
+ - **Trigger:** tag a document `paperless-gpt-ocr` to trigger OCR processing
+ - **API token:** `71c8ba36985118d419cf9a1f5f8497290d816e10`
---
@@ 78,11 83,22 @@
eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..jE4UUgAXi4UpbmBi.1HbSWJSBdDAHur9EAI28WT2Cbt4rJqzakS_RrjQq9jT34iRJBb7nEW97Ml218sECdaShktMrdxAXrl6zQFXBkM4G2jpWtSEOUlIKlOH0v1KAtXRfpb9b.tNDkqQ1JdOz6gALxNDKhVA
```
- **PUT /api/v1/links/:id gotcha** — payload must include `collection: {id, name, ownerId}` and tags as `[{id, name}]`. Omitting either returns `400`.
+ **PUT /api/v1/links/:id gotcha** — payload must include `collection: {id, name, ownerId}` and tags as `[{id, name}]`. Omitting either returns `400: expected number, received undefined [id]`.
+
+ ### Auto-sort Script (PCT 109)
+
+ - **Script:** `/opt/linkwarden-sort/sort.py`
+ - **Cron:** `*/2 * * * *` — runs every 2 minutes, logs to `/var/log/linkwarden-sort.log`
+
+ Moves new links out of Unorganized (id 1) automatically. Categorization priority:
+ 1. AI tags (Linkwarden auto-tags on save)
+ 2. URL patterns — known subreddits, makerworld.com, kemono.cr, etc.
+ 3. TikTok — follows redirect to get `@username`, maps known creators; falls back to username pattern matching
+ 4. Title/meta keywords
- **Auto-sort script:** runs on PCT 109 every 2 minutes — `/opt/linkwarden-sort/sort.py`. Moves links out of Unorganized (id 1) based on AI tags, URL patterns, title keywords. Log: `/var/log/linkwarden-sort.log`.
+ To add new rules, edit `TAG_RULES`, `URL_RULES`, `TITLE_RULES`, or `TIKTOK_USER_RULES` at the top of the script.
- **Collection IDs:**
+ ### Collection IDs
| ID | Name |
|----|------|
@@ 105,9 121,20 @@
This wiki. Docker Compose on PCT 104 at `/mnt/tank/appdata/otterwiki/docker-compose.yml`.
- **Image:** `redimp/otterwiki:2`
- - **Port:** 8081 (host-mode)
- - **Static route:** `routes.yml` → `http://192.168.2.105:8081`
- - **Appdata:** `/mnt/tank/appdata/otterwiki/` — contains `settings.cfg` and `repository/` (git repo, all pages as `.md` files)
+ - **Port:** 8081 (host-mode on 192.168.2.105)
+ - **Static route:** Traefik `routes.yml` → `http://192.168.2.105:8081`
+ - **Repository:** `/mnt/tank/appdata/otterwiki/repository/` — all pages stored as `.md` files, tracked in git
- **PUID/PGID:** `33:33`
- Restart: `pct exec 104 -- docker compose -f /mnt/tank/appdata/otterwiki/docker-compose.yml restart`
+ **Restart:**
+ ```bash
+ pct exec 104 -- docker compose -f /mnt/tank/appdata/otterwiki/docker-compose.yml restart
+ ```
+
+ **To edit pages directly via git** (e.g. bulk imports):
+ ```bash
+ git config --global --add safe.directory /mnt/tank/appdata/otterwiki/repository
+ cd /mnt/tank/appdata/otterwiki/repository
+ git add -A && git commit -m "message"
+ ```
+ OtterWiki picks up committed changes immediately on next page load.
Downloads.md ..
@@ 11,11 11,15 @@
| Container | URL | Port | Purpose |
|-----------|-----|------|---------|
| `qbittorrent-mam` | `qbittorrent.carr-family.org` | 8080 | MyAnonamouse torrent client |
- | `qbittorrent-vpn` | `qbittorrent-vpn.carr-family.org` | 8081 | VPN-routed general torrents |
+ | `qbittorrent-vpn` | `qbittorrent-vpn.carr-family.org` | 8081 (via gluetun) | VPN-routed general torrents |
| `gluetun-proton` | — | — | ProtonVPN gateway for qbittorrent-vpn |
| `qui` | `qui.carr-family.org` | 7476 | Multi-instance qBittorrent web UI |
- Compose files on PCT 101: `/root/stacks/qbittorrent-mam/docker-compose.yml`, `/root/stacks/bittorrent-vpn/docker-compose.yml`
+ **Compose files on PCT 101:**
+ - `qbittorrent-mam` + `qui`: `/root/stacks/qbittorrent-mam/docker-compose.yml`
+ - `qbittorrent-vpn` + `gluetun-proton`: `/root/stacks/bittorrent-vpn/docker-compose.yml`
+
+ All four containers excluded from Watchtower — `qbittorrent-vpn` uses `network_mode: "service:gluetun"` which is incompatible with rolling restart.
---
@@ 24,13 28,18 @@
MAM (MyAnonamouse) torrent client. Image: `linuxserver/qbittorrent:4.6.7`.
- **WebUI:** `192.168.2.190:8080`, creds: `admin / 32Ab0321!!`
- - **Volume:** `/mnt/tank/media:/data` (only — bookingest accessible as `/data/bookingest` via this mount; separate mount breaks hardlinks)
- **Config:** `/mnt/tank/appdata/qbittorrent/`
+ - **Volume:** `/mnt/tank/media:/data` — **only this mount**. Do not add a separate bookingest mount; `bookingest` is accessible as `/data/bookingest` via the main mount, and a separate mount would break hardlinks (files would span datasets).
+ - **Ports:** 8080 (WebUI), 52525 TCP+UDP (BitTorrent)
**Tuned settings (4-core Xeon E5-1620 v2, 8 GB RAM, 1 Gbps):**
- - `max_active_downloads`: 70
- - `disk_cache`: 512 MB
- - `max_connec`: 1000, `max_connec_per_torrent`: 200
+
+ | Setting | Value | Notes |
+ |---------|-------|-------|
+ | `max_active_downloads` | 70 | |
+ | `disk_cache` | 512 MB | ZFS has its own ARC; auto mode balloons to ~6 GB |
+ | `max_connec` | 1000 | |
+ | `max_connec_per_torrent` | 200 | |
### Categories
@@ 38,43 47,52 @@
|----------|-----------|---------|
| `MAM` | `/data/content/books-seeds` | Direct MAM ebook downloads |
| `audiobooks` | `/data/content/audiobooks` | Audiobookshelf library |
- | `books-shelfmark` | `/data/bookingest` (default) | Shelfmark ebooks |
+ | `books-shelfmark` | `/data/bookingest` | Shelfmark ebook downloads |
+ | `audiobook-shelfmark` | `/data/bookingest` | Unused |
### On-completion Hook
- Script: `/mnt/tank/appdata/qbittorrent/scripts/hardlink-to-ingest.sh` — fires only for `MAM` category. Hardlinks by extension, preserving directory structure:
+ Script: `/mnt/tank/appdata/qbittorrent/scripts/hardlink-to-ingest.sh` — fires only for `MAM` category. Hardlinks by file extension (preserving directory structure):
+
+ | Extensions | Destination |
+ |------------|-------------|
+ | `.epub`, `.mobi`, `.azw3`, `.pdf`, etc. | `/data/bookingest` (CWA ingest) |
+ | `.m4b`, `.mp3`, `.m4a`, `.aax`, etc. | `/data/content/audiobooks` (Audiobookshelf) |
+ | `.cbz`, `.cbr`, `.cb7`, `.cbt`, etc. | `/data/content/comics` (Komga) |
- - `.epub/.mobi/.azw3/.pdf` etc. → `/data/bookingest` (CWA ingest)
- - `.m4b/.mp3/.m4a/.aax` etc. → `/data/content/audiobooks` (Audiobookshelf)
- - `.cbz/.cbr/.cb7/.cbt` etc. → `/data/content/comics` (Komga)
+ Hardlinks work because `bookingest` (`/tank/media/bookingest`) and `books-seeds` (`/tank/media/content/books-seeds`) are both on the `tank/media` ZFS dataset. Do not move bookingest back to the `tank` root dataset.
- Hardlinks work because `bookingest` and `books-seeds` are both on the `tank/media` ZFS dataset.
+ A background `chown 1000:1000` runs 5 minutes after each MAM torrent completes to keep CWA import ownership correct.
### Fixing Incorrect Save Paths
- The API `setLocation` returns 200 but silently fails. Reliable method:
- 1. `docker kill --signal SIGKILL qbittorrent-mam`
- 2. Patch `qBt-savePath` AND `save_path` in `/mnt/tank/appdata/qbittorrent/qBittorrent/BT_backup/<hash>.fastresume`
- 3. Delete all `*.fastresume.*` stale backup files
+ The qBittorrent API `setLocation` returns 200 but silently fails. The only reliable fix:
+
+ 1. `docker kill --signal SIGKILL qbittorrent-mam` (SIGKILL skips state save; graceful stop would overwrite your edits)
+ 2. Patch `qBt-savePath` **and** `save_path` in `/mnt/tank/appdata/qbittorrent/qBittorrent/BT_backup/<hash>.fastresume`
+ 3. Delete all `*.fastresume.*` stale backup files in that directory
4. `docker start qbittorrent-mam`
- 5. Force-recheck the affected torrents
+ 5. Force-recheck the affected torrents via API or WebUI
---
## qbittorrent-vpn + gluetun-proton
- VPN-routed torrents. `qbittorrent-vpn` uses `network_mode: "service:gluetun"` — both containers excluded from Watchtower (`com.centurylinklabs.watchtower.enable=false`).
+ VPN-routed torrents via ProtonVPN.
- **Compose:** `/root/stacks/bittorrent-vpn/docker-compose.yml` on PCT 101
- - **WebUI:** `192.168.2.190:8081` (internal), `qbittorrent-vpn.carr-family.org` (external)
- - **VPN:** ProtonVPN via gluetun
+ - **WebUI:** `192.168.2.190:8081` (via gluetun network), `qbittorrent-vpn.carr-family.org` (external)
+ - `qbittorrent-vpn` uses `network_mode: "service:gluetun"` — traffic routes through gluetun's ProtonVPN tunnel
+ - Both containers excluded from Watchtower (`com.centurylinklabs.watchtower.enable=false`) — update manually
---
## qui
- Multi-instance qBittorrent web UI at `qui.carr-family.org`. Not in Swarm — static route in `routes.yml` → `192.168.2.190:7476`.
+ Multi-instance qBittorrent web UI at `qui.carr-family.org`. Port 7476. Not in Swarm — static route in Traefik `routes.yml` → `192.168.2.190:7476`.
- **Instances:**
+ **Configured instances:**
- `qbittorrent-mam` → `http://192.168.2.190:8080` (admin / 32Ab0321!!)
- `qbittorrent-vpn` → `http://192.168.2.190:8081`
+
+ Config: `/mnt/tank/appdata/qui/`
/dev/null .. GC Jobs.md
@@ 0,0 1,132 @@
+ # GC Jobs
+
+ ← [[Home]]
+
+ Two services on PCT 107 (debian, 192.168.2.81) for Government of Canada job hunting.
+
+ ---
+
+ ## gcjobs-qa (stack: `gcjobs-qa`)
+
+ Streamlit STAR interview prep assistant at `gcjobs.carr-family.org` (Authentik protected).
+
+ - **Port:** 8501 (host-mode on 192.168.2.81)
+ - **Image:** `gcjobs-qa:latest` — built locally on PCT 107
+ - **Data volume:** `gcjobs-qa_gcjobs_data` → `/app/data/` (resume.json, star_answers.csv, etc.)
+ - **Env:** `OPENAI_API_KEY` in compose
+
+ ---
+
+ ## gcjobs-filler (stack: `gcjobs-filler`)
+
+ Selenium bot that pre-fills GC Jobs (PSC) applications section-by-section and emails a summary for manual review before submitting.
+
+ - **URL:** `gcjobs-filler.carr-family.org` (no Authentik — static route in `routes.yml`)
+ - **Port:** 8000 (FastAPI, host-mode on 192.168.2.81)
+ - **Image:** `gcjobs-filler:latest` — built locally on PCT 107 from `/tank/appdata/gcjobs-filler/app/`
+ - **Compose:** `/tank/appdata/gcjobs-filler/docker-compose.yml` — `traefik.enable=false`, routing via `routes.yml`
+ - **Data:** `/tank/appdata/gcjobs-filler/data/` on ZFS — mounted into PCT 107 via `mp3`, then Docker bind-mounts to `/app/data`
+ - **STAR data:** gcjobs-qa answer library mounted read-only at `/app/star_data` from `gcjobs-qa_gcjobs_data` volume on PCT 107
+ - **Permissions:** data dir `chmod 777`, files `666`, screenshots dir `777` (ZFS owned root:root, container writes as unprivileged user)
+ - **Notifications:** email via Proton Bridge SMTP when pre-fill completes
+
+ ---
+
+ ## Bot Flow
+
+ Navigate to `page1710?careerChoiceId=<id>&psrsMode=11` → PSC login (UserNumber/Password) → OTP from Proton Bridge IMAP (`Folders/GC Jobs`) → optional `page1570` (Notice page — bot clicks Continue) → optional `page1960` (Confirmation/consent — bot checks checkbox then clicks Continue) → `/applicant/<appid>/page1600` → fill 8 sections → stop before Submit → email notification.
+
+ **8 sections filled:** Employee info, Résumé, Screening Questions, Work locations, Classification, Education, Languages, Employment Equity.
+
+ **On unexpected pages:** bot saves a screenshot to `/app/data/screenshots/` for debugging.
+
+ ---
+
+ ## Supported Job URL Formats
+
+ | Format | Example |
+ |--------|---------|
+ | Standard posting | `page1800?poster=2424673` |
+ | Direct apply link | `page1710?careerChoiceId=2424673` |
+ | Newer PSC format | `/applicant/1156524/page1820?careerChoice=2424673` |
+
+ Career ID is extracted automatically from any of these formats.
+
+ ---
+
+ ## Config Files
+
+ | File | Purpose |
+ |------|---------|
+ | `/tank/appdata/gcjobs-filler/data/config.yml` | GCKey creds (`[email protected]`), IMAP config, email, monitor settings |
+ | `/tank/appdata/gcjobs-filler/data/profile.yml` | Employment info, language prefs, screening Q answers + narratives |
+
+ ### Key profile.yml Settings
+
+ - `employment.classification` — must match visible text in the PSC dropdown (e.g. `"EC-05"`)
+ - `screening_questions` — map question-text substrings to `'yes'`/`'no'`; bot fuzzy-matches (≥85%) dynamically per job
+ - `screening_narratives` — free-text responses for Yes answers; same substring keys
+
+ ---
+
+ ## Screening Question Resolution Pipeline (`app/ai_helper.py`)
+
+ 1. Fuzzy match (≥85% via `rapidfuzz`) against `profile.yml` `screening_questions` → use profiled answer
+ 2. If answer is `yes` and no narrative in `screening_narratives`, fuzzy-match question against STAR CSV → AI adapts that answer as narrative
+ 3. No profile match → fuzzy-match (≥70%) against STAR CSV → answer `yes` + AI-adapt STAR narrative
+ 4. No STAR match → ask OpenAI (`gpt-4o-mini`) yes/no from resume.json → if yes, generate narrative via `gpt-4o`
+
+ **"Section link not found for screeningQuestions"** — normal; the bot skips jobs with no screening questions.
+
+ ---
+
+ ## Web UI Tabs
+
+ | Tab | Purpose |
+ |-----|---------|
+ | Apply | Submit a job URL |
+ | Monitor | Watch the bot's progress |
+ | Jobs | Browse scraped job listings |
+ | History | Past applications |
+ | Profile | Employment, languages, education, EE consent, screening Qs + narratives |
+ | STAR Answers | Searchable answer library (from gcjobs-qa) |
+ | Settings | GCKey creds, IMAP, SMTP, monitor config |
+
+ **Settings password gotcha** — browsers block pre-filling `type="password"` fields, so opening Settings shows blank boxes. The backend preserves existing passwords when an empty string is submitted — safe to save without touching password fields.
+
+ ---
+
+ ## Monitor Filters (Settings tab)
+
+ - `keywords` — title/classification substrings
+ - `classifications` — e.g. `EC-05`
+ - `language_requirements` — e.g. `bilingual`, `english essential`; blank = all
+
+ **Jobs table** shows extracted language requirement per posting (colour-coded: purple = bilingual, blue = English essential).
+
+ ---
+
+ ## Rebuild & Redeploy
+
+ ```bash
+ # Build (context is parent dir — Dockerfile does COPY app/ .)
+ tar -cf - -C /tank/appdata/gcjobs-filler --exclude='./data' . | \
+ pct exec 107 -- bash -c 'rm -rf /tmp/gcjobs-build && mkdir -p /tmp/gcjobs-build && tar -xf - -C /tmp/gcjobs-build'
+ pct exec 107 -- docker build -t gcjobs-filler:latest /tmp/gcjobs-build/
+
+ # Image-only change (no compose changes):
+ pct exec 108 -- docker service update --force gcjobs-filler_gcjobs-filler
+
+ # Compose changes (volumes, ports, etc.) — must pipe file in; PCT 108 can't read /tank/appdata/gcjobs-filler/ directly:
+ cat /tank/appdata/gcjobs-filler/docker-compose.yml | \
+ pct exec 108 -- bash -c 'cat > /tmp/gcjobs-filler-compose.yml'
+ pct exec 108 -- docker stack deploy -c /tmp/gcjobs-filler-compose.yml gcjobs-filler
+ ```
+
+ **Deploy gotcha:** PCT 108 cannot access `/tank/appdata/gcjobs-filler/` (not in its LXC mounts). Always pipe the compose file in via stdin. `cp /tank/... /tmp/...` on the Proxmox host writes to the host's `/tmp/`, not PCT 108's.
+
+ ---
+
+ ## Known Issue
+
+ `Client.__init__() got an unexpected keyword argument 'proxies'` — AI narrative generation is broken in gcjobs-filler. Leaving screening question narratives unfilled. Cause: OpenAI client version incompatibility.
/dev/null .. Home Assistant.md
@@ 0,0 1,93 @@
+ # Home Assistant
+
+ ← [[Home]]
+
+ QEMU VM 200 (`haos16.3`) running Home Assistant OS. 2 cores, 4 GB RAM, 32 GB disk (local-lvm), OVMF/q35.
+
+ - **IP:** `192.168.2.129`
+ - **URL:** `homeassist.carr-family.org` (static Traefik route)
+ - **API:** REST at `http://192.168.2.129:8123/api/`
+ - **API Bearer token:** `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI3NzA0YWE1NGYxNTY0YWEzYmQ5M2JlMDY4YjEwY2YyMCIsImlhdCI6MTc4MTM3MzI1NCwiZXhwIjoyMDk2NzMzMjU0fQ.xBbynT7he_kdh2MylXCJ4E6ZEEYC-s870HhJEVvKb7I`
+ - **Lovelace:** Storage mode — update via websocket `lovelace/config/save` (REST API returns 404 in this HA version)
+
+ ---
+
+ ## USB Passthrough (VM config)
+
+ | USB ID | Device |
+ |--------|--------|
+ | `0a12:0001` | Bluetooth adapter |
+ | `1a86:7523` | Zigbee stick |
+ | `303a:1001` | ESP32-C6 (added 2026-06-13) |
+
+ ---
+
+ ## Areas & Key Entities
+
+ | Area | Key Entities |
+ |------|-------------|
+ | Entryway | `climate.entryway`, `sensor.entryway_temperature`, `sensor.entryway_humidity` |
+ | Living Room | `light.patio_left`, `light.wiz_tunable_white_49a46c` (Patio Right), `media_player.sound_bar` |
+ | Bedroom | `switch.lamp_socket_1` (Bedroom Lamp), `media_player.bedroom` |
+ | Homelab | `light.ikea_of_sweden_tradfri_bulb_e26_ww_806lm` (Office Light), `switch.ikea_of_sweden_tretakt_smart_plug`, qBittorrent sensors |
+ | Kitchen | `media_player.coffee_station`, `media_player.mini_tv` |
+ | Elliot's Room | `media_player.elliot_s_alexa` |
+ | Play Area | `camera.c100_mainstream`, `media_player.bedroom_old` |
+ | Laundry Room | `sensor.loki_weight/visits_today`, `sensor.milo_weight/visits_today`, `sensor.enzo_weight/visits_today` (cat litter box) |
+
+ ---
+
+ ## Overview Dashboard
+
+ Sections view, 3 columns.
+
+ | Section | Cards |
+ |---------|-------|
+ | Overview | Weather forecast, Thermostat (`climate.entryway`), Damien presence |
+ | Lights | Patio Left, Patio Right, Office, Bedroom Lamp, Homelab Plug |
+ | Climate | Entryway temperature, Entryway humidity |
+ | Media | Living Room TV (media-control) |
+ | Downloads | qBittorrent download/upload speed, active torrents, speed limit toggle |
+
+ ---
+
+ ## Updating Dashboard via Code
+
+ Lovelace is in storage mode, so the REST API (`/api/lovelace/config`) returns 404. Use the WebSocket API instead:
+
+ ```python
+ import asyncio, json, websockets
+
+ async def push_lovelace(token, config):
+ async with websockets.connect("ws://192.168.2.129:8123/api/websocket") as ws:
+ await ws.recv() # auth_required
+ await ws.send(json.dumps({"type": "auth", "access_token": token}))
+ await ws.recv() # auth_ok
+ await ws.send(json.dumps({"id": 1, "type": "lovelace/config/save", "config": config}))
+ print(await ws.recv())
+
+ TOKEN = "eyJhbGci..." # see API token above
+ asyncio.run(push_lovelace(TOKEN, your_config_dict))
+ ```
+
+ ---
+
+ ## REST API Examples
+
+ ```bash
+ TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
+
+ # Get all entity states
+ curl -s http://192.168.2.129:8123/api/states \
+ -H "Authorization: Bearer $TOKEN" | jq '.[].entity_id'
+
+ # Get a specific entity
+ curl -s http://192.168.2.129:8123/api/states/climate.entryway \
+ -H "Authorization: Bearer $TOKEN"
+
+ # Call a service
+ curl -s -X POST http://192.168.2.129:8123/api/services/light/turn_on \
+ -H "Authorization: Bearer $TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{"entity_id": "light.patio_left"}'
+ ```
Infrastructure.md ..
@@ 2,7 2,7 @@
← [[Home]]
- Proxmox host running Debian 12 Bookworm with six LXC containers and two ZFS pools.
+ Proxmox host running Debian 12 Bookworm with LXC containers, a QEMU VM, and two ZFS pools.
---
@@ 15,12 15,21 @@
| `local-lvm` | lvmthin | 354 GB | ~17% | LVM thin (PCT 108 rootfs) |
| `local` | dir | 98 GB | ~34% | Proxmox local storage |
- ZFS ACLs: `tank/appdata` has `acltype=posixacl`. Use `setfacl` when a non-owner user needs access.
+ ### ZFS Notes
+
+ - `tank/appdata` has `acltype=posixacl` enabled (2026-05-25). Use `setfacl` when a non-owner user needs access to files owned by a different UID:
+ ```bash
+ setfacl -R -m u:100000:rx /tank/appdata/nextcloud/data/data/nextcloud/files/obsidian
+ setfacl -R -m u:1000:rx /tank/appdata/nextcloud/data/data/nextcloud/files/obsidian
+ ```
---
## LXC Mount Points
+ All containers: `timezone: America/Toronto`, `onboot: 1`.
+ Default PUID/PGID: `1000:1000`. Exceptions: nextcloud/firefly `33:33`, guacamole/immich `0:0`.
+
### PCT 101 (downloads — 192.168.2.190)
| Mount | Host | Container |
@@ 29,13 38,15 @@
### PCT 102 (media-core — 192.168.2.191)
- Privileged container. Full ZFS tank pool mounted.
+ Privileged container (`unprivileged: 0`). Full ZFS tank pool mounted.
| Mount | Host | Container |
|-------|------|-----------|
| mp0 | `/tank/` | `/mnt/tank/` |
- **Media library root:** `/mnt/tank/media/content/` — movies, tv, music, books, audiobooks, podcasts, comics, roms
+ **Media library root:** `/mnt/tank/media/content/` — movies, tv, music, books, audiobooks, podcasts, comics, openbooks, roms, photos
+
+ Any Docker container without the `media-core_` Swarm prefix is standalone. Standalone containers using `mode: host` ports can conflict with Swarm services — always check `pct exec 102 -- docker ps` before troubleshooting.
### PCT 104 (documents — 192.168.2.105)
@@ 45,51 56,91 @@
|-------|------|-----------|-------|
| mp0 | `/tank/media` | `/mnt/tank/media` | Media library |
| mp1 | `/mnt/media-storage` | `/mnt/media-storage` | Separate media storage |
- | mp2 | `/tank/appdata/nextcloud` | `/mnt/tank/appdata/nextcloud` | Nextcloud data (migrated 2026-05-25) |
+ | mp2 | `/tank/appdata/nextcloud` | `/mnt/tank/appdata/nextcloud` | Nextcloud data on ZFS (migrated 2026-05-25) |
| lxc.mount.entry | `/tank/docker` | `tank/docker` | Docker storage on ZFS, not rootfs |
### PCT 107 (debian — 192.168.2.81)
- | Mount | Host | Container |
- |-------|------|-----------|
- | mp1 | `/tank/appdata/nextcloud/data/data/nextcloud/files/obsidian` | `/mnt/obsidian` |
+ | Mount | Host | Container | Notes |
+ |-------|------|-----------|-------|
+ | mp1 | `/tank/appdata/nextcloud/data/data/nextcloud/files/obsidian` | `/mnt/obsidian` | Obsidian vault (ro) |
+
+ **Standalone containers on PCT 107:**
+
+ | Container | Notes |
+ |-----------|-------|
+ | `archibus-scheduler-archibus-scheduler-1` | Custom scheduler, no published ports |
### PCT 108 (network — 192.168.2.82)
- Swarm manager. Appdata on rootfs (local-lvm), NOT ZFS. Portainer compose files are on ZFS.
+ **Docker Swarm manager/leader.** Appdata on rootfs (local-lvm, 50 GB, ~55% used) — NOT on ZFS. Portainer compose files at `/mnt/tank/appdata/portainer/compose/` are on ZFS (mp0 mount).
| Mount | Host | Container |
|-------|------|-----------|
| mp0 | `/tank/` | `/mnt/tank/` |
+ - Portainer stacks: `/mnt/tank/appdata/portainer/compose/<id>/docker-compose.yml`
+ - Portainer data: `network_portainer_data` Docker volume (on rootfs)
+
### PCT 109 (ai — 192.168.2.83)
- Privileged container. `lxc.apparmor.profile: unconfined` (required for `docker build`). Not a Swarm node.
+ Privileged container. **Not a Swarm node.** `lxc.apparmor.profile: unconfined` (required so `docker build` can run `apparmor_parser`). `lxc.cgroup2.devices.allow: a`.
| Mount | Host | Container | Notes |
|-------|------|-----------|-------|
- | mp0 | `/tank/appdata/nextcloud/data/data/nextcloud/files/obsidian` | `/mnt/obsidian` | Obsidian vault (read-write) |
+ | mp0 | `/tank/appdata/nextcloud/data/data/nextcloud/files/obsidian` | `/mnt/obsidian` | Obsidian vault (read-write) for Hermes + n8n |
+
+ Node.js 22 installed (via NodeSource) — required for `mcp-server-filesystem`.
+
+ ### PCT 300 (pterodactyl-panel — 192.168.2.136)
+
+ Pterodactyl Panel. Not a Swarm node. Services: `apache2`, `mariadb`, `redis-server`, `pteroq.service` (queue worker).
+
+ **Always use explicit env** to avoid inheriting `TMPDIR=/tmp/claude-0` from Proxmox host shell (breaks MariaDB):
+ ```bash
+ pct exec 300 -- env -i HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin TMPDIR=/tmp LANG=en_US.UTF-8 bash -c "..."
+ ```
+
+ ### PCT 301 (pterodactyl-wings — 192.168.2.134)
+
+ Pterodactyl Wings daemon. Not a Swarm node. Game server data at `/var/lib/pterodactyl/volumes/<uuid>/`.
+
+ Local Docker registry at `192.168.2.134:5000` (container named `registry`, auto-restart).
---
## GPU Passthrough
- NVIDIA GPU passthrough is configured for PCT 102, 104, and 109 via `lxc.cgroup2.devices.allow` and `lxc.mount.entry` in `/etc/pve/lxc/<vmid>.conf`.
+ NVIDIA GPU passthrough configured for PCT 102, 104, and 109 via `lxc.cgroup2.devices.allow` and `lxc.mount.entry` in `/etc/pve/lxc/<vmid>.conf`. Key NVIDIA libs bind-mounted from the Proxmox host.
- **PCT 102:** GTX card, full media stack (Jellyfin transcoding, Immich ML)
- **PCT 104:** GTX 1050 — Immich machine-learning (CUDA)
- **PCT 109:** NVIDIA GPU — Whisper transcription
+ | Container | GPU | Use |
+ |-----------|-----|-----|
+ | PCT 102 | NVIDIA (media-core) | Jellyfin transcoding, Immich ML |
+ | PCT 104 | GTX 1050 | Immich machine-learning (CUDA) |
+ | PCT 109 | NVIDIA | Whisper transcription |
- `nvidia-container-toolkit` is installed in each. Docker default runtime set to `nvidia` in `/etc/docker/daemon.json`.
+ `nvidia-container-toolkit` installed in each. Default Docker runtime set to `nvidia` in `/etc/docker/daemon.json`. Runtime mode: **legacy** (not CDI) — CDI auto-detection fails in LXC because NVML can't init.
- To enable GPU in a container, set:
+ **To enable GPU in a container:**
```yaml
environment:
NVIDIA_VISIBLE_DEVICES: all
NVIDIA_DRIVER_CAPABILITIES: all
```
+ No `runtime:` key needed — nvidia is already the default.
+
+ **After a Proxmox host driver upgrade:** update the versioned lib filenames in `/etc/pve/lxc/<vmid>.conf` to match the new driver version, `chmod 755` the new versioned files, and reboot the affected container.
+
+ ---
+
+ ## Home Assistant VM
+
+ QEMU VM 200 (`haos16.3`), 2 cores, 4 GB RAM, 32 GB disk (local-lvm), OVMF/q35. IP: `192.168.2.129`. See [[Home Assistant]] for full details.
- **After a Proxmox host driver upgrade:** update versioned lib filenames in `/etc/pve/lxc/<vmid>.conf` and reboot the affected container.
+ **USB passthrough (in VM config):**
+ - `0a12:0001` — Bluetooth
+ - `1a86:7523` — Zigbee stick
+ - `303a:1001` — ESP32-C6 (added 2026-06-13)
---
/dev/null .. Known Issues.md
@@ 0,0 1,53 @@
+ # Known Issues
+
+ ← [[Home]]
+
+ Active bugs, workarounds, and incomplete migrations.
+
+ ---
+
+ ## Overlay Network Stale Attachments (PCT 102)
+
+ After a stack redeploy, containers on PCT 102 can get broken VXLAN entries. The service shows `1/1` and the container is healthy, but Traefik gets 504 because it can't reach the container over the overlay.
+
+ **Diagnose:**
+ ```bash
+ TRAEFIK=$(pct exec 108 -- docker ps -q --filter name=traefik)
+ IP=$(pct exec 102 -- docker inspect <container-name> --format '{{(index .NetworkSettings.Networks "traefik-public").IPAddress}}')
+ pct exec 108 -- docker exec $TRAEFIK ping -c 2 $IP
+ ```
+
+ **Fix:**
+ ```bash
+ pct exec 108 -- docker service update --force <service-name>
+ ```
+
+ All 11 media-core services were force-updated 2026-05-13 to clear a full-stack stale attachment event.
+
+ See [[Docker Swarm]] → Overlay Network Health Check for more detail.
+
+ ---
+
+ ## PCT 108 Appdata Migration Incomplete
+
+ `migrate-108-appdata.sh` failed (script ran inside the container where `pct` is unavailable). PCT 108 appdata remains on rootfs (local-lvm, 50 GB, ~55% used), not on ZFS.
+
+ Portainer compose files at `/mnt/tank/appdata/portainer/compose/` are on ZFS (mp0) and are fine. Only Portainer's own internal data (`network_portainer_data` volume) and Homarr's appdata are on rootfs.
+
+ ---
+
+ ## gcjobs-filler AI Narrative Generation Broken
+
+ `Client.__init__() got an unexpected keyword argument 'proxies'` — OpenAI client version incompatibility in `gcjobs-filler`. AI narrative generation for screening questions fails silently, leaving narratives unfilled.
+
+ See [[GC Jobs]] for context.
+
+ ---
+
+ ## Authentik Middleware Removed from routes.yml (2026-06-12)
+
+ All `routes.yml` routers are now unprotected — the `authentik` middleware definition was removed. Services protected via compose `deploy.labels` still have the label but it references a non-existent middleware, so it has no effect.
+
+ To re-protect a static route: add the `authentik@file` middleware definition back to `routes.yml` and force-restart Traefik.
+
+ See [[Network Stack]] → Authentik for details.
/dev/null .. LAN Machines.md
@@ 0,0 1,62 @@
+ # LAN Machines
+
+ ← [[Home]]
+
+ Physical machines on the LAN that are not LXC containers — primarily Ollama inference servers.
+
+ ---
+
+ ## Machines
+
+ | IP | Hardware | Role |
+ |----|----------|------|
+ | `192.168.2.11` | RTX 3060 (12 GB VRAM) | Ollama — large models |
+ | `192.168.2.40` | RTX 2060 Super (16 GB RAM) | Ollama — small models |
+ | `192.168.2.73` | GTX 1050 | Local workstation |
+
+ ---
+
+ ## Ollama @ 192.168.2.11 (RTX 3060)
+
+ Primary inference server for large models. API: `http://192.168.2.40:11434`
+
+ **Running models:**
+
+ | Model | Notes |
+ |-------|-------|
+ | `qwen3.6:27b` | 27B MoE — fits in 12 GB VRAM |
+ | `qwen3.5` | |
+ | `ministral-3` | |
+ | `llama3.2` | |
+ | `llama3.1:8b` | Aliased as `llama3.1:8b-gpu` in LiteLLM |
+ | `llava:7b` | Vision/OCR — used by paperless-gpt |
+ | `nomic-embed-text` | Embeddings for Qdrant (vector size 192) |
+
+ ---
+
+ ## Ollama @ 192.168.2.40 (RTX 2060 Super)
+
+ Secondary inference server for small models. API: `http://192.168.2.40:11434`
+
+ Models stored at `C:\Users\Damien\.ollama\` (Windows machine).
+
+ **Running models:**
+
+ | Model | Notes |
+ |-------|-------|
+ | `llama3.1:8b` | |
+ | `llama3.2:3b` | |
+
+ ---
+
+ ## Model Sizing Notes
+
+ - **RTX 3060 (12 GB):** fits up to ~14B dense or ~27B MoE at Q4_K_M
+ - **Qwen3-coder-30b-a3b** (MoE) needs ~22 GB VRAM at Q4_K_M — exceeds both `.11` and `.40`. Not runnable locally.
+ - **Rule of thumb:** Q4_K_M quantization uses roughly 0.5–0.6 GB per billion parameters for dense models; MoE models use much less because only a fraction of params activate per token.
+
+ ---
+
+ ## LiteLLM Integration
+
+ Both Ollama servers are configured as backends in LiteLLM on PCT 109. See [[AI Stack]] for the full model list and proxy config. Reference Ollama models in LiteLLM as their configured model names (e.g. `qwen3.6:27b`, `llama3.1:8b`).
Media Stack.md ..
@@ 22,6 22,7 @@
| OpenBooks | `openbooks.carr-family.org` | 8875 |
| Komga | `komga.carr-family.org` | 25600 |
| RomM | `romm.carr-family.org` | 8984 |
+ | FlareSolverr | (internal only) | 8191 |
---
@@ 31,7 32,7 @@
|------|----------|
| `/mnt/tank/media/content/movies` | Movies |
| `/mnt/tank/media/content/tv` | TV shows |
- | `/mnt/tank/media/content/books` | Calibre library (managed by CWA — don't move files manually) |
+ | `/mnt/tank/media/content/books` | Calibre library (managed by CWA — never move files manually) |
| `/mnt/tank/media/content/books-seeds` | MAM torrent seeding copies (never touch) |
| `/mnt/tank/media/content/audiobooks` | Audiobookshelf + Shelfmark audiobook downloads |
| `/mnt/tank/media/content/comics` | Komga library |
@@ 46,21 47,32 @@
Config: `NETWORK_SHARE_MODE=true`, `CWA_WATCH_MODE=poll`
- **Ingest mount gotcha** — verify mount with `docker inspect <container> | grep Mounts`. If CWA sees an empty ingest folder, the compose may have been updated without a redeploy.
+ **Ingest mount gotcha** — verify with `docker inspect <container> | grep Mounts`. If CWA sees an empty ingest folder after a compose update, the container wasn't redeployed. Fix:
+ ```bash
+ pct exec 108 -- docker stack deploy -c /mnt/tank/appdata/portainer/compose/26/docker-compose.yml media-core
+ ```
+
+ **root:root ownership breaks deletes** — CWA web server runs as `abc` (uid 1000) but the ingest processor runs as root, and `NETWORK_SHARE_MODE=true` skips the post-import chown. Files land as `root:root` and `abc` gets `[Errno 13] Permission denied` on delete. Fix existing library:
+ ```bash
+ pct exec 102 -- chown -R 1000:1000 /mnt/tank/media/content/books
+ ```
+ The MAM hardlink script runs a background chown 5 minutes after each torrent completes to keep future imports clean.
- **root:root ownership breaks deletes** — CWA web server runs as `abc` (uid 1000) but ingest processor runs as root with `NETWORK_SHARE_MODE=true` which skips the post-import chown. Files land as `root:root` and deletes fail with `[Errno 13]`. Fix: `chown -R 1000:1000 /tank/media/content/books`.
+ **Bulk-delete always shows success (CWA bug)** — `/ajax/deleteselectedbooks` always returns `{"success": true}`. Check container logs for `Deleting book X failed` if books reappear. `config_calibre_split` must be `0` in `app.db` — if `1` with wrong dir, all cover lookups silently fail.
- **Bulk-delete always shows success (CWA bug)** — the `/ajax/deleteselectedbooks` route always returns `{"success": true}`. Check container logs for `Deleting book X failed` if books reappear.
+ **Stale thumbnails:** `DELETE FROM thumbnail;` in `app.db` to force regeneration. Cover generation runs nightly at 04:00.
- **Don't bulk-delete duplicates while ingest is running** — wait for all imports to complete first.
+ **Don't bulk-delete duplicates while ingest is running** — drop files into bookingest, wait for CWA to finish all of them, then delete. Deleting mid-ingest causes silent EPERM failures.
### Calibre Email Setup
- - SMTP hostname: `192.168.2.83` (Proton Bridge)
+ - SMTP hostname: `192.168.2.83` (Proton Bridge on PCT 109)
- Port: `1025`, Encryption: None
- Login: `[email protected]`, Password: `8A2SC9qao04GsSqBrfjtFg`
- ### calibredb Commands
+ ---
+
+ ## calibredb Commands
```bash
# Get CWA container name
@@ 68,26 80,86 @@
# List all books as JSON
pct exec 102 -- docker exec <cwa-container> calibredb list \
- --fields=id,title,authors,series,series_index,tags --sort-by=title \
- -s '' --library-path=/calibre-library --for-machine
+ --fields=id,title,authors,series,series_index,tags,publisher,formats \
+ --sort-by=title -s '' --library-path=/calibre-library --for-machine
- # Remove by ID
+ # Remove by ID (deletes metadata + files; does NOT touch books-seeds)
pct exec 102 -- docker exec <cwa-container> calibredb remove \
--library-path=/calibre-library <id1>,<id2>
```
---
+ ## Calibre Deduplication
+
+ Always keep the entry with the most metadata (series, series_index, tags, publisher). Never touch `/mnt/tank/media/content/books-seeds`.
+
+ **Known duplicate patterns (exact-title matching misses these):**
+
+ | Pattern | Example |
+ |---------|---------|
+ | Series prefix | `"Mistborn 02 - The Well of Ascension"` vs `"The Well of Ascension"` |
+ | Subtitle append | `"After the Funeral: A Hercule Poirot Mystery"` vs `"After the Funeral"` |
+ | Author punctuation | `"J. K. Rowling"` vs `"J.K. Rowling"` |
+ | Series tag in title | `"Famine (The Four Horsemen Book 3)"` vs `"Famine"` |
+
+ Calibre has a large Agatha Christie / Hercule Poirot set where every book exists twice: once as `"Title"` (bare) and once as `"Title: A Hercule Poirot Mystery"` (with series + series_index). Always keep the latter.
+
+ ---
+
+ ## Calibre Library Cleanup
+
+ **Junk patterns to watch for:**
+
+ | Pattern | Search term | Notes |
+ |---------|-------------|-------|
+ | Comics — Spider-Man | `spider-man` | Title starts with `Amazing Spider-Man` |
+ | Comics — Bone | `bone` + author `Jeff Smith` | Individual chapter volumes |
+ | Lonely Planet (long) | `lonely planet` | Author is `Lonely Planet` or `Planet, Lonely & ...` |
+ | Lonely Planet (short) | `LP ` | Title starts with `LP `, author `Unknown` |
+ | D&D — Forgotten Realms | `forgotten realms` | Title starts with `Forgotten Realms -` |
+ | Rough Guides | `rough guide` | Travel guides |
+ | Torrent metadata | `anonamouse` | Titled `Torrent_downloaded_from_anonamouse.net` |
+ | RPG character sheets | `character sheet` | e.g. Lone Wolf character sheets |
+ | Bare index files | `index` | Title is exactly `index` |
+
+ **Bulk-remove by keyword** (filters precisely before deleting):
+ ```bash
+ IDS=$(pct exec 102 -- docker exec <cwa-container> calibredb list \
+ --fields=id,title --sort-by=title -s '<keyword>' \
+ --library-path=/calibre-library --for-machine | python3 -c "
+ import json,sys
+ data=json.load(sys.stdin)
+ ids=[str(b['id']) for b in data if '<keyword>'.lower() in b['title'].lower()]
+ print(','.join(ids))
+ ")
+ pct exec 102 -- docker exec <cwa-container> calibredb remove --library-path=/calibre-library ${IDS}
+ ```
+
+ ---
+
## Shelfmark
- Book search & request tool. Config: `/mnt/tank/appdata/shelfmark/plugins/`.
+ Book search & request at `shelfmark.carr-family.org`. Image: `ghcr.io/calibrain/shelfmark:latest` (full, with browser automation). Config: `/mnt/tank/appdata/shelfmark/plugins/`.
+
+ After any config edit:
+ ```bash
+ pct exec 108 -- docker service update --force media-core_shelfmark
+ ```
+
+ Auth: mounts `/mnt/tank/appdata/calibre-web-automated/app.db` read-only to `/auth/app.db` — reuses Calibre-Web logins.
- After any config edit: `pct exec 108 -- docker service update --force media-core_shelfmark`
+ **Volumes:**
+ - `/mnt/tank/media/bookingest:/books` — ebook download destination
+ - `/mnt/tank/media/content/audiobooks:/audiobooks` — audiobook download destination
- - **qBittorrent connection:** `192.168.2.190:8080`, creds `admin / 32Ab0321!!`
- - **Ebook category:** `books-shelfmark`, **Audiobook category:** `audiobooks`
- - **Remote path mappings** (`advanced.json`) — must use camelCase keys and `host: "qbittorrent"`:
+ **qBittorrent connection** (`prowlarr_clients.json`):
+ - URL: `192.168.2.190:8080`, creds: `admin / 32Ab0321!!`
+ - Ebook category: `books-shelfmark`, Audiobook category: `audiobooks`
+ - `DOWNLOAD_PROGRESS_UPDATE_INTERVAL=10` (was 1 — caused login spam in qbt logs)
+ - `HARDLINK_TORRENTS=false`, `HARDLINK_TORRENTS_AUDIOBOOK=false`
+ **Remote path mappings** (`advanced.json`) — must use camelCase keys and `host: "qbittorrent"` (snake_case silently ignored):
```json
[
{"host": "qbittorrent", "remotePath": "/data/bookingest", "localPath": "/books"},
@@ 109,9 181,11 @@
ROM manager with in-browser EmulatorJS at `romm.carr-family.org`. Port 8984, host-mode on 192.168.2.191. Auth: RomM built-in (no Authentik — would break EmulatorJS API calls).
- - **ROMs:** `/mnt/tank/media/content/roms:/romm/library` — organize by platform subfolder
- - **Config:** `/mnt/tank/appdata/romm/config/config.yml` — must exist before first start
- - **Metadata:** IGDB (`u5audru2zn9na5a0x3wq26yslmk1s5`) + RetroAchievements configured in compose env
+ - **ROMs:** `/mnt/tank/media/content/roms:/romm/library` — organize by platform subfolder (e.g. `roms/gba/`, `roms/n64/`)
+ - **Config:** `/mnt/tank/appdata/romm/config/config.yml` — **must exist before first start** or settings won't persist
+ - **DB:** `/mnt/tank/appdata/romm/db`
+ - **Assets/covers:** `/mnt/tank/appdata/romm/assets`, `/mnt/tank/appdata/romm/resources`
+ - **Metadata:** IGDB client ID `u5audru2zn9na5a0x3wq26yslmk1s5`, RetroAchievements key `UV9Xf6VdH9D8N1bpzuePqAAUkwGErZW5`
```bash
pct exec 108 -- docker stack deploy -c /mnt/tank/appdata/portainer/compose/32/docker-compose.yml romm
@@ 121,10 195,27 @@
## Jellyfin (PCT 101 — standalone, not Swarm)
- Media server at `jellyfin.carr-family.org`. Static route in `routes.yml` → `192.168.2.191:8096`.
+ Media server at `jellyfin.carr-family.org`. Static route in Traefik → `192.168.2.191:8096`.
+
+ ---
+
+ ## Audio Recorder (PCT 107 — stack: `audiorecorder`)
+
+ Browser-based system audio capture at `audiorecorder.carr-family.org`. Chrome/Edge only — enable "Share system audio" in the share dialog. Transcription via Whisper on PCT 109.
+
+ - **Image:** `audiorecorder:latest` — built locally on PCT 107 from `/tank/appdata/audiorecorder/app/`
+ - **Recordings:** `/tank/appdata/audiorecorder/recordings/` — `.webm` files with `.txt` transcripts alongside
+ - **Compose:** `/tank/appdata/audiorecorder/docker-compose.yml` (must be piped to PCT 108 for deploy)
+
+ **Rebuild & redeploy:**
+ ```bash
+ tar -cf - -C /tank/appdata/audiorecorder/app . | pct exec 107 -- bash -c 'mkdir -p /tmp/audiorecorder-build && tar -xf - -C /tmp/audiorecorder-build'
+ pct exec 107 -- docker build -t audiorecorder:latest /tmp/audiorecorder-build/
+ pct exec 108 -- docker service update --force audiorecorder_audiorecorder
+ ```
---
## FlareSolverr
- Internal only (`192.168.2.191:8191`). Cloudflare bypass for Prowlarr indexers.
+ Cloudflare bypass for Prowlarr indexers. Internal only at `192.168.2.191:8191`.
Network Stack.md ..
@@ 26,80 26,113 @@
Services: server, worker, proxy outpost, postgres:16-alpine (host port 5433), redis:7-alpine.
- **Proxy outpost** (`ghcr.io/goauthentik/proxy:2026.2`) — handles Traefik ForwardAuth. Connects to `authentik` and `traefik-public` networks. forwardAuth address: `http://authentik_authentik-proxy:9000/outpost.goauthentik.io/auth/traefik`.
+ **Proxy outpost** (`ghcr.io/goauthentik/proxy:2026.2`) — handles Traefik ForwardAuth requests. Connects to `authentik` and `traefik-public` networks. ForwardAuth address: `http://authentik_authentik-proxy:9000/outpost.goauthentik.io/auth/traefik`.
- **Env file:** `/root/authentik.env` on PCT 108 (chmod 600). Required: `AUTHENTIK_SECRET_KEY`, `PG_USER`, `PG_PASS`, `PG_DB`.
+ The proxy outpost also has its own Traefik router (`authentik-outpost`) that catches all `*.carr-family.org` requests with path `/outpost.goauthentik.io/` (priority 15) so auth callbacks work after login.
+
+ **Env file:** `/root/authentik.env` on PCT 108 (chmod 600). Required vars: `AUTHENTIK_SECRET_KEY`, `PG_USER`, `PG_PASS`, `PG_DB`.
**Redeploy:**
```bash
pct exec 108 -- bash -c "set -a && source /root/authentik.env && set +a && docker stack deploy -c /mnt/tank/appdata/portainer/compose/23/docker-compose.yml authentik"
```
- **Trusted IP bypass:** LAN `192.168.2.0/24`, Tailscale `100.64.0.0/10`, `205.194.16.9/32` (friend's house) — Authentik Admin → Policy Engine → Policies → `Trusted IP Bypass`.
+ **Trusted IP bypass:** LAN `192.168.2.0/24`, Tailscale `100.64.0.0/10`, `205.194.16.9/32` (friend's house). Edit in Authentik Admin → Policy Engine → Policies → `Trusted IP Bypass`.
+
+ **IP bypass mechanism:** Expression Policy `Trusted IP Bypass` is bound (un-negated) to the Identification and Password stages of `default-authentication-flow`. Returns `False` for trusted CIDRs → stages skipped → auto-authenticated.
- > Note (2026-06-12): `authentik` middleware removed from all routes.yml routers. Routes are now unprotected via Traefik. Services with authentik applied via compose labels still have it but it's effectively unused without the middleware definition.
+ > Note (2026-06-12): `authentik` middleware removed from all `routes.yml` routers — all static routes are now unprotected. Services with authentik in their compose `deploy.labels` still have it but the middleware definition is gone. To re-enable for a service, add `authentik@file` middleware definition back to `routes.yml` first.
+
+ > Note (2026-05-29): Secret key and PG_PASS were regenerated after Portainer lost env vars. Sessions invalidated; DB intact.
---
## Homarr (stack: `documents-homarr`, compose 22)
- Dashboard at `homepage.carr-family.org`. Pinned to manager node. Image: `ghcr.io/homarr-labs/homarr:latest`.
+ Dashboard at `homepage.carr-family.org`. Image: `ghcr.io/homarr-labs/homarr:latest`. Pinned to manager node.
- - **Port:** 3001 (published) → 7575 (internal); Traefik label targets 7575
+ - **Port:** 3001 (published) → 7575 (internal nginx); Traefik label also targets 7575 (internal port is 7575, not 3000)
+ - **Appdata:** `/mnt/tank/appdata/homarr/` on PCT 108 rootfs (LVM, not ZFS)
- **DB:** `/mnt/tank/appdata/homarr/db/db.sqlite` (SQLite)
- - **Populate script:** `/root/populate-homarr.js` on Proxmox host
+ - **Encryption key:** `SECRET_ENCRYPTION_KEY` in compose (AES-256-CBC)
+ - **Population script:** `/root/populate-homarr.js` on Proxmox host (regenerates all apps/sections/integrations from scratch)
- **After re-populate, restore board permissions:**
+ **After any re-populate, restore board permissions:**
```bash
pct exec 108 -- sqlite3 /mnt/tank/appdata/homarr/db/db.sqlite "
INSERT OR IGNORE INTO boardUserPermission VALUES ('a2tzcbvgfkkt16aamvuwa6rs','bskmlbq5oayy4845ekbomk9y','full');
INSERT OR IGNORE INTO boardGroupPermission VALUES ('a2tzcbvgfkkt16aamvuwa6rs','nw94xpf6j307ceruir2b82x9','full');"
```
- **Integrations:** Sonarr, Radarr, Prowlarr, Jellyseerr (apiKey). Jellyfin + qBittorrent need manual setup (no apiKey support). `immich` and `paperlessNgx` kinds crash the integrations page — do not add.
+ To edit DB safely: scale to 0, edit, scale to 1.
+
+ **Integrations configured:** Sonarr, Radarr, Prowlarr, Jellyseerr (all apiKey).
+
+ **Manual setup required:** Jellyfin (username+password, not API key), qBittorrent.
+
+ **Do not add:** `immich` and `paperlessNgx` integration kinds crash the integrations page in this Homarr version.
---
## Guacamole (stack: `network-guacamole`, compose 6)
- Remote desktop gateway at `guac.carr-family.org`. v1.6.0 + guacd + postgresql:15 (host port 5434).
+ Remote desktop gateway at `guac.carr-family.org`. guacamole v1.6.0 + guacd + postgresql:15 (host port 5434). All services 1/1.
---
## CloudBeaver (stack: `network-cloudbeaver`, compose 31)
- DB viewer at `db.carr-family.org` (lan-only). Workspace: `/mnt/tank/appdata/cloudbeaver/workspace`.
+ DB viewer at `db.carr-family.org` (lan-only). Pinned to `network` node. Workspace: `/mnt/tank/appdata/cloudbeaver/workspace`.
**DB Connections:**
- | Name | Host | Port | User | DB |
- |------|------|------|------|----|
- | authentik | 192.168.2.82 | 5433 | authentik | authentik |
- | guacamole | 192.168.2.82 | 5434 | guacamole | guacamole |
- | nextcloud | 192.168.2.105 | 5432 | nextcloud | nextcloud |
- | paperless | 192.168.2.105 | 5433 | paperless | paperless |
- | linkwarden | 192.168.2.105 | 5434 | postgres | postgres |
- | immich | 192.168.2.105 | 5435 | immich | immich |
- | litellm | 192.168.2.83 | 5433 | litellm | litellm |
+ | Name | Host | Port | User | Password | DB |
+ |------|------|------|------|----------|----|
+ | authentik | 192.168.2.82 | 5433 | authentik | (see `/root/authentik.env` on PCT 108) | authentik |
+ | guacamole | 192.168.2.82 | 5434 | guacamole | 32Ab0321!! | guacamole |
+ | nextcloud | 192.168.2.105 | 5432 | nextcloud | 32Ab0321!! | nextcloud |
+ | paperless | 192.168.2.105 | 5433 | paperless | eWJarhDasRNBu0LfBwuI6VPOXwnRCUy | paperless |
+ | linkwarden | 192.168.2.105 | 5434 | postgres | 32Ab0321!! | postgres |
+ | immich | 192.168.2.105 | 5435 | immich | T5dBbAvgHWceH7jhsxjism4b2Cre9NA | immich |
+ | litellm | 192.168.2.83 | 5433 | litellm | litellm | litellm |
- Postgres ports are exposed host-mode on each node's LAN IP.
+ Postgres ports are exposed host-mode on each node's LAN IP. Redeploy the DB stack after adding new port mappings.
---
## Watchtower (stack: `watchtower`)
- Automatic image updates daily at 04:00 AM (`0 0 4 * * *` — cron6 format).
+ Automatic image updates daily at 04:00 AM (`0 0 4 * * *` — cron6 format, seconds first).
- - **Mode:** Global (one instance per node — covers Swarm services and standalone containers)
+ - **Mode:** Global (one instance per node — covers Swarm services and standalone containers on all nodes)
- **Config:** `WATCHTOWER_CLEANUP=true`, `WATCHTOWER_ROLLING_RESTART=true`, `DOCKER_API_VERSION=1.40`
- **Compose:** `/mnt/tank/appdata/watchtower/docker-compose.yml`
- - **Notifications:** Disabled (was spamming emails — removed 2026-06-12)
+ - **TZ:** `America/New_York`
+ - **Notifications:** Disabled (removed 2026-06-12 — was spamming emails). The `smtp-relay` sidecar (`boky/postfix`) remains in the compose file but is unused.
+
+ `DOCKER_API_VERSION=1.40` is required — Watchtower defaults to 1.25 which Docker 29.x rejects (minimum accepted is 1.40).
- **Excluded containers** (incompatible with rolling restart — `network_mode: "service:..."` dependency):
- - `gluetun-proton` and `qbittorrent-vpn` — label `com.centurylinklabs.watchtower.enable=false`
+ **Excluding containers** — containers with `network_mode: "service:..."` or `depends_on` are incompatible with `WATCHTOWER_ROLLING_RESTART=true`. Watchtower exits with code 1 on startup if it finds one. Exclude with:
+ ```yaml
+ labels:
+ - "com.centurylinklabs.watchtower.enable=false"
+ ```
+
+ **Currently excluded (update manually):**
+ - `gluetun-proton` and `qbittorrent-vpn` — compose at `/root/stacks/bittorrent-vpn/docker-compose.yml` on PCT 101; qbittorrent-vpn uses `network_mode: "service:gluetun"`
---
## Dozzle (stack: `dozzel`, compose 25)
- Live container log viewer at `dozzle.carr-family.org`. Global mode — runs on all 4 worker nodes (not PCT 108).
+ Live container log viewer at `dozzle.carr-family.org`. Global mode — one instance per worker node (does not run on PCT 108 manager).
+
+ ---
+
+ ## Portainer (stack: `network`)
+
+ Swarm UI at `portainer.carr-family.org` (port 9443 HTTPS). Image: `portainer/portainer-ce:latest`.
+
+ - Stack compose files: `/mnt/tank/appdata/portainer/compose/<id>/docker-compose.yml`
+ - Internal DB: `network_portainer_data` Docker volume on PCT 108 rootfs
+ - **Env var gotcha:** Portainer stores stack env vars in its internal DB — there are no `.env` files on disk. When redeploying via CLI, always export vars manually in the shell first.
Traefik.md ..
@@ 2,7 2,7 @@
← [[Home]]
- Reverse proxy running in the `network` Swarm stack on PCT 108. Handles all `*.carr-family.org` traffic.
+ Reverse proxy running in the `network` Swarm stack on PCT 108. Handles all `*.carr-family.org` traffic from the Cloudflare Tunnel.
- **Config:** `/mnt/tank/appdata/traefik/traefik.yml`
- **Dynamic routes:** `/mnt/tank/appdata/traefik/routes.yml`
@@ 11,30 11,37 @@
- **Entrypoints:** `web` (80 → 443 redirect), `websecure` (443)
- **Trusted IPs:** `192.168.2.0/24` (LAN), `100.64.0.0/10` (Tailscale), Cloudflare IP ranges
- Two providers: **Swarm** (local Docker socket on PCT 108) + **Docker** (`tcp://192.168.2.191:2375` for standalone containers on PCT 102).
+ Two providers:
+ - **Swarm** — local Docker socket on PCT 108 (Swarm service labels)
+ - **Docker** — `tcp://192.168.2.191:2375` for standalone containers on PCT 102 (e.g. Jellyfin)
+
+ External access: Cloudflare Tunnel on the Proxmox host → `https://192.168.2.82:443`. A 504 means the tunnel is up but Traefik (or the backend) is unreachable.
---
## Static Routes (`routes.yml`)
- Used for services not in Docker Swarm (standalone containers, VMs, or other LXC nodes).
+ Used for services not in Docker Swarm. After any edit, force-restart Traefik:
+ ```bash
+ pct exec 108 -- docker service update --force network_traefik
+ ```
- | Host | Backend |
- |------|---------|
- | `homeassist.carr-family.org` | `192.168.2.129:8123` |
- | `qbittorrent-vpn.carr-family.org` | `192.168.2.190:8081` |
- | `qbittorrent.carr-family.org` | `192.168.2.190:8080` (lan-only) |
- | `ai.carr-family.org` | `192.168.2.81:3000` |
- | `gcjobs.carr-family.org` | `192.168.2.81:8501` |
- | `jellyfin.carr-family.org` | `192.168.2.191:8096` |
- | `n8n.carr-family.org` | `192.168.2.83:5678` (lan-only) |
- | `litellm.carr-family.org` | `192.168.2.83:4000` (lan-only) |
- | `openclaw.carr-family.org` | `192.168.2.83:18789` (lan-only) |
- | `odysseus.carr-family.org` | `192.168.2.83:7000` (lan-only) |
- | `otterwiki.carr-family.org` | `192.168.2.105:8081` |
- | `pterodactyl.carr-family.org` | `192.168.2.136:80` (lan-only) |
- | `qui.carr-family.org` | `192.168.2.190:7476` |
- | `gcjobs-filler.carr-family.org` | `192.168.2.81:8000` |
+ | Host | Backend | Notes |
+ |------|---------|-------|
+ | `homeassist.carr-family.org` | `192.168.2.129:8123` | Home Assistant VM |
+ | `qbittorrent-vpn.carr-family.org` | `192.168.2.190:8081` | via gluetun |
+ | `qbittorrent.carr-family.org` | `192.168.2.190:8080` | lan-only |
+ | `ai.carr-family.org` | `192.168.2.81:3000` | PCT 107 |
+ | `gcjobs.carr-family.org` | `192.168.2.81:8501` | PCT 107 |
+ | `gcjobs-filler.carr-family.org` | `192.168.2.81:8000` | PCT 107, no Authentik |
+ | `jellyfin.carr-family.org` | `192.168.2.191:8096` | PCT 101 standalone |
+ | `qui.carr-family.org` | `192.168.2.190:7476` | PCT 101 |
+ | `n8n.carr-family.org` | `192.168.2.83:5678` | lan-only |
+ | `litellm.carr-family.org` | `192.168.2.83:4000` | lan-only |
+ | `openclaw.carr-family.org` | `192.168.2.83:18789` | lan-only |
+ | `odysseus.carr-family.org` | `192.168.2.83:7000` | lan-only |
+ | `otterwiki.carr-family.org` | `192.168.2.105:8081` | PCT 104 standalone |
+ | `pterodactyl.carr-family.org` | `192.168.2.136:80` | lan-only |
---
@@ 42,12 49,12 @@
| Name | Purpose |
|------|---------|
- | `lan-only` | IP allowlist — LAN + Tailscale |
- | `auth` | Basic auth via `traefik_auth` secret |
+ | `lan-only` | IP allowlist — LAN (`192.168.2.0/24`) + Tailscale (`100.64.0.0/10`) |
+ | `auth` | Basic auth via `traefik_auth` Docker secret |
| `secure-headers` | HSTS |
- | `authentik` | ForwardAuth → Authentik outpost (removed from routes.yml as of 2026-06-12 — compose labels may still reference it) |
+ | `authentik` | ForwardAuth → Authentik outpost (removed from `routes.yml` as of 2026-06-12) |
- **Cross-provider reference:** Middlewares defined in `routes.yml` must be referenced as `authentik@file` / `lan-only@file` in Swarm service labels — plain names default to `@swarm` and 404.
+ **Cross-provider middleware reference:** Middlewares defined in `routes.yml` (file provider) must be referenced as `authentik@file` / `lan-only@file` in Swarm service `deploy.labels` — plain names default to `@swarm` and return 404.
---
@@ 55,15 62,15 @@
| Secret | Purpose |
|--------|---------|
- | `cf_dns_token` | Cloudflare DNS challenge for TLS |
+ | `cf_dns_token` | Cloudflare DNS challenge for wildcard TLS |
| `cf_api_email` | Cloudflare account email |
- | `traefik_auth` | Dashboard basic auth |
+ | `traefik_auth` | Dashboard basic auth credentials |
---
## routes.yml Edit Gotcha
- `sed -i` replaces the file with a new inode; Traefik's bind-mount stays pinned to the old inode and misses changes. Always write in-place **and** force-restart after any edit:
+ `sed -i` creates a new inode; Traefik's bind-mount stays pinned to the old inode and won't see the change. Always write in-place (e.g. `tee` or `python3`) **and** force-restart after any edit:
```bash
pct exec 108 -- docker service update --force network_traefik
@@ 71,8 78,32 @@
---
- ## Cloudflare DNS-only (UDP game traffic — bypasses Traefik)
+ ## Authentik ForwardAuth
+
+ The Authentik proxy outpost handles ForwardAuth requests. ForwardAuth address:
+ ```
+ http://authentik_authentik-proxy:9000/outpost.goauthentik.io/auth/traefik
+ ```
+
+ The outpost also has its own router (`authentik-outpost`) catching `*.carr-family.org` paths starting with `/outpost.goauthentik.io/` at priority 15, so auth callbacks after login are handled correctly.
+
+ **To protect a Swarm service**, add to its `deploy.labels`:
+ ```yaml
+ - "traefik.http.routers.<name>.middlewares=authentik@file"
+ ```
+
+ **To protect a static route** in `routes.yml`, add to the router:
+ ```yaml
+ middlewares:
+ - authentik
+ ```
+
+ > Note (2026-06-12): authentik middleware definition removed from routes.yml. All static routes are currently unprotected. Swarm services with the label still have it but the middleware is not defined, so it has no effect.
+
+ ---
+
+ ## Cloudflare DNS-only (bypasses Traefik entirely)
- | Host | IP | Notes |
- |------|----|-------|
- | `satisfactory.carr-family.org` | `174.95.181.77` (public IP) | Grey cloud (proxy off); router port forwards → `192.168.2.134`. TCP+UDP 7777, TCP 8888 (ReliableMessaging). |
+ | Host | IP | Ports | Notes |
+ |------|----|-------|-------|
+ | `satisfactory.carr-family.org` | `174.95.181.77` (public IP) | TCP+UDP 7777, TCP 8888 | Grey cloud (proxy off); router port forwards → `192.168.2.134`. TCP 8888 = ReliableMessaging. Ports 15000/15777 obsolete since Patch 1.0. |
home.md ..
@@ 18,9 18,18 @@
| 107 | debian | 192.168.2.81 | Docker Swarm worker — general purpose |
| 108 | network | 192.168.2.82 | **Docker Swarm manager**, Portainer, Traefik |
| 109 | ai | 192.168.2.83 | AI services — not a Swarm node |
+ | 200 | haos | 192.168.2.129 | Home Assistant OS (QEMU VM) |
| 300 | pterodactyl-panel | 192.168.2.136 | Pterodactyl Panel |
| 301 | pterodactyl-wings | 192.168.2.134 | Pterodactyl Wings + game servers |
+ ## LAN Machines
+
+ | IP | Hardware | Role |
+ |----|----------|------|
+ | 192.168.2.11 | RTX 3060 (12 GB) | Ollama — large models |
+ | 192.168.2.40 | RTX 2060 Super | Ollama — small models |
+ | 192.168.2.73 | GTX 1050 | Local workstation |
+
---
## Sections
@@ 34,3 43,7 @@
- [[AI Stack]] — LiteLLM, n8n, Qdrant, Hermes, OpenClaw, Odysseus, Whisper, Proton Bridge
- [[Downloads]] — qBittorrent, gluetun/ProtonVPN
- [[Game Servers]] — Pterodactyl, Satisfactory
+ - [[Home Assistant]] — VM, USB devices, entities, dashboard
+ - [[LAN Machines]] — Ollama servers, model sizing
+ - [[GC Jobs]] — gcjobs-qa (interview prep) + gcjobs-filler (application bot)
+ - [[Known Issues]] — active bugs and workarounds
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9