Commit Graph
43 Commits
Author SHA1 Message Date
Frank 09c54113b1 deploy: make Playwright e2e a mandatory post-deploy gate
The 2026-09-12 homepage outage slipped through because the live-fleet
Playwright suite was a manual 'MUST invoke' that deploys skipped. Wire it
into deploy.sh as the [3/3] gate: after a behavior-affecting deploy,
run tests/e2e/run.sh against the live fleet; a failure exits DEPLOY
INCOMPLETE. Add --skip-e2e as an emergency escape hatch. Update the
AGENTS.md deployment-completeness contract and flow diagram to match.

Verified: bash -n clean, --skip-e2e parsed, unknown flags rejected,
seam suite green (only the pre-existing comfyui test 73 fails).
2026-09-12 11:50:40 -07:00
Frank 91e9410c5e deploy.sh: allowlist dankclaw:marreta in the top-level service gate
The scoped runner (deploy-remote.sh) already knew marreta; the top-level
deploy.sh valid_service() gate did not, so --service marreta was rejected
before reaching the planner.
2026-09-12 10:10:01 -07:00
Frank 69f87c8483 feat(llm): rename inference services dropping 'default' from names
frank-default-large -> frank-large (Dankclaw :7770), frank-default-small ->
frank-small (Dankserver :7770), frank-default-sightsound -> frank-npu
(Dankclaw :7771). Units, env files, serve scripts, deploy-plan mappings,
deploy-remote dispatch (stop/disable old units + relink + enable new), and the
test seams move together. Retired frank-default-npu.* artifacts moved to
scripts/.AGENTTRASH/ (tracked-in-history; ignored on disk).

Served aliases become frank-large / frank-small via --alias; the NPU endpoint
serves the whole FLM registry, so its rename changes no served tags.
2026-09-03 16:15:43 -07:00
Frank 93f16f68be deployer: route plex and tautulli as dankserver quadlet services
- deploy_plex(): install unit, stop+mask native plexmediaserver.service on
  first cutover (mask, never disable), gate on /identity for 180s
- deploy_tautulli(): install unit, stop+disable snap on first cutover,
  seed config.ini with PMS_Web_Host=plex only if absent
- allow-lists (deploy.sh valid_service, deploy-remote.sh service_is_allowed),
  planner routing (SERVICE_ORDER, DANKSERVER_SERVICES, quadlet_services),
  test-changed cutover case, quadlet seam enumeration + new
  userns/PUID=997/PGID=984 ownership-contract test
2026-09-01 22:31:14 -07:00
Frank 7457a9c607 deploy.sh: accept dankclaw:homepage as a valid explicit service (drift vs deploy-plan.py) 2026-09-01 16:43:06 -07:00
Frank 4ad85fae70 authentik: add LDAP outpost service for Jellyfin LDAP-Auth
- New authentik-outpost-ldap quadlet: ghcr.io/goauthentik/ldap:2026.8.0,
  publishes host 389/636 -> container 3389/6636 for cross-host binds from
  dankserver's Jellyfin LDAP-Auth plugin; outpost token in host .env.
- deploy-plan.py: register authentik-outpost-ldap in SERVICE_ORDER and
  DANKCLAW_SERVICES; map dankclaw/authentik/quadlet/ unit path.
- deploy-remote.sh: deploy_authentik_outpost_ldap handler + dispatch case.
- deploy.sh: valid-service entry.
2026-09-01 15:25:56 -07:00
Frank f4c9b4f381 seerr: quadlet on dankclaw + guard-chain vhost + deployer wiring
Unit: ghcr.io/seerr-team/seerr:v3.4.1, loopback-only 5055 publish
(homepage probe; users go through Anubis->Authentik->caddy:8017->seerr),
Containers data root, no env secrets. Guard chain: seerr.franksdank.cloud
vhost behind anubis:8923 with post-auth handler on 8017; addn-hosts record;
homepage widget; deploy-remote/deploy-plan/deploy.sh routing; seam test.
2026-09-01 14:40:10 -07:00
Frank 949f68e2b5 frankbook-pro: *arr stack + qbittorrent quadlets inside podman machine
Seven units installed INSIDE the Mac VM (/home/core/.config/containers/
systemd) by a new deploy_frankbook_stack handler: gluetun (ProtonVPN
WireGuard, env-file credentials, publishes qbit's 8080/6881), qbittorrent
(Network=gluetun.container, verbatim /Volumes data roots, first-boot
config seed with fresh machine-local WebUI password and the Finished
Torrents category), and prowlarr/sonarr/radarr/lidarr/readarr (grab-only,
config migrated from dankserver at provisioning, readarr pinned to
amd64-1.0.980). deploy.sh routes the new target through the ssh-config
alias with REMOTE_SHELL=/usr/local/bin/bash (macOS bash 3.2 lacks
associative arrays); the planner learns FRANKBOOK_SERVICES and the
gluetun->qbittorrent netns pairing; new seam test pins the contract.
2026-09-01 14:10:23 -07:00
Frank b206f477ad dankserver: quadlet units + deploy handlers for romm trio, jellyfin, calibre-web, copyparty
Cutover of the six surviving dankserver services from compose-era bare
containers to boot-persistent Quadlet units (WantedBy=default.target; podman
5.x v1 generator honors it at user-manager start).

Units are faithful to live inspect ground truth (image pins = running tags,
not compose inventory):
- romm trio shares one data root: /home/dank/Containers/romm mounted whole at
  /romm; mariadb datadir and redis data dir live inside it. Aliases mariadb/
  redis preserved on dankserver_appnet via Network=...:alias=. Secrets stay in
  the host-local env file (EnvironmentFile + ${VAR} expansion in the generated
  ExecStart, gitea precedent). copyparty passes its admin account via
  Exec=-a ${COPAY_ACCOUNT}:${COPAY_PASSWORD} (Exec= not Command=: the v1
  generator hard-fails on Command=); entropy stays rw only there.
- jellyfin keeps every creation-time env as Environment= lines (zero drift);
  Device=/dev/dri + GroupAdd=keep-groups preserve hardware transcoding.
- calibre-web pins the exact running image tag (id 986277563e76 =
  0.6.27-ls397), pasta network preserved.

Deployer: first-cutover handlers retire the old container, rescue unshadowed
anonymous-volume content (podman volume export + tar --skip-old-files), mv
data roots on the same fs, then start units in dependency order with health
gates. Re-deploys restart only the selected unit. deploy-plan.py maps compose
keys mariadb/redis to the new unit names; allowlists updated. New seam test
tests/dankserver_quadlet.bats pins boot-persistence + v1-generator contract;
test-changed.sh maps the new dirs to it. Full suite green (209 tests).
2026-09-01 04:14:08 -07:00
Frank c3ed3a6e12 sightsound: new FLM NPU sight+sound service on XDNA2 (gemma4-it:e4b)
New Dankclaw service frank-default-sightsound: FLM 1.0.1 serving the
Gemma4-E4B-IT-NPU2 pack (vision+audio+reasoning, 131072 ctx) on the XDNA2
NPU at port 52626. Model choice rationale: gemma4-it is the only NPU2 model
family with BOTH image and audio towers; the uncensored candidate qwythos:9b
is text-only, so sight+sound wins over uncensored here.

Deployer wiring: planner maps env/unit/serve files to the new service name;
deploy-remote gains deploy_llm_sightsound (relink + daemon-reload + enable +
restart + health gate on /v1/version — FLM has no /health route). Unlike the
retired frank-default-npu relink-only path, this unit is boot-enabled.
2026-08-30 04:37:42 -07:00
Frank 999cecda7b fix(deploy): allow caddy in the explicit-service gate
deploy-remote.sh already implements dankclaw:caddy (render + reload);
the top-level gate rejected it before dispatch could reach the handler.
2026-08-28 22:30:44 -07:00
Frank 3a16375cfe deploy: accept authentik-outpost in the top-level service gate 2026-08-28 19:10:35 -07:00
Frank 0ae3650414 anubis: bot-challenge proxy in front of the guarded vhosts (two-tier chain)
Bots and AI scrapers hit the SSO-fronted vhosts and eat Authentik auth
work for 403s. Anubis verifies clients first: DENY for known AI scraper
UAs, JS challenge for everyone else; verified traffic resumes the normal
Authentik forward_auth chain on Caddy's internal listener (:8016).

Chain: client -> Caddy (TLS) -> Anubis :8923 (verify) -> Caddy :8016
(auth + routing resumes; Host survives both hops) -> app. Reverse-proxy
through Anubis, not forward_auth: at the pinned tag a CHALLENGE answers
HTTP 200 + HTML, and forward_auth treats 200 as "authenticated" — the
page would pass through unchallenged. Exempt by construction (never
routed through Anubis): plex (plain HTTP legacy), auth.* (IdP login
must be reachable ungated), gitea (git smart-HTTP clients cannot solve
JS challenges), and every app with its own login.

Pinned ghcr.io/TecharoHQ/anubis:v1.13.0 — the tag was source-verified
before pinning: at this tag the policy parser is JSON-only with a
minimal bots[] schema (no imports, no CEL expressions), flags are the
config surface via flagenv (TARGET/POLICY_FNAME map to --target/
--policy-fname), and the image's exec-form HEALTHCHECK is inherited by
the quadlet unit (dodges the v1 generator's HealthCmd space-split quirk).

Rollback nuance: stopping anubis 502s everything behind the chain (the
public listeners proxy through it); a true rollback is reverting the
Caddyfile and redeploying, not just systemctl --user stop anubis.
2026-08-28 06:38:24 -07:00
Frank 105a4cdfef deploy.sh: accept hermes-webui in the top-level service inventory 2026-08-26 08:22:23 -07:00
Frank d52bcc15f8 Migrate Gitea proxy to canonical Caddy 2026-08-25 21:56:04 -07:00
Frank bc718f04f4 Fix scoped deployment state and config ownership 2026-08-25 20:24:47 -07:00
Frank 1ba47e4d22 Deploy services independently by changed paths 2026-08-25 20:13:33 -07:00
Frank 835996a67b Keep deployment moving when Caddy restart is blocked 2026-08-25 16:55:10 -07:00
Frank eae5bc2565 Continue LLM deployment when compose restart fails 2026-08-25 16:54:18 -07:00
Frank c7a72b465e Fix privileged shared config deployment 2026-08-25 16:53:13 -07:00
Frank 3cc99d81cc Authentik SSO + homepage naked domain
Auth layer (Authentik 2026.8.0):
- compose: 3-service stack (authentik-postgres, authentik-server, authentik-worker),
  postgres health-gated, bind mounts under /home/dank/authentik
- Caddyfile: auth.franksdank.cloud vhost; forward_auth on the no-auth vhosts
  (homepage, SearXNG, Hermes, Prometheus, RomM, Copyparty); outpost path
  proxied first in a route block
- .env.example: AUTHENTIK_PG_PASS + AUTHENTIK_SECRET_KEY
- deploy.sh: render only CADDY_BASICAUTH_HASH (the homepage per-user hashes
  are retired; grafana + pihole keep basicauth)
- tests/authentik.bats (new) + updated homepage/caddyfile/pihole seams
- docs: Authentik.md + per-service pages; Caddy.md + Homepage.md updated

Homepage (naked domain):
- Caddyfile: dankclaw.franksdank.cloud -> franksdank.cloud www.franksdank.cloud;
  dankclaw + dankserver redirect to it; bare-IP catch-all updated
- addn-hosts: A records for the naked domain, www, and auth (all -> 143)
- tests/pihole.bats: vhost_names handles multi-address blocks; TLD check
  allows the naked domain

All 146 tests pass.
2026-08-24 23:48:27 -07:00
Frank c58dab0a89 Homepage: give the user page its own login (dankuser)
The homepage user page + shared data now use a dedicated CADDY_HOMEPAGE_HASH
(username dankuser), separate from the shared family password (CADDY_BASICAUTH_HASH,
username dank, still used by the other vhosts) and the operator password
(CADDY_ADMIN_HASH, username frank, the /admin/ page). This lets the homepage be
re-logged independently without touching the other sites. deploy.sh renders the
new placeholder; the .env.example documents all three hashes.
2026-08-23 21:08:21 -07:00
Frank b485a27784 deploy: run the /srv/homepage steps under sudo
The homepage deploy's mkdir/cp/write of /srv/homepage ran as the deploy user
and hit 'Permission denied' (/srv is root-owned). Prefix them with the sudo
wrapper, write deployed.js via a temp file + sudo cp, and chmod the tree
a+rX so Caddy can serve it. Run the one-off checker as root too (it writes
status.json into /srv/homepage).
2026-08-23 15:44:46 -07:00
Frank f400be76c6 deploy: make the Caddy hash grep defensive under set -e
A missing CADDY_*_HASH in the .env made the 'grep | tail | cut' pipeline
return non-zero, which aborted the deploy under 'set -e' before it could warn
and keep the placeholder. Add '|| true' so an absent hash warns instead of
aborting. (The homepage deploy aborted here because CADDY_ADMIN_HASH was not
in the repo-root .env the deploy reads.)
2026-08-23 15:25:53 -07:00
Frank 2e6c44f3ba Homepage (TDD slice 6): the git-driven deploy
Red tests first.
- tests/homepage_deploy.bats: the deploy ships the homepage (assets to
  /srv/homepage, the checker timer, both Caddy hashes, deployed.js) and is
  git-driven (pulls main); the checker runs as a systemd timer.
- scripts/homepage-status.service / .timer: the checker as a oneshot service
  + a 3-minute timer (Q2/Q35).
- scripts/deploy.sh: the Caddyfile render now substitutes BOTH hashes
  (REPLACE_WITH_HASH <- CADDY_BASICAUTH_HASH, REPLACE_WITH_ADMIN_HASH <-
  CADDY_ADMIN_HASH). A new homepage step copies the assets to /srv/homepage,
  generates deployed.js from the deployed commit's datetime (Q43), installs
  the checker timer, and runs the checker once so status.json exists at once.
- tests/llm_units.bats: scoped the three LLM-unit tests to
  frank-default-*.service (the homepage unit is a different pattern: it
  installs to /usr/local/bin and runs from /srv at deploy time).
2026-08-23 12:59:56 -07:00
Frank 6a9edd4ee0 deploy.sh: keep the relink comment free of a literal loop-var token
The explanatory comment contained an unescaped loop-variable token, which
the local shell (set -u) tried to expand while building the double-quoted
remote script ('unit: unbound variable'). Reword the comment.
2026-08-22 22:36:04 -07:00
Frank 33438f223e deploy.sh: fix LLM unit relink (unquote source path so ${unit} expands on the remote)
The relink loop wrapped the source path in single quotes, which shipped a
literal ${unit} to the remote and created a dangling symlink
(/etc/systemd/system/frank-default-large.service -> .../${unit}.service),
so 'systemctl enable' failed with 'unit does not exist'. The path has no
spaces; unquoting lets ${unit} expand on the remote for both source and
destination.
2026-08-22 22:33:16 -07:00
Frank fbfbc5c6db Move frank-default-large from :52625 to :7770 (dankclaw)
Operator request (2026-08-22): the primary inference server
(frank-default-large, the 27B CIRU model on Dankclaw) moves to the
standard llama.cpp port 7770. Both LLM services now listen on :7770,
distinguished by host (dankserver vs dankclaw).

Functional changes:
- scripts/frank-default-large.env: LISTEN_PORT 52625 -> 7770
- scripts/serve-frank-large.sh: default LISTEN_PORT 52625 -> 7770
- scripts/deploy.sh: LLM_HEALTH[dankclaw] -> :7770
- open-webui quadlet: OPENAI_API_BASE_URL -> 192.168.0.143:7770
  (re-pointed so chat keeps working; data dir preserved)
- scripts/bench-fast-greedy.sh: PORT 52625 -> 7770

Tests updated to the new topology (both LLM endpoints on :7770 by host;
open-webui -> :7770). Docs + pi KB updated; dated historical runbooks
keep their point-in-time ports. models.json (pi client) re-pointed to
:7770 separately, effective on the next client restart.
2026-08-22 22:11:53 -07:00
Frank 0461988884 scripts: unified deploy.sh; units + bench point at the merged checkout
deploy.sh now deploys both workloads per target in one run:
push -> pull on /home/dank/serverAppConfigs -> podman stack steps
(dankclaw) -> LLM unit relink + daemon-reload + service restart ->
homepage health + LLM health (up to 20 min model-load wait).
Sudo: plain sudo on the fleet hosts (passwordless verified
2026-08-22); FRANK_SUDO_PW covers headless runs elsewhere. The
vllmScripts deploy.sh is superseded.

Units: frank-default-{small,large,npu}.service now reference
/home/dank/serverAppConfigs/scripts/... (the merged checkout).
bench-spec-stack.sh: env file path updated to the merged checkout.
2026-08-22 15:50:39 -07:00
Frank 0951fc165c deploy.sh: restart caddy when the reload cannot be applied
A 'caddy reload' delivered over the admin API cannot apply a config
that disables the admin API -- the fix cannot ride the API it is meant
to enable (2026-08-22: admin off sat in the running config across
deploys). Track the reload result; on failure, restart the container
so it reads the rendered file on start.
2026-08-22 13:35:31 -07:00
Frank bb1ef9bfb3 caddy: keep admin on for hot reload; deploy.sh reloads after render
The Caddyfile had 'admin off', which disables Caddy's config file
watcher. Rendered Caddyfile changes never loaded until the container
was recreated, so the double-hash basicauth fix sat on disk unapplied
across deploys (2026-08-22). Admin binds to 127.0.0.1:2019 inside the
container and the port is unpublished, so nothing outside reaches it.

deploy.sh now issues an explicit 'caddy reload' (retry loop) after
'up -d', with the file watcher as backstop.
2026-08-22 13:22:01 -07:00
Frank 5c9ade4e7a deploy.sh: fix quoting of the cfhash label template 2026-08-22 04:16:18 -07:00
Frank e0a25415a1 deploy.sh: rebuild local images when their Containerfile hash changes
buildah can serve a stale COPY --from layer (cache keyed on stage
name), so a Containerfile edit silently produced an unchanged image.
The image now carries a cfhash label; a mismatch forces a --no-cache
rebuild.
2026-08-22 04:14:43 -07:00
Frank dc421efea4 deploy.sh: escape the image-build loop vars for the remote string 2026-08-22 04:04:24 -07:00
Frank 1f4766f233 observability: fix alloy ui component, busybox healthchecks for loki/alloy
- alloy 1.x renamed the server component to ui; the old block failed the
  initial load and the container crash-looped
- grafana/loki is distroless and grafana/alloy lacks wget, so their
  healthchecks could never run; local images add a static busybox from
  alpine for that one command
- deploy.sh builds all three local images if missing
- tests: 3 new seams
2026-08-22 03:59:14 -07:00
Frank d5e9c63802 deploy.sh: drop --pull=missing (podman-compose rejects it) 2026-08-22 02:51:36 -07:00
Frank 5caa2178c3 deploy.sh: extract CADDY_BASICAUTH_HASH via grep/cut, not by sourcing .env
Sourcing the .env in a set -u shell dies on the bcrypt hash's $2
expansion. grep/cut pulls the value without any shell interpretation.
2026-08-22 02:51:13 -07:00
Frank 7f4ce06927 Caddy phase 2: TLS via Porkbun DNS-01 wildcard certs
- Caddyfile: drop http:// site prefixes, global acme_dns porkbun block,
  HTTPS redirects on the bare-IP and legacy vhosts
- new dankclaw/caddy/Containerfile: xcaddy build with caddy-dns/porkbun
- compose: caddy uses franksdank/caddy-porkbun:latest + Porkbun env vars
- deploy.sh: build the image on first deploy if missing
- .env.example: rename PORKBUN_API_SECRET -> PORKBUN_API_SECRET_KEY
  (the name the caddy-dns/porkbun module expects)
- tests: 7 new caddyfile/compose seams for the TLS config
2026-08-22 02:39:41 -07:00
Frank e9277fa8af deploy.sh: declare the per-target arrays (COMPOSE_UP, COPY_SHARED) 2026-08-21 23:17:04 -07:00
Frank 6ea490d64c deploy: sed delimiter '|' for the Caddyfile hash render (bcrypt contains '/')
Plus a regression seam that reproduces the render with a slash-bearing
hash.
2026-08-21 21:48:15 -07:00
Frank 4a679fce80 Caddy consolidation: single Caddy on dankclaw fronts both hosts
Per user direction, Caddy runs only on dankclaw. It fronts every
service on both hosts: local services by container name, quadlet
services by published LAN port, dankserver services by published LAN
port (192.168.0.235:PORT).

- dankclaw/Caddyfile: merged vhost list (was: two Caddyfiles, one per
  host). gitea's single vhost moves in from the retired gitea-caddy
  quadlet (option B). Upstreams fixed to live reality: gitea :3000,
  search :8081 (gluetun), chat :3005 (open-webui quadlet), media
  services via 192.168.0.235. Retired: llms (open-webui on dankserver
  is removed; chat is its successor), the dankserver homepage (302 to
  the shared one).
- dankserver/Caddyfile, dankclaw/gitea/Caddyfile: deleted (git rm).
- dnsmasq addn-hosts: every name -> 192.168.0.143 (the one Caddy
  host); no .235 records, no llms.
- compose.yaml: removed the openwebui/postgres/gitea services (they
  are quadlets now); caddy service mounts the rendered Caddyfile from
  /home/dank/Containers/caddy, healthcheck probes the bare-IP
  catch-all (302).
- deploy.sh: per-target logic (dankserver = pull-only, health via the
  shared Caddy at 192.168.0.143); renders CADDY_BASICAUTH_HASH from
  the host .env into the mounted Caddyfile.
- .env.example: CADDY_BASICAUTH_HASH on dankclaw; Porkbun block
  dropped from dankserver (its Caddy is gone); SEARXNG_BASE_URL to
  the flat name; stale OPENAI_* block dropped (quadlet is
  self-contained).
- tests: caddyfile.bats now asserts the single-Caddyfile invariant;
  dnsmasq.bats cross-checks vhosts -> .143 and the retired names.
2026-08-21 21:44:18 -07:00
Frank e3cd2aca1b SearXNG: add limiter.toml trusted group (LAN /24), wire into deploy
The trusted group lives in limiter.toml, not settings.yml. Add
shared/searxng/limiter.toml trusting 192.168.0.0/24 in pass_ip and
trusted_proxies, copy it into /opt/searxng/config on deploy, and correct
the SearXNG doc. New test seam: tests/searxng_limiter.bats.
2026-08-18 13:55:37 -07:00
Frank e3eb5334cb feat: initialize serverAppConfigs with podman stacks for Dankserver and Dankclaw
- Dankserver stack: RomM, Copyparty, Calibre-Web, node-exporter
- Dankclaw stack: Gluetun VPN (SearXNG tunnel), SearXNG+Valkey, ComfyUI
  (CPU backend), OpenWebUI, Hermes, Gitea+Postgres (migrated), Caddy
  reverse proxy, Grafana+Prometheus+Loki+Alloy observability
- Caddyfiles with .local hostname convention and basicauth homepage
- bind-mount storage policy, no named volumes
- deploy.sh mirroring vllmScripts git workflow (push -> pull -> restart -> health)
- docs/ Obsidian vault: per-service docs from frank-default-small research
  subagents (promtail EOL -> Alloy; gitea SECRET_KEY preservation; ComfyUI --cpu)
- bats test suite at 6 seams — 22 tests green
2026-08-13 20:19:41 -07:00