The 5-min sync pulls /opt/homelab-context but does not re-install the
CLI. A copy at /usr/local/bin/homelab therefore goes stale after every
CLI fix until someone re-runs bootstrap. Symlinking points
/usr/local/bin/homelab directly at the synced source, so updates land
on the next pull. Doc updated with the one-line migration for hosts
bootstrapped before this commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
/etc/age/key.txt is 0600 root and /etc/age is 0700 root, so the CLI's
Path.exists() check was returning False under regular users — making the
subcommand look broken when bootstrap had actually written the key fine.
Re-exec via sudo preserves the existing UX (one password prompt, then
plaintext) without loosening the key's perms.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 first workstation enrolled. age1vf8... is republic-laptop's
issued pubkey; added as a recipient on hello.yaml so the post-bootstrap
decrypt test works there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the full enrollment flow validated during Phase 2 rollout: per-OS
dep install (dnf/apt/brew), Gitea PAT prerequisite, DNS gotchas, the
bootstrap command, post-bootstrap verification, the homelab client add
ceremony for new inventory entries, secret grant/revoke, and a
troubleshooting table mapping every failure mode we hit during validation
to the commit that fixed it.
Linked from README under Operations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Caddy + split-horizon DNS now resolve these to LXC 105 (via 121).
Workstations off-LAN reach them via Netbird (192.168.8.0/24 is a
network resource routed through hubris).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Was apt-only on Linux; republic-laptop is Nobara so installs use dnf and
yaml is python3-pyyaml (not python3-yaml).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
We're nftables-gated to mesh+LAN; the browser-attack threat doesn't
apply, and the default whitelist (127.0.0.1/localhost/[::1] only) blocks
every LAN/mesh client.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The MCP service unit expects port 9810 per the inventory; FastMCP only
binds correctly when we set mcp.settings.host/port before run().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Encrypted to hubris + apps; expand recipients as new clients enrol via
'sops updatekeys -y secrets/hello.yaml'. Tests the full sops + age path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hubris is Netbird-only and LXC 105 is Tailscale-only; they share LAN
but not a mesh, so on-host \`curl http://192.168.8.205:9820/issue\`
arrives with source IP 192.168.8.77. In a homelab LAN with no
untrusted devices the trust boundary is reasonable; if that changes
later, narrow this.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The homelab CLI imports yaml; missing on a fresh LXC. Preflight now
checks and emits the right install hint per OS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
git credential helper does exact prefix match including scheme. Hardcoding
https:// breaks for in-LAN clones using http://192.168.8.121:3000 (which
LXC 105 needs because its DNS doesn't have the split-horizon override).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each non-hubris client needs HTTPS auth against gitea for the initial
context clone (chicken-and-egg: a PAT stored in SOPS can't be fetched
until after the clone exists). Adds a --gitea-token flag that writes
credentials to /etc/homelab-context/git-credentials and points git's
credential.helper at it, so the clone and all future pulls succeed
without prompting.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the foundation for distributing homelab context to every client
(LXCs, VMs, workstations including republic-laptop, mac-mini, ludo-mini)
with a single source of truth, structured query layer (MCP), and per-client
age-key issuance for secrets:
- inventory.yaml — canonical topology (hosts, services, mesh addresses)
- hosts/*.yaml — per-host identity files generated from inventory by
mcp/build_host_files.py; do not edit by hand
- AGENTS.md — orientation doc symlinked to /root/AGENTS.md on every client
- bootstrap.sh — one-shot enroll (Linux + macOS), clones repo, fetches age
key from issuance, installs sync timer/launchd job, drops the homelab CLI
- bin/homelab — single-binary Python CLI: whoami, list, ssh, pct, logs,
restart, open, status, secret, sync, mcp, client add/remove, nuke
- mcp/server.py — FastMCP server: context tools + read-only management
tools (no mutations exposed); shell-outs use mcp-reader restricted ssh key
- mcp/deploy/ — claudio-monitor-style gitea webhook deploy scaffold for the
MCP service on LXC 105 (ports 9810 mcp, 9811 webhook)
- secrets-issuance/ — per-client age key auto-provisioning over the mesh;
source-IP gated against inventory, with denylist for revoked clients
(ports 9820 issue, 9821 webhook)
- secrets/, .sops.yaml — SOPS recipient scaffolding; the operator fills in
age public keys after Phase 3a generates them
- scripts/sync/ — systemd timer (Linux) + launchd plist (macOS) pulling
/opt/homelab-context every 5 min
Mesh: both Netbird (preferred, 100.122.0.0/16) and Tailscale accepted
during the in-flight migration; no client is gated on completing the move.
Plan reference: /root/.claude/plans/lets-make-a-plan-fluttering-trinket.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PhotoPrism rotates its session HMAC key on every container start, so any
auto-deploy that recreated pp-app invalidated in-flight OIDC logins. The
deploy script was force-recreating + image-pulling on each push; pinned
both so pp-app survives a routine code deploy.
Measured cold-cache behaviour: thumbnails ~2ms, HEVC video playback
12-21s TTFB because libx264 transcodes inline and serialised one
ffmpeg at a time. Sibling thumbs unaffected (~2ms during transcode).
With 11/45 .mov files already pre-baked, ~75% of clicks were cold.
Ran `photoprism convert` to bring sidecar coverage to 45/45; previously
cold videos now serve in ~2ms TTFB.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NFS was mounted at /DATA/library, so the icewhale-files trash regex
(^/media/([^/]+)) derived drive=ZimaOS-HD and tried to rename into
/media/ZimaOS-HD/.trash on local ext4 -- cross-device EXDEV from NFS.
Remounting at /media/library makes 'library' its own /media/<name>
segment, so trash resolves to /media/library/.trash on the NFS itself.
Synapse runs on SQLite (not Postgres — Postgres only hosts the
mautrix bridge dbs). Documented the post-disk-full
event_push_actions cleanup that took @admin'\''s phantom count
from 125 to 4.
Rootfs grew to 16 GiB on 2026-05-15. Added bridges section
(mautrix slack/signal/meta/linkedin/whatsapp), synapse-admin
port, Postgres backend note, and operational tips for future
disk growth.
Container had been stopped since 2026-04-21 and was never re-enabled.
pct destroy 109 --purge cleaned up vm-109-disk-0 on local-lvm and the
config file. /mnt/library/syncthing subtree was already empty at the
time of destruction and is retained as an empty dir (no real data to
migrate or back up).
- README.md, containers/index.md: removed row, moved to "recently
destroyed" table
- hosts/hubris.md: dropped from /mnt/library subtree list, updated
containers/index summary line, added changelog entry
- infrastructure/media-permissions.md: dropped from membership table
and onboarding example, generalised pct-exec gotcha hostname,
added changelog
- vms/100-zimaos.md: dropped from "existing fleet" enumeration
- containers/102-nfs-export.md: dropped from bind-mount sibling list
(7 LXCs now, not 8)
- containers/109-syncthing.md: deleted
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After Files-UI evaluation passed (library renders as folder, thumbnails
work), flipped /etc/exports from ro to rw on LXC 102. Tested: write from
ZimaOS appears on /mnt/library as www-data:media, confirming the
all_squash,anonuid=33,anongid=10000 design works.
Documented two architectural findings discovered this session:
- ZimaOS Drives panel sources from GET /v2/local_storage/storages (read-only
API). Network shares cannot become Drives — Files-as-folder is supported.
- Mesh peers reach ZimaOS via hubris's existing 192.168.8.0/24 netbird subnet
advertisement; no new infra needed, just DNS (Management nameserver group
for hubris.network or per-device /etc/hosts override).
User destroyed the heaper LXC on 2026-05-14. Removed it from the
container index, README quicktable, hubris host doc, and
media-permissions membership table; moved to the "recently destroyed"
archaeology list. /mnt/library/heaper retained (224 MiB).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reflects state after the 2026-05-14 session: the OpenCLIP vision
classifier and worker-vision are gone, the db image is now postgres:16,
and the one-shot full_refresh.py reaped 7982 orphan thumbnail dirs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ENABLE_METADATA_MANAGEMENT=True + METADATA_SERVER_URL=http://seafile-md-server:8084 in seahub_settings.py. Library owners can now toggle metadata per library and create table/gallery/kanban Views over their files.
Two new services on LXC 125's docker stack:
- seafile-md-server (Pro extended metadata, internal-only on :8084)
- thumbnail-server (Pro accelerated thumbnails, bound to .185:8081)
Caddy now routes /thumbnail/* to the thumbnail-server; everything else stays
on the main seafile container. End-to-end smoke verified: routing works,
per-request permission checks via INNER_SEAHUB_SERVICE_URL=http://seafile
correctly return 403 for cross-user thumbnail requests.
Same-day-as-deploy upgrade: image swapped to seafileltd/seafile-pro-mc:13.0-latest, elasticsearch:8.15.0 added as new compose service for Pro's full-text search. Free Pro tier (<=3 users, no license). Existing data + users survived.
Also documented the Caddy patch stripping IETF resumable-upload headers (Upload-Draft-Interop-Version etc.) so the iOS Seafile Pro 4.0.2 app falls back to plain multipart upload; without it large uploads stalled and cancelled after ~60s.
LXC 125 stood up as a Seafile CE 13.0 docker-compose deployment, behind
files.hubris.network. Authentik OAuth wired up via ak shell. No data
migration — exploration alongside Nextcloud (114).
- Vision fetches NC preview at 640px via new sync helper.
- WORKER_THUMB_SIZES = set(); generate_thumbnails still does pHash.
- All medium.webp purged after verification.
- Bug: SECRET_KEY was missing from workers in compose, so
Fernet decrypt silently failed everywhere outside the backend
container. Phase 3 had been falling back to ExifTool the whole
time. Fix replicates SECRET_KEY to all worker services.
Tries Memories /api/image/info/{fileid} first (OCS-APIRequest header
bypasses CSRF), falls back to ExifTool on 404 / non-NC / errors.
Kept the full date-fallback chain because 35% of the library uses
filename-encoded dates that Memories doesn't recover.
- handle_directory_rename in scan.py covers NC-side renames via the
webhook. Iterates rows in Python (asyncpg int-type quirk on raw
SUBSTRING+LENGTH).
- Existing PATCH /folders/{id} handles mule-side renames; webhook
feedback hits the same helper and is a 0-row no-op (idempotent).
- Folder delete now propagates: webhook handler detects directory
deletes and runs a single UPDATE that discards every Photo under
the path prefix.
- PUT-overwrite of a previously-discarded file now resurrects the
Photo row (is_discarded=false, re-queue extract_metadata).
- Trashbin restore and folder rename remain known gaps (documented).
- Worker-watcher retired; file events come from NC webhook_listeners.
- Thumbnails proxy /index.php/core/preview keyed by photos.nextcloud_fileid.
- backfill_gps auto-trigger killed (was queueing ~60k tasks per deploy).
- Range support added to /api/v1/photos/{id}/original so .mov plays.
- NC cron tightened to */1 for ~60s webhook latency.
ActiveHeapCard gone, Edit + Metadata collapsibles collapsed into one
Metadata block (readonly + hr + form), Camera split out at the
bottom. Section helper deleted along with the orphan ActiveHeapCard
component file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
date_from went out as bare YYYY-MM-DD; pydantic v2 datetime parser
rejected it with 422, frontend swallowed the failure, grid showed
nothing. Fixed both sides.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EXIF:ModifyDate dropped, SubSec promoted, path-guess fallback added,
manual edits now preserved on rescan. backfill_taken_at sweep firing
across all non-manual photos.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the hard-remove SourceRoot endpoint shipped, two real-world FK
issues surfaced (parent_id self-FK and cross-source-root children) and
one tz-aware datetime issue on PATCH /photos/{id}. All three fixed in
the same session. Trash icon also picks up a spinner during the multi-
second cascade.
Deploy.sh's --force-recreate flag isn't actually recreating reliably;
flagged as open infra question.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trash icon is now an actual hard delete (SourceRoot + Folders +
Photos, Nextcloud files untouched). Watcher's 5-minute soft-time-
limit kill is fixed. Beat schedule fires discard_missing_photos
every 30 min as a safety net for events the real-time watcher
might miss.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>