From bdead570e2f56fbbabc9cea7adb5d8d00889662d Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 13 Oct 2026 22:35:07 -0700 Subject: [PATCH] feat(inventory): one service inventory drives planner, allow-lists, and seams rank 3. shared/services.yml is the single source of truth for every deployable service (hosts, unit dirs, units, path claims, tests, scrape/ dns/probe facts, aliases, stacks); scripts/service-map.py is its only reader (hosts/services/allows/plan/seams/owner/units/stack/get/render/ check). deploy-plan.py becomes a thin CLI wrapper; both --service allow-lists (deploy.sh, deploy-remote.sh) and the changed-test mapper are the inventory. Selection semantics: host scoping (a host's tree selects only its own services), the quadlet netns/network coupling graph resolved across directories (gluetun's riders live in their own dirs), stacks, and canonical restart order. Against the pinned old-planner fixture: 921 raw deltas reduced to 17, all intentional (anubis now maps; cross-host paths select nothing; the retired qwen-sharp template plans nothing and the live froggeric template plans frank-archon; dankserver/romm/romm.container, missed by the old filename rule, plans the romm trio). The fixture is regenerated as the new pin. Integrity gates: schema, unit claims, quadlet-file ownership, path-glob liveness, Pi-hole hosts / Prometheus jobs / DNS / homepage probes drift. tests/service_inventory.bats pins all of it plus the plan fixture and the deploy-plan CLI. Allow-list grep seams in hermes_webui/observability_quadlet become behavioral allows calls. --- AGENTS.md | 40 +- .../20261013-compose-retire}/.env.example | 0 .../20261013-compose-retire}/compose.yaml | 0 .../qwen-sharp-v22.4.0.jinja | 0 scripts/deploy-plan.py | 515 +---- scripts/deploy.sh | 32 +- scripts/service-map.py | 647 ++++++ scripts/test-changed.sh | 247 +-- .../Architecture/Service Inventory.md | 98 + serverAppConfigs-docs/index.md | 1 + shared/services.yml | 598 ++++++ .../compose_validity.bats | 0 tests/changed_tests.bats | 36 +- tests/fixtures/service-plan-golden.tsv | 1773 +++++++++++++++++ tests/hermes_webui.bats | 9 +- tests/observability_quadlet.bats | 13 +- tests/service_inventory.bats | 125 ++ 17 files changed, 3435 insertions(+), 699 deletions(-) rename dankserver/{ => .AGENTTRASH/20261013-compose-retire}/.env.example (100%) rename dankserver/{ => .AGENTTRASH/20261013-compose-retire}/compose.yaml (100%) rename scripts/{ => .AGENTTRASH/20261013-dead-files}/qwen-sharp-v22.4.0.jinja (100%) create mode 100755 scripts/service-map.py create mode 100644 serverAppConfigs-docs/Architecture/Service Inventory.md create mode 100644 shared/services.yml rename tests/{ => .AGENTTRASH/20261013-compose-retire}/compose_validity.bats (100%) create mode 100644 tests/fixtures/service-plan-golden.tsv create mode 100755 tests/service_inventory.bats diff --git a/AGENTS.md b/AGENTS.md index 4d08d4c..cee4cff 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,11 +35,16 @@ the [runbook index](#runbook-index). The full swap history lives in ## Repository Layout -- `dankserver/` — compose.yaml + Caddyfile + .env.example for the media stack -- `dankclaw/` — compose.yaml + Caddyfile + .env.example for the AI stack -- `shared/` — homepage, Prometheus, Loki, Alloy, SearXNG configs -- `scripts/` — `deploy.sh` / `deploy-plan.py` / `deploy-remote.sh`, LLM serve - scripts, systemd units, env tunables, bench harnesses +- `dankserver/` — quadlet units + Caddyfile for the media stack (the + compose.yaml inventory retired 2026-10-13; its mounts had diverged from + the quadlets) +- `dankclaw/` — quadlet units + Caddyfile for the AI stack (compose retired 2026-09-16) +- `shared/` — the service inventory (`services.yml`), homepage, Prometheus, + Loki, Alloy, SearXNG configs +- `scripts/` — `deploy.sh` / `deploy-plan.py` / `deploy-remote.sh` and + `service-map.py` (the inventory's only reader), LLM serve scripts + the + shared artifact gates (`scripts/lib/llm-gates.sh`), systemd units, env + tunables, bench harnesses - `old/` — retired LLM scripts (reference only) - `serverAppConfigs-docs/` — knowledge base (Obsidian vault, OKF format). The managed skill library is distilled there one page per skill-cluster: @@ -70,8 +75,13 @@ All edits flow through git. Never edit files on a remote machine directly. ./scripts/deploy.sh dankclaw --dry-run ``` -The planner maps changed paths to services; the remote runner restarts only -the selected service and records its revision outside the checkout. +The inventory (`shared/services.yml`, read only through +`scripts/service-map.py`) maps changed paths to services; the remote runner +restarts only the selected service and records its revision outside the +checkout. Both `--service` allow-lists, the planner, and the changed-test +seam mapper ARE the inventory — adding, moving, or retiring a service is a +data edit, not a script edit +(`serverAppConfigs-docs/Architecture/Service Inventory.md`). Documentation-only changes are a no-op. LLM services are selected only by their own files — every endpoint (`frank-dragoon`/`frank-zealot`/`frank-archon`) by its unit/env/serve trio in `scripts/` — so @@ -164,12 +174,17 @@ Tests are mandatory before commit (`bats` required on the dev machine): ./tests/run.sh ``` -Seams: compose validity, env consistency (no leaked secrets), Caddyfile +Seams: env consistency (no leaked secrets), Caddyfile validity, quadlet units, docs completeness **including vault link integrity**, script syntax (shell + python), LLM unit integrity, LLM secret hygiene, -deployment flow, service-scoped planning, homepage config/pages/deploy/checker, -and `e2e_scaffold` — which fails if a Playwright spec file's number prefix is in -no project, because such a spec silently never runs. +LLM artifact gates (`llm_gates`/`llm_deploy_gates` — the shared +`scripts/lib/llm-gates.sh` every endpoint's serve and deploy gates run on), +deployment flow, service-scoped planning, the **service inventory** +(`service_inventory.bats`: inventory soundness, the pinned path->service +plan fixture, render drift for the Pi-hole hosts file), homepage +config/pages/deploy/checker, and `e2e_scaffold` — which fails if a +Playwright spec file's number prefix is in no project, because such a spec +silently never runs. See `serverAppConfigs-docs/Architecture/Testing Policy.md`. ## Secret Rules @@ -1041,7 +1056,7 @@ Active: |---------|---------| | frank-dragoon (flash-server 0.16.2, abliterated Swift1.5 v2 .hgn + MTP always-on, 2×262,144, vision ON, Dankclaw) | `serverAppConfigs-docs/LLM/frank-dragoon-flash-server-v2-swift15.md` | | frank-zealot (llama.cpp CUDA, Qwen3.5-9B-Uncensored Q6_K, 2×200,704 exact f16 KV MTP-off, vision ON, Dankclaw) | `serverAppConfigs-docs/LLM/frank-zealot-llama-qwen35-9b-q6k-2x200704.md` | -| frank-archon (radiance StillDeadcode, Qwen3.8-Flash-Next-FP8 native, 2×R9700 TP=2, Dankserver) | `serverAppConfigs-docs/LLM/frank-archon-radiance-qwen38-native-fp8.md` | +| frank-archon (radiance StillDeadcode, Swift1.5-Qwen3.8-Flash-Next GPTQ w4 + bf16-protected experts, int8 trunk, bf16 KV, 2×R9700 TP=2, Dankserver) | `serverAppConfigs-docs/LLM/frank-archon-radiance-swift15-w4dn8.md` | | Dankclaw kernel posture (`amd_iommu=off`) | `serverAppConfigs-docs/LLM/dankclaw-iommu-translation-off.md` | | Deployment flow + incidents | `serverAppConfigs-docs/LLM/deployment.md` | @@ -1050,6 +1065,7 @@ Retired (rollback targets and history): | Runtime | Host | Window | Runbook | |---------|------|--------|---------| | llama.cpp CUDA Gemma4-26B-A4B QAT Q4_K_M, 1×200,704 f16-K/q8_0-V MTP-off (`frank-zealot`) | Dankclaw | 2026-10-07 → 10-07 (same-day swap; rollback target — payload on disk; owns the `attention_k_eq_v` pair-billing KV law and the gemma4-assistant drafter-cost boots) | `frank-zealot-llama-gemma4-q4km-200704.md` | +| radiance StillDeadcode Qwen3.8-Flash-Next native-FP8, 2×R9700 TP=2 (`frank-archon`) | Dankserver | 2026-10-08 → w4dn8 swap (rollback target — `.rad` payload on disk) | `frank-archon-radiance-qwen38-native-fp8.md` | | ROCmFPX llama.cpp fork Qwen3.8-27B Uncensored Q6_0_ROCMFPX_AGENT + MTP (`frank-dragoon`) | Dankclaw | 2026-10-03 → 10-05 (rollback target, payload + fork build on disk) | `frank-dragoon-rocmfpx-q6agent-uncensored-mtp.md` | | tcclaviger vLLM fork Qwen3.8-Flash-Next-Uncensored FP8, TP=2, shape G (`frank-archon`) | Dankserver | 2026-09-29 → 10-05 (rollback target — tag `frank-archon-vllm-shapeG-20261003`; trio + env lore in the tag, image in rootless storage) | `dankserver-qwen38-fp8-expert-offload-tuning.md` | | FreeToken PR #587 ROCm, Qwen3.8-Flash-Next-Uncensored-FP8, ONE R9700 (`frank-archon`) | Dankserver | 2026-10-05 → 10-05 (rollback target — tag `frank-archon-freetoken-pr587-20261005`; trio + Dockerfile in the tag, image `localhost/frank-archon-freetoken:pr587-77a7201` and orcarouter payload on disk) | `frank-archon-freetoken-pr587.md` | diff --git a/dankserver/.env.example b/dankserver/.AGENTTRASH/20261013-compose-retire/.env.example similarity index 100% rename from dankserver/.env.example rename to dankserver/.AGENTTRASH/20261013-compose-retire/.env.example diff --git a/dankserver/compose.yaml b/dankserver/.AGENTTRASH/20261013-compose-retire/compose.yaml similarity index 100% rename from dankserver/compose.yaml rename to dankserver/.AGENTTRASH/20261013-compose-retire/compose.yaml diff --git a/scripts/qwen-sharp-v22.4.0.jinja b/scripts/.AGENTTRASH/20261013-dead-files/qwen-sharp-v22.4.0.jinja similarity index 100% rename from scripts/qwen-sharp-v22.4.0.jinja rename to scripts/.AGENTTRASH/20261013-dead-files/qwen-sharp-v22.4.0.jinja diff --git a/scripts/deploy-plan.py b/scripts/deploy-plan.py index 8decde7..0d95f55 100755 --- a/scripts/deploy-plan.py +++ b/scripts/deploy-plan.py @@ -1,475 +1,68 @@ #!/usr/bin/env python3 -"""Plan runtime services affected by repository changes. +"""deploy-plan.py — map changed paths to deployable services. -The planner is intentionally side-effect free. It accepts either an explicit -path list or two Git revisions. The latter lets compose-file changes resolve -to only the service definitions that actually changed instead of restarting an -entire compose project. +Thin wrapper over scripts/service-map.py, which owns the path->service +semantics in the shared inventory (shared/services.yml): per-service path +rules, the quadlet netns/network coupling graph, host scoping (a host's +directory can only select that host's services), and canonical restart +order. This file keeps the deployer's CLI contract only. + +Usage: + deploy-plan.py --target NAME --repo DIR --base REV --head REV + deploy-plan.py --target NAME [paths...] (paths also on stdin) + +Prints one service name per line (canonical restart order); exit 0 with no +output when nothing relevant changed. """ -from __future__ import annotations - -import argparse +import os import subprocess import sys -from pathlib import PurePosixPath -from typing import Any -import yaml - -# Dependency-sensitive services come before their consumers. Independent -# compose services follow. LLM services are deliberately last and are never -# implied by a generic host, stack, documentation, or config change. -SERVICE_ORDER = ( - "gluetun", - "searxng-valkey", - "searxng", - "marreta", - "pihole", - "open-webui", - "gitea-postgres", - "gitea-server", - "comfyui", - "seerr", - "hermes", - "hermes-dashboard", - "hermes-webui", - "prometheus", - "grafana", - "loki", - "alloy", - "node-exporter", - "alertmanager", - "authentik-postgres", - "authentik-server", - "authentik-worker", - "authentik-outpost", - "authentik-outpost-ldap", - "caddy", - "homepage", - "romm-mariadb", - "romm-redis", - "romm", - "copyparty", - "jellyfin", - "calibre-web", - "calibre-server", - "plex", - "tautulli", - "prowlarr", - "qbittorrent", - "radarr", - "sonarr", - "lidarr", - "readarr", - "frank-dragoon", - "frank-zealot", - "frank-archon", -) - -DANKCLAW_SERVICES = { - "hermes-webui", - "gluetun", - "searxng-valkey", - "searxng", - "marreta", - "pihole", - "open-webui", - "gitea-postgres", - "gitea-server", - "comfyui", - "seerr", - "hermes", - "hermes-dashboard", - "prometheus", - "grafana", - "loki", - "alloy", - "node-exporter", - "alertmanager", - "authentik-postgres", - "authentik-server", - "authentik-worker", - "authentik-outpost", - "authentik-outpost-ldap", - "caddy", - "homepage", - "frank-dragoon", - "frank-zealot", -} - -DANKSERVER_SERVICES = { - "romm-mariadb", - "romm-redis", - "romm", - "copyparty", - "jellyfin", - "calibre-web", - "calibre-server", - "plex", - "tautulli", - "prowlarr", - "qbittorrent", - "radarr", - "sonarr", - "lidarr", - "readarr", - "frank-archon", - "node-exporter", -} - -# FrankBook-Pro runs the *arr stack + qBittorrent inside a podman machine on -# macOS. Service names overlap the Linux hosts (gluetun, qbittorrent, ...); -# the per-target sets and the frankbook-pro intercept in deploy-remote.sh -# keep dispatch unambiguous. -FRANKBOOK_SERVICES = { - "gluetun", - "qbittorrent", - "prowlarr", - "radarr", - "sonarr", - "lidarr", - "readarr", -} - -DANKCLAW_COMPOSE_SERVICES = set() -# Dankserver's compose file is still the source of truth for its media stack; -# Dankclaw's is gone (deleted 2026-09-16). Every service on Dankclaw is a -# Quadlet unit now — the last compose services were the hermes trio, whose units -# have owned their containers since 2026-09-12 and whose compose blocks were -# dead weight that could only create a duplicate container. - -DANKSERVER_COMPOSE_SERVICES = { - "mariadb", - "redis", - "romm", - "copyparty", - "jellyfin", - "calibre-web", -} -COMPOSE_NAME_MAP = {"mariadb": "romm-mariadb", "redis": "romm-redis"} +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +SERVICE_MAP = os.path.join(SCRIPT_DIR, "service-map.py") - -def _add(services: set[str], *names: str) -> None: - services.update(names) - - -def target_services(target: str) -> set[str]: - if target == "dankclaw": - return DANKCLAW_SERVICES - if target == "dankserver": - return DANKSERVER_SERVICES - if target == "frankbook-pro": - return FRANKBOOK_SERVICES - raise ValueError(f"unknown target: {target}") - - -def _compose_services(target: str) -> set[str]: - return DANKCLAW_COMPOSE_SERVICES if target == "dankclaw" else DANKSERVER_COMPOSE_SERVICES - - -def _git_show(repo: str, revision: str, path: str) -> str | None: - result = subprocess.run( - ["git", "-C", repo, "show", f"{revision}:{path}"], - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - check=False, - ) - return result.stdout if result.returncode == 0 else None - - -def _yaml_document(contents: str | None) -> dict[str, Any]: - if contents is None: - return {} - try: - data = yaml.safe_load(contents) or {} - except yaml.YAMLError: - return {} - return data if isinstance(data, dict) else {} - - -def _yaml_services(contents: str | None) -> dict[str, Any]: - services = _yaml_document(contents).get("services", {}) - return services if isinstance(services, dict) else {} - - -def compose_changes(repo: str, target: str, base: str, head: str) -> set[str]: - """Return only compose services whose YAML definitions or dependencies changed. - - A target with no compose file (Dankclaw since 2026-09-16) plans nothing: - its services are Quadlet units, selected by their own paths. - """ - path = f"{target}/compose.yaml" - if not _git_show(repo, head, path) and not _git_show(repo, base, path): - return set() - old_doc = _yaml_document(_git_show(repo, base, path)) - new_doc = _yaml_document(_git_show(repo, head, path)) - old = old_doc.get("services", {}) if isinstance(old_doc.get("services", {}), dict) else {} - new = new_doc.get("services", {}) if isinstance(new_doc.get("services", {}), dict) else {} - names = set(old) | set(new) - allowed = _compose_services(target) - - # An unavailable or malformed revision is safer as a broad compose plan - # than as a silent no-op. It still cannot include LLM units. - if (old_doc and not new_doc) or (new_doc and not old_doc and names): - return {COMPOSE_NAME_MAP.get(n, n) for n in allowed} - - # Top-level networks/configuration can affect every compose service even - # when no individual service mapping changed. Ignore the services key while - # comparing the rest of the Compose document. - old_globals = {key: value for key, value in old_doc.items() if key != "services"} - new_globals = {key: value for key, value in new_doc.items() if key != "services"} - if old_globals != new_globals: - return {COMPOSE_NAME_MAP.get(n, n) for n in allowed} - - changed = {name for name in names if name in allowed and old.get(name) != new.get(name)} - return {COMPOSE_NAME_MAP.get(n, n) for n in changed} - - -def services_for_path(path: str, target: str) -> set[str]: - """Return services affected by one repository-relative path.""" - path = path.replace("\\", "/") - p = PurePosixPath(path) - parts = p.parts - services: set[str] = set() - if not parts: - return services - - # Non-runtime material is deliberately a no-op. - if parts[0] in {"docs", "tests", "old", ".githooks"}: - return services - if path in {"README.md", "AGENTS.md"} or parts[0] == ".obsidian": - return services - - if parts[0] == "shared": - if len(parts) < 2: - return services - group = parts[1] - if group == "searxng": - _add(services, "searxng") - elif group == "prometheus": - _add(services, "prometheus") - if len(parts) >= 3 and parts[2] == "alertmanager.yml": - # The Alertmanager config ships from this directory: a change - # to it must redeploy the service that installs it. - _add(services, "alertmanager") - elif group == "loki": - _add(services, "loki") - elif group == "alloy": - _add(services, "alloy") - elif group == "grafana": - # Provisioned datasources and dashboards: git is the source of - # truth, so a change here must restart the service that reads them. - _add(services, "grafana") - elif group == "homepage": - # Homepage is a Dankclaw-owned deployment. Shared homepage files - # are intentionally not copied during a Dankserver-only run. - if target == "dankclaw": - _add(services, "homepage") - return services - - if parts[0] == "dankclaw": - if len(parts) >= 2 and parts[1] == "compose.yaml": - # Dankclaw has no compose file since 2026-09-16: every service is a - # Quadlet unit, each selected by its own path. A change (or a - # deletion) of this path therefore restarts nothing. - return services - if len(parts) >= 2 and parts[1] == "Caddyfile": - _add(services, "caddy") - return services - if len(parts) >= 2 and parts[1] == "Containerfile.comfyui": - _add(services, "comfyui") - return services - if len(parts) >= 2 and parts[1] in {"caddy", "comfyui", "loki", "alloy", "hermes", "anubis", "seerr"}: - _add(services, parts[1]) - return services - if len(parts) >= 4 and parts[1] == "authentik" and parts[2] == "quadlet": - name = parts[3].removesuffix(".container") - if name in {"authentik-postgres", "authentik-server", "authentik-worker", "authentik-outpost", "authentik-outpost-ldap"}: - _add(services, name) - return services - if len(parts) >= 2 and parts[1] == "searxng": - if len(parts) >= 4 and parts[2] == "quadlet": - name = parts[3] - if name == "gluetun.container": - # SearXNG and Marreta (+ its browser companion) share - # Gluetun's network namespace. A Gluetun recreation - # tears the netns down, so every sharer must restart. - _add(services, "gluetun", "searxng", "marreta") - elif name == "searxng.container": - _add(services, "searxng") - elif name == "searxng-valkey.container": - _add(services, "searxng-valkey", "searxng") - elif name == "searxng.network": - _add(services, "gluetun", "searxng-valkey", "searxng", "marreta") - return services - if len(parts) >= 2 and parts[1] == "marreta": - # One deployable owns both the app and its browser companion; - # both live in gluetun's netns. A change to either unit selects - # the marreta service, which restarts browser-then-app. - _add(services, "marreta") - return services - if len(parts) >= 2 and parts[1] == "pihole": - _add(services, "pihole") - return services - if len(parts) >= 2 and parts[1] == "open-webui": - _add(services, "open-webui") - return services - # frank-dragoon left the quadlet shape on 2026-09-19 (CIRU v4.4.1 host - # build, root unit): its files live in scripts/ and map there. - if len(parts) >= 2 and parts[1] == "observability": - # The five observability containers became Quadlet units on - # 2026-09-11. A unit change selects exactly its own service; - # SERVICE_ORDER already puts each dependency ahead of its - # consumer (prometheus before grafana, loki before alloy). - if len(parts) >= 4 and parts[2] == "quadlet": - name = parts[3].removesuffix(".container") - if name in {"prometheus", "grafana", "loki", "alloy", "node-exporter", "alertmanager"}: - _add(services, name) - return services - if len(parts) >= 2 and parts[1] == "gitea": - if len(parts) >= 4 and parts[2] == "quadlet": - name = parts[3] - if name == "gitea-postgres.container": - _add(services, "gitea-postgres") - elif name == "gitea-server.container": - _add(services, "gitea-server") - elif name == "gitea-caddy.container": - # Removing the old split proxy requires the canonical - # Caddy handler to retire its installed unit/container - # before it claims ports 80/443. - _add(services, "caddy") - elif name == "gitea.network": - _add(services, "gitea-postgres", "gitea-server") - return services - return services - - if parts[0] == "dankserver": - if len(parts) >= 2 and parts[1] == "compose.yaml": - # Compose service keys map onto the deployed unit names. - _add(services, *(COMPOSE_NAME_MAP.get(n, n) for n in DANKSERVER_COMPOSE_SERVICES)) - return services - if len(parts) >= 3 and parts[2] == "quadlet": - quadlet_services = { - "romm": {"romm", "romm-mariadb", "romm-redis"}, - "jellyfin": {"jellyfin"}, - "calibre-web": {"calibre-web"}, - "calibre-server": {"calibre-server"}, - "copyparty": {"copyparty"}, - "plex": {"plex"}, - "tautulli": {"tautulli"}, - } - _add(services, *quadlet_services.get(parts[1], set())) - if len(parts) >= 2 and parts[1] == "observability": - # Dankserver's node-exporter became a Quadlet unit on 2026-09-16 - # (serverAppConfigs-docs/Infrastructure/Observability.md): its - # compose service died at a reboot and stayed dead, so Prometheus' - # node-dankserver job had nothing to scrape. - if len(parts) >= 4 and parts[2] == "quadlet": - name = parts[3].removesuffix(".container") - if name == "node-exporter": - _add(services, "node-exporter") - return services - # frank-archon's quadlet-era directory left the tree on 2026-09-26 - # (the Lucebox DS4.1 unit was retired for the llama.cpp root unit — - # the trio in scripts/ now maps it). Nothing lives under - # dankserver/frank-archon/ except dated trash, and trash plans - # nothing (the retire rule above the service blocks). - return services - - if parts[0] == "frankbook-pro": - if len(parts) >= 3 and parts[2] == "quadlet": - name = parts[3].removesuffix(".container") - if name == "gluetun": - # Every *arr + qBittorrent container shares gluetun's - # network namespace; a gluetun recreation kills all of - # them, so the plan restarts the whole netns family. - _add(services, "gluetun", "qbittorrent", - "prowlarr", "sonarr", "radarr", "lidarr", "readarr") - elif name in FRANKBOOK_SERVICES: - _add(services, name) - return services - - if parts[0] == "scripts": - name = p.name - if name in {"homepage-status.py", "homepage-status.service", "homepage-status.timer"}: - _add(services, "homepage") - if name in {"signal-relay.py", "signal-relay.service"}: - # The Signal relay is installed by the alertmanager deploy, so a - # change to it redeploying alertmanager is the only path that - # reaches it on the host. - _add(services, "alertmanager") - if name in {"frank-zealot.env", "frank-zealot.service", "serve-frank-zealot.sh"}: - # llama.cpp frank-zealot (2026-09-16) keeps its unit, env and serve - # files here; each one selects frank-zealot only. The ik_llama.cpp - # trio that lived here for two days is in scripts/.AGENTTRASH/, - # which is archive material and plans nothing. - _add(services, "frank-zealot") - if name in {"frank-dragoon.env", "frank-dragoon.service", "serve-frank-dragoon.sh"}: - # CIRU v4.4.1 frank-dragoon (2026-09-19) keeps its unit, env and - # serve files here the same way: a root unit on a host-built - # runtime. The download script fetches the payload, which is - # host-side state, so it selects nothing. - _add(services, "frank-dragoon") - if name == "qwen-sharp-v22.4.0.jinja": - # The Qwen-Sharp chat template frank-dragoon loads via - # --chat-template-file. It lived under dankserver/frank-medium/ until - # the 2026-09-26 cutover retired that directory; editing it changes - # what the live frank-dragoon endpoint renders, so it plans - # frank-dragoon. - _add(services, "frank-dragoon") - if name in {"frank-archon.env", "frank-archon.service", "serve-frank-archon.sh"}: - # llama.cpp frank-archon (2026-09-26, replacing the Lucebox - # DS4.1 quadlet) keeps its unit, env and serve files here the - # same way: a root unit on the unsloth b11030-mix prebuilt - # serving Qwen3.8-Flash-Next Q8_0. The download script fetches - # the payload, which is host-side state, so it selects nothing. - _add(services, "frank-archon") - # Deployment/test/benchmark scripts and other repo tooling do not - # restart a runtime service. - return services - - # Examples and env templates are not deployed runtime state. - if p.name in {".env.example", "env.example"}: - return services - return services - -def changed_paths(repo: str, base: str, head: str) -> list[str]: - result = subprocess.run( - ["git", "-C", repo, "diff", "--name-only", "--diff-filter=ACDMRTUXB", base, head], - text=True, - stdout=subprocess.PIPE, - check=True, - ) - return [line for line in result.stdout.splitlines() if line] - - -def plan(paths: list[str], target: str, repo: str | None = None, - base: str | None = None, head: str | None = None) -> list[str]: - services: set[str] = set() - for path in paths: - if path == f"{target}/compose.yaml" and repo and base and head: - services.update(compose_changes(repo, target, base, head)) - else: - services.update(services_for_path(path, target)) - allowed = target_services(target) - return [name for name in SERVICE_ORDER if name in services and name in allowed] +def git_paths(repo: str, base: str, head: str) -> list[str]: + proc = subprocess.run( + ["git", "-C", repo, "diff", "--name-only", base, head], + text=True, capture_output=True) + if proc.returncode != 0: + sys.stderr.write(f"deploy-plan: git diff failed: {proc.stderr}") + raise SystemExit(1) + return [line for line in proc.stdout.splitlines() if line] def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--target", choices=("dankclaw", "dankserver", "frankbook-pro"), required=True) - parser.add_argument("--paths", nargs="+") - parser.add_argument("--repo", help="repository path for revision-aware planning") - parser.add_argument("--base", help="base Git revision") - parser.add_argument("--head", help="head Git revision") - args = parser.parse_args() - - if not args.paths and not (args.repo and args.base and args.head): - parser.error("provide --paths or all of --repo, --base, and --head") - paths = args.paths or changed_paths(args.repo, args.base, args.head) - print("\n".join(plan(paths, args.target, args.repo, args.base, args.head))) + args = sys.argv[1:] + if not args or args[0] != "--target": + print(__doc__, file=sys.stderr) + return 2 + if len(args) < 2: + print("missing target name after --target", file=sys.stderr) + return 2 + target = args[1] + # `--paths` is the historic seam keyword: a flag, not an option — the + # paths are the positional arguments that follow it. + rest = [a for a in args[2:] if a != "--paths"] + if "--repo" in rest: + try: + repo = rest[rest.index("--repo") + 1] + base = rest[rest.index("--base") + 1] + head = rest[rest.index("--head") + 1] + except (ValueError, IndexError): + print("--repo, --base and --head must appear together", file=sys.stderr) + return 2 + paths = git_paths(repo, base, head) + else: + paths = rest or [line.strip() for line in sys.stdin if line.strip()] + if not paths: + return 0 + proc = subprocess.run([sys.executable, SERVICE_MAP, "plan", "--host", target] + paths, + text=True, capture_output=True) + if proc.returncode != 0: + sys.stderr.write(proc.stderr) + return proc.returncode + sys.stdout.write(proc.stdout) return 0 diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 99a1353..7731b2e 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -131,31 +131,13 @@ else fi valid_service() { - local service="$1" - case "$TARGET:$service" in - dankclaw:caddy|dankclaw:gluetun|dankclaw:searxng-network|dankclaw:searxng-valkey|\ - dankclaw:searxng|dankclaw:marreta|dankclaw:pihole|dankclaw:open-webui|dankclaw:gitea-postgres|\ - dankclaw:gitea-server|dankclaw:homepage|\ - dankclaw:comfyui|dankclaw:hermes|dankclaw:hermes-dashboard|\ - dankclaw:hermes-webui|dankclaw:prometheus|dankclaw:grafana|dankclaw:loki|dankclaw:alloy|\ - dankclaw:alertmanager|\ - dankclaw:node-exporter|dankclaw:authentik-postgres|dankclaw:authentik-server|\ - dankclaw:authentik-worker|dankclaw:authentik-outpost|dankclaw:authentik-outpost-ldap|dankclaw:anubis|dankclaw:seerr|\ - dankclaw:frank-dragoon|dankclaw:frank-zealot|\ - dankserver:romm-mariadb|dankserver:romm-redis|dankserver:romm|\ - dankserver:copyparty|dankserver:jellyfin|dankserver:calibre-web|dankserver:calibre-server|dankserver:plex|dankserver:tautulli|\ - dankserver:qbittorrent|dankserver:radarr|dankserver:sonarr|\ - dankserver:lidarr|dankserver:readarr|dankserver:node-exporter|\ - dankserver:frank-archon|\ - frankbook-pro:gluetun|frankbook-pro:qbittorrent|frankbook-pro:prowlarr|\ - frankbook-pro:radarr|frankbook-pro:sonarr|frankbook-pro:lidarr|\ - frankbook-pro:readarr) - return 0 - ;; - *) - return 1 - ;; - esac + # The inventory is the only allow-list (see scripts/service-map.py); + # service-map's "allows" exit code is the answer. The former literal + # case list here drifted from reality (it still listed the *arr stack + # under dankserver, which moved to frankbook-pro, and a + # "searxng-network" name that is a generated systemd unit, not a + # deployable service). + python3 "$REPO_ROOT/scripts/service-map.py" allows --host "$TARGET" --service "$1" >/dev/null } for service in "${EXPLICIT_SERVICES[@]}"; do diff --git a/scripts/service-map.py b/scripts/service-map.py new file mode 100755 index 0000000..4485cff --- /dev/null +++ b/scripts/service-map.py @@ -0,0 +1,647 @@ +#!/usr/bin/env python3 +"""service-map.py — the only reader of the service inventory. + +Four lists used to agree by discipline (the planner's path rules, the +deployer's allow-list, the changed-test mapper, and the scrape/probe/DNS +facts written by hand in three config files). They now all read +`shared/services.yml` through this tool: + + hosts the deploy targets + services [--host H] deployables a target accepts, canonical order + allows --host H --service S exit 0 when H accepts S (silent) + plan --host H PATH... services selected by changed paths, order-safe + seams [--host H] PATH... the Bats seams a changed path must re-run + owner --host H PATH the service that owns a file (empty: none) + units --host H --service S " " lines the handler installs + stack --host H --service S the deployable + members one handler deploys + get --service S [--host H] F one field as JSON + render pihole-hosts the Pi-hole addn-hosts file + check [NAME...] integrity + drift gates (exit 1 on drift) + +Two laws the engine enforces structurally: + +* A path selects only services that RUN on the planned target. The old planner + matched paths host-blind and filtered at the end, so a frankbook-pro file + could plan a dankserver service (the stale *arr entries made that reachable). +* The netns family is DERIVED, never declared: a unit file that says + `Network=gluetun.container` rides that tunnel, so a gluetun change selects + every sharer. The unit files are the source of that coupling. +""" +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +from functools import lru_cache +from pathlib import Path, PurePosixPath +from typing import Any + +import yaml + +REPO_ROOT = Path(__file__).resolve().parent.parent +DEFAULT_INVENTORY = REPO_ROOT / "shared" / "services.yml" + +REQUIRED_KEYS = ("name", "hosts", "kind", "tests") +KINDS = {"quadlet", "root-unit", "vm-quadlet", "host"} +UNIT_SUFFIXES = (".container", ".network") + +# Repo material that is never a deployed runtime's state. +NON_RUNTIME_ROOTS = {"docs", "report", "old", ".githooks", ".obsidian", ".github", + "tests", "serverAppConfigs-docs"} +NON_RUNTIME_NAMES = {".env.example"} +NON_RUNTIME_PATHS = {"README.md", "AGENTS.md", ".gitignore"} + + +def is_trash(path: str) -> bool: + return ".AGENTTRASH" in PurePosixPath(path).parts + + +# -------------------------------------------------------------------------- +# inventory +# -------------------------------------------------------------------------- +class Inventory: + def __init__(self, doc: dict[str, Any], origin: str) -> None: + self.origin = origin + self.hosts: dict[str, dict[str, Any]] = doc["hosts"] + self.services: list[dict[str, Any]] = doc["services"] + self.unowned: dict[str, str] = doc.get("unowned", {}) + for index, svc in enumerate(self.services): + svc["_index"] = index + + @classmethod + def load(cls, path: str | Path = DEFAULT_INVENTORY) -> "Inventory": + doc = yaml.safe_load(Path(path).read_text()) + if not isinstance(doc, dict) or "services" not in doc: + raise SystemExit(f"{path}: not a service inventory document") + return cls(doc, str(path)) + + def on(self, host: str) -> list[dict[str, Any]]: + return [s for s in self.services if host in s["hosts"]] + + def named(self, host: str, name: str) -> list[dict[str, Any]]: + return [s for s in self.services if s["name"] == name and host in s["hosts"]] + + def any_named(self, name: str) -> list[dict[str, Any]]: + return [s for s in self.services if s["name"] == name] + + @property + def order(self) -> list[str]: + seen: list[str] = [] + for svc in self.services: + if svc["name"] not in seen: + seen.append(svc["name"]) + return seen + + def edge_ip(self) -> str: + """The single address every DNS name resolves at (Caddy's edge).""" + for host in self.hosts.values(): + if "dns_edge" in host: + return str(host["dns_edge"]) + raise SystemExit(f"{self.origin}: no host declares dns_edge") + + def unit_dirs(self, svc: dict[str, Any], host: str | None = None) -> list[str]: + """The repo dirs holding this service's unit files, for one host.""" + dirs = svc.get("unit_dir") + if not dirs: + return [] + if isinstance(dirs, dict): + if host is not None: + return [dirs[host]] if host in dirs else [] + return [dirs[h] for h in svc["hosts"] if h in dirs] + return [dirs] + + def owns_unit(self, svc: dict[str, Any], host: str, unit: str, directory: str) -> bool: + return unit in svc.get("units", []) and directory in self.unit_dirs(svc, host) + + +# -------------------------------------------------------------------------- +# repo facts +# -------------------------------------------------------------------------- +@lru_cache(maxsize=1) +def tracked_files() -> tuple[str, ...]: + # Tracked + untracked (a check that only saw HEAD would ignore the very + # files a work-in-progress commit is adding). + out = subprocess.run( + ["git", "-C", str(REPO_ROOT), "ls-files", "--cached", "--others", + "--exclude-standard"], + text=True, stdout=subprocess.PIPE, check=True) + return tuple(line for line in out.stdout.splitlines() if line) + + +def _network_values(container: Path) -> list[str]: + """Every `Network=` value a quadlet unit declares (comma lists included).""" + values: list[str] = [] + try: + text = container.read_text() + except OSError: + return values + for line in text.splitlines(): + if line.startswith("Network="): + values.extend(v.strip() for v in line[len("Network="):].split(",") if v.strip()) + return values + + +class Graph: + """The unit-file coupling, read out of the tree: netns + netns-net users. + + Quadlet installs every user unit flat, so `Network=gluetun.container` + names a unit by name, not by path; the same host is the scope, which is + also the deployment scope. + """ + + def __init__(self, hosts: list[str]) -> None: + self.units: dict[tuple[str, str], str] = {} # (host, unit) -> path + self.networks: dict[tuple[str, str], str] = {} # (host, net) -> path + self.netns_children: dict[tuple[str, str], set[tuple[str, str]]] = {} + self.network_users: dict[tuple[str, str], set[tuple[str, str]]] = {} + for path in tracked_files(): + parts = PurePosixPath(path).parts + if not parts or parts[0] not in hosts or is_trash(path): + continue + host, directory, name = parts[0], str(PurePosixPath(*parts[1:-1])), parts[-1] + if name.endswith(".container"): + self.units[(host, name[: -len(".container")])] = path + elif name.endswith(".network"): + self.networks[(host, name[: -len(".network")])] = path + for (host, unit), path in self.units.items(): + for value in _network_values(REPO_ROOT / path): + if value.endswith(".container"): + parent = (host, value[: -len(".container")]) + self.netns_children.setdefault(parent, set()).add((host, unit)) + elif value.endswith(".network"): + net = (host, value[: -len(".network")]) + self.network_users.setdefault(net, set()).add((host, unit)) + + def rides(self, start: list[tuple[str, str]]) -> set[tuple[str, str]]: + """The transitive netns closure: a unit, everything riding it, and + everything riding those (a netns inside a netns).""" + found: set[tuple[str, str]] = set() + frontier = list(start) + while frontier: + node = frontier.pop() + if node in found: + continue + found.add(node) + frontier.extend(self.netns_children.get(node, ())) + return found + + +@lru_cache(maxsize=8) +def graph_for(hosts: tuple[str, ...]) -> Graph: + return Graph(list(hosts)) + + +# -------------------------------------------------------------------------- +# selection +# -------------------------------------------------------------------------- +def _owner_index(inv: Inventory) -> dict[tuple[str, str, int]]: + """(host, unit) -> service index, for unit-file ownership. + + A quadlet unit name is unique per host (``check units`` enforces one + claim), so the coupling closure can resolve a riding unit that lives in + a DIFFERENT directory than the changed file — gluetun's netns children + are the point. + """ + index: dict[tuple[str, str], int] = {} + for svc in inv.services: + for host in svc["hosts"]: + for unit in svc.get("units", []): + key = (host, unit) + if key in index and index[key] != svc["_index"]: + raise SystemExit( + f"{inv.origin}: unit {unit!r} on {host} claimed by " + f"{inv.services[index[key]]['name']} and {svc['name']}" + ) + index[key] = svc["_index"] + return index + + +def select(inv: Inventory, host: str, paths: list[str]) -> list[str]: + """Canonical-order service names the changed paths select for one host.""" + if host not in inv.hosts: + raise SystemExit(f"{inv.origin}: no host {host!r}") + graph = graph_for(tuple(inv.hosts)) + owners = _owner_index(inv) + hits: set[int] = set() + + def add(index: int) -> None: + hits.add(index) + + for raw in paths: + path = raw.replace("\\", "/") + parts = PurePosixPath(path).parts + if not parts or is_trash(path) or path in NON_RUNTIME_PATHS: + continue + if PurePosixPath(path).name in NON_RUNTIME_NAMES: + continue + head = parts[0] + if head in NON_RUNTIME_ROOTS: + continue + # A host's directory is the scope: a frankbook-pro file can never + # select a dankserver service. + scoped_host = head if head in inv.hosts else None + if scoped_host is not None and scoped_host != host: + continue + + name = parts[-1] + directory = str(PurePosixPath(*parts[1:-1])) if len(parts) > 1 else "" + + if scoped_host and name.endswith(UNIT_SUFFIXES): + stem = name.rsplit(".", 1)[0] + key = (scoped_host, stem) + if name.endswith(".network") and key in graph.networks: + users = graph.network_users.get(key, set()) + for unit in graph.rides(sorted(users)): + index = owners.get(unit) + if index is not None: + add(index) + elif key in graph.units: + for unit in graph.rides([key]): + index = owners.get(unit) + if index is not None: + add(index) + + for index in list(hits): + for dependent in inv.services[index].get("dependents", []): + for svc in inv.named(host, dependent): + add(svc["_index"]) + + for svc in inv.on(host): + if path in svc.get("files", []): + add(svc["_index"]) + if path in svc.get("retires", []): + add(svc["_index"]) + for directory_rule in svc.get("dirs", []): + if path == directory_rule or path.startswith(directory_rule + "/"): + add(svc["_index"]) + + return [svc["name"] for svc in sorted((inv.services[i] for i in hits), + key=lambda s: s["_index"])] + + +def seams_for(inv: Inventory, host: str, paths: list[str]) -> list[str]: + """The Bats seams the selected services require, deduplicated, in order.""" + seams: list[str] = [] + for name in select(inv, host, paths): + for svc in inv.named(host, name): + for seam in svc.get("tests", []): + if seam not in seams: + seams.append(seam) + if seams and "tests/deployment_scoping.bats" not in seams: + seams.append("tests/deployment_scoping.bats") + return seams + + +# -------------------------------------------------------------------------- +# renderers + facts +# -------------------------------------------------------------------------- +def render_pihole_hosts(inv: Inventory) -> str: + edge = inv.edge_ip() + lines = [ + "# A records for the flat franksdank.cloud scheme (dnsmasq addn-hosts", + "# format: ). RENDERED — do not edit by hand:", + "#", + "# scripts/service-map.py render pihole-hosts > dankclaw/pihole/addn-hosts", + "#", + "# The source of truth is the `dns:` field of shared/services.yml; the", + "# drift gate in tests/service_inventory.bats fails when this file and", + "# the inventory disagree. Caddy is consolidated on Dankclaw, so every", + "# name — including the services running on dankserver and", + "# frankbook-pro — resolves at the Dankclaw edge.", + "#", + "# dnsmasq strips ONLY whole-line comments in addn-hosts: a trailing", + "# comment on a data line becomes a HOSTNAME token. No inline notes", + "# here; the service attribution lives in the inventory.", + "", + ] + for svc in inv.services: + for name in svc.get("dns", []): + lines.append(f"{edge} {name}") + return "\n".join(lines) + "\n" + + +def scrape_facts(inv: Inventory) -> list[dict[str, str]]: + facts = [] + for svc in inv.services: + for job in svc.get("scrape", []): + facts.append({"job": job["job"], "target": job["target"], + "metrics_path": job.get("metrics_path", "/")}) + return facts + + +def prometheus_facts(path: Path) -> list[dict[str, str]]: + doc = yaml.safe_load(path.read_text()) + facts = [] + for job in doc.get("scrape_configs", []): + for sc in job.get("static_configs", []): + for target in sc.get("targets", []): + facts.append({"job": job["job_name"], "target": target, + "metrics_path": job.get("metrics_path", "/")}) + return facts + + +def probe_facts(inv: Inventory) -> list[str]: + facts: list[str] = [] + for svc in inv.services: + probe = svc.get("probe") + if isinstance(probe, dict): + facts.extend(str(v) for v in probe.values()) + elif probe: + facts.append(str(probe)) + return facts + + +def homepage_probes(path: Path) -> list[str]: + return sorted({s["probe"] for s in json.loads(path.read_text()) if s.get("probe")}) + + +# -------------------------------------------------------------------------- +# checks +# -------------------------------------------------------------------------- +def check_ownership(inv: Inventory, graph: Graph) -> list[str]: + """Every repo file belongs to a service, a rule, or the unowned list.""" + problems: list[str] = [] + exact: set[str] = set() + directories: set[str] = set() + for svc in inv.services: + for host in svc["hosts"]: + for directory in inv.unit_dirs(svc, host): + for unit in svc.get("units", []): + exact.add(f"{directory}/{unit}.container") + for field in ("files", "retires"): + exact.update(svc.get(field, [])) + directories.update(svc.get("dirs", [])) + # A shared network file is claimed by the units that ride it; the unit + # graph is the referrer, and check_units proves every reference resolves. + exact.update(graph.networks.values()) + + def is_claimed(path: str) -> bool: + if path in exact: + return True + first = PurePosixPath(path).parts[0] + if first in NON_RUNTIME_ROOTS or path in NON_RUNTIME_PATHS or is_trash(path): + return True + if PurePosixPath(path).name in NON_RUNTIME_NAMES: + return True + for owned in directories | set(inv.unowned): + if path == owned or path.startswith(owned + "/"): + return True + return False + + for path in tracked_files(): + if not is_claimed(path): + problems.append(f"file belongs to no service and is not in unowned: {path}") + + # The reverse direction: an unowned entry that names nothing is stale. + for directory in inv.unowned: + if not any(p == directory or p.startswith(directory + "/") for p in tracked_files()): + problems.append(f"unowned entry names no tracked file: {directory}") + + # Every inventory file/dir claim names a tracked file. + for svc in inv.services: + # `retires` names a file whose DELETION selects the service, so by + # definition it is not in the tree. + for field in ("files", "dirs"): + for entry in svc.get(field, []): + if entry not in tracked_files() and not any( + p.startswith(entry + "/") for p in tracked_files()): + problems.append(f"{svc['name']}: {field} entry names no tracked file: {entry}") + del graph + return problems + + +def check_units(inv: Inventory, graph: Graph) -> list[str]: + """Every unit file has exactly one owner; every unit reference resolves.""" + problems: list[str] = [] + owners: dict[str, list[str]] = {} + for svc in inv.services: + for host in svc["hosts"]: + for directory in inv.unit_dirs(svc, host): + for unit in svc.get("units", []): + path = f"{directory}/{unit}.container" + owners.setdefault(path, []).append(f"{svc['name']}@{host}") + if path not in tracked_files(): + problems.append(f"{svc['name']}: unit file is not tracked: {path}") + for path in tracked_files(): + if not path.endswith(".container") or is_trash(path): + continue + found = owners.get(path, []) + if not found: + problems.append(f"unit file claimed by no service: {path}") + elif len(found) > 1: + problems.append(f"unit file claimed by {len(found)} services: {path}: {found}") + + # Root units (scripts/*.service) are claimed by exactly one `files:` list. + for path in tracked_files(): + if not (path.startswith("scripts/") and path.endswith(".service")) or is_trash(path): + continue + found = [s["name"] for s in inv.services if path in s.get("files", [])] + if not found: + problems.append(f"root unit claimed by no service: {path}") + elif len(found) > 1: + problems.append(f"root unit claimed by {found}: {path}") + + # Every Network= reference in a unit file names a file on the same host. + for (host, unit), path in graph.units.items(): + for value in _network_values(REPO_ROOT / path): + if value.endswith(".container"): + target = (host, value[: -len(".container")]) + if target not in graph.units: + problems.append(f"{path}: Network={value} names no unit on {host}") + elif value.endswith(".network"): + target = (host, value[: -len(".network")]) + if target not in graph.networks: + problems.append(f"{path}: Network={value} names no network on {host}") + return problems + + +def check_config(inv: Inventory) -> list[str]: + problems: list[str] = [] + for svc in inv.services: + label = f"{'+'.join(svc['hosts'])}/{svc['name']}" + for key in REQUIRED_KEYS: + if key not in svc: + problems.append(f"{label}: missing required key {key}") + if svc.get("kind") not in KINDS: + problems.append(f"{label}: kind {svc.get('kind')!r} not one of {sorted(KINDS)}") + unknown = set(svc.get("hosts", [])) - set(inv.hosts) + if unknown: + problems.append(f"{label}: undeclared hosts: {sorted(unknown)}") + if svc.get("units") and not svc.get("unit_dir"): + problems.append(f"{label}: units declared without unit_dir") + if svc.get("deployed_by") and not inv.any_named(svc["deployed_by"]): + problems.append(f"{label}: deployed_by names no service: {svc['deployed_by']}") + if (svc.get("health") and svc.get("wait") is None) or \ + (svc.get("wait") is not None and not svc.get("health")): + problems.append(f"{label}: health and wait are a pair") + for seam in svc.get("tests", []): + if seam not in tracked_files(): + problems.append(f"{label}: seam does not exist: {seam}") + for name in svc.get("dns", []): + if not str(name).replace(".", "").replace("-", "").isalnum(): + problems.append(f"{label}: dns name is not a plain hostname: {name}") + if svc.get("kind") == "root-unit" and svc.get("unit_dir"): + problems.append(f"{label}: a root unit lives in scripts/, not unit_dir") + seen: set[str] = set() + for svc in inv.services: + key = (svc["name"], tuple(svc["hosts"])) + if key in seen: + problems.append(f"duplicate inventory entry: {svc['name']} on {svc['hosts']}") + seen.add(key) + return problems + + +def check_drift(inv: Inventory) -> list[str]: + problems: list[str] = [] + rendered = render_pihole_hosts(inv) + hosts_file = REPO_ROOT / "dankclaw" / "pihole" / "addn-hosts" + if hosts_file.read_text() != rendered: + problems.append("dankclaw/pihole/addn-hosts is not the render of the inventory " + "(scripts/service-map.py render pihole-hosts > dankclaw/pihole/addn-hosts)") + + want = sorted(json.dumps(f, sort_keys=True) for f in scrape_facts(inv)) + have = sorted(json.dumps(f, sort_keys=True) for f in + prometheus_facts(REPO_ROOT / "shared" / "prometheus" / "prometheus.yml")) + for extra in [f for f in have if f not in want]: + problems.append(f"prometheus scrapes a target the inventory does not declare: {extra}") + for missing in [f for f in want if f not in have]: + problems.append(f"inventory declares a scrape prometheus.yml lacks: {missing}") + + probes_want = sorted(set(probe_facts(inv))) + probes_have = homepage_probes(REPO_ROOT / "shared" / "homepage" / "services.json") + for extra in [p for p in probes_have if p not in probes_want]: + problems.append(f"homepage probes a target the inventory does not declare: {extra}") + for missing in [p for p in probes_want if p not in probes_have]: + problems.append(f"inventory declares a probe homepage does not list: {missing}") + return problems + + +CHECKS = { + "config": lambda inv, graph: check_config(inv), + "units": check_units, + "ownership": check_ownership, + "drift": lambda inv, graph: check_drift(inv), +} + + +def check(inv: Inventory, names: list[str]) -> list[str]: + graph = graph_for(tuple(inv.hosts)) + problems: list[str] = [] + for name in names or list(CHECKS): + problems.extend(CHECKS[name](inv, graph)) + return problems + + +# -------------------------------------------------------------------------- +# CLI +# -------------------------------------------------------------------------- +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--inventory", type=Path, default=DEFAULT_INVENTORY) + sub = parser.add_subparsers(dest="command", required=True) + + sub.add_parser("hosts") + p = sub.add_parser("services") + p.add_argument("--host") + p = sub.add_parser("allows") + p.add_argument("--host", required=True) + p.add_argument("--service", required=True) + p = sub.add_parser("plan") + p.add_argument("--host", required=True) + p.add_argument("paths", nargs="+") + p = sub.add_parser("seams") + p.add_argument("--host") + p.add_argument("paths", nargs="+") + p = sub.add_parser("owner") + p.add_argument("--host", required=True) + p.add_argument("path") + p = sub.add_parser("units") + p.add_argument("--host", required=True) + p.add_argument("--service", required=True) + p = sub.add_parser("stack") + p.add_argument("--host", required=True) + p.add_argument("--service", required=True) + p = sub.add_parser("get") + p.add_argument("--host") + p.add_argument("--service", required=True) + p.add_argument("field") + p = sub.add_parser("render") + p.add_argument("artifact", choices=("pihole-hosts",)) + p = sub.add_parser("check") + p.add_argument("names", nargs="*") + + args = parser.parse_args(argv) + inv = Inventory.load(args.inventory) + + if args.command == "hosts": + print("\n".join(inv.hosts)) + elif args.command == "services": + services = inv.on(args.host) if args.host else inv.services + seen: list[str] = [] + for svc in services: + if svc["name"] not in seen: + seen.append(svc["name"]) + print("\n".join(seen)) + elif args.command == "allows": + return 0 if inv.named(args.host, args.service) else 1 + elif args.command == "plan": + print("\n".join(select(inv, args.host, args.paths))) + elif args.command == "seams": + if args.host is not None: + seams = seams_for(inv, args.host, args.paths) + else: + # No host: the union across every host (the dev machine's + # pre-commit mapper has no deployment target). + seams = [] + for host in inv.hosts: + for seam in seams_for(inv, host, args.paths): + if seam not in seams: + seams.append(seam) + print("\n".join(seams)) + elif args.command == "owner": + found = select(inv, args.host, [args.path]) + print(found[-1] if found else "") + elif args.command == "units": + matches = inv.named(args.host, args.service) + if not matches: + print(f"no service {args.service} on {args.host}", file=sys.stderr) + return 2 + for svc in matches: + for directory in inv.unit_dirs(svc, args.host): + for unit in svc.get("units", []): + print(f"{unit} {directory}") + elif args.command == "stack": + matches = inv.named(args.host, args.service) + if not matches: + print(f"no service {args.service} on {args.host}", file=sys.stderr) + return 2 + owner = matches[0].get("deployed_by") or args.service + print(owner) + for svc in inv.on(args.host): + if svc.get("deployed_by") == owner and svc["name"] != owner: + print(svc["name"]) + elif args.command == "get": + matches = inv.on(args.host) if args.host else inv.services + matches = [s for s in matches if s["name"] == args.service] + if not matches: + print(f"no service {args.service}" + (f" on {args.host}" if args.host else ""), + file=sys.stderr) + return 2 + values = [s.get(args.field) for s in matches if args.field in s] + print(json.dumps(values[0] if len(values) == 1 else values)) + elif args.command == "render": + sys.stdout.write(render_pihole_hosts(inv)) + elif args.command == "check": + problems = check(inv, args.names) + if problems: + for problem in problems: + print(f"FAIL: {problem}", file=sys.stderr) + return 1 + print("OK: the service inventory is sound (schema, units, ownership, drift)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/test-changed.sh b/scripts/test-changed.sh index 770b9ee..a91130e 100755 --- a/scripts/test-changed.sh +++ b/scripts/test-changed.sh @@ -62,234 +62,101 @@ declare -a TESTS=() add_test() { local test="$1" local existing + MAPPED=1 for existing in "${TESTS[@]:-}"; do [ "$existing" != "$test" ] || return 0 done TESTS+=("$test") } +# The path->test mapping has two layers: +# 1. cross-cutting seams that no single service owns (compose validity, +# env consistency, the deployer itself, docs, syntax); +# 2. the service inventory (shared/services.yml via service-map.py +# "seams"): every service the path selects on ANY host contributes its +# declared tests. Service seams are the services' own — the former +# shotgun fallbacks (every dankclaw quadlet unit ran the open-webui, +# pihole, and searxng seams) are retired; the inventory names each +# service's real seams, and adding a service is a data edit. for path in "${CHANGED[@]}"; do + MAPPED=0 case "$path" in tests/*.bats) add_test "$path" + continue ;; tests/helpers.bash|tests/run.sh) die "$path is shared test infrastructure; run ./tests/run.sh explicitly" ;; - .githooks/*) - add_test tests/changed_tests.bats - ;; - dankserver/compose.yaml|dankclaw/compose.yaml) - add_test tests/compose_validity.bats - add_test tests/env_consistency.bats - add_test tests/deployment_scoping.bats - ;; - dankserver/Caddyfile|dankclaw/Caddyfile) - add_test tests/caddyfile.bats - add_test tests/deployment_scoping.bats - ;; tests/e2e/*) add_test tests/e2e_scaffold.bats + continue ;; + .githooks/*) + add_test tests/changed_tests.bats + continue + ;; + # Archive material and host-level facts: a path under any + # .AGENTTRASH/ directory is retired-by-policy (the planner plans + # nothing from it), udev rules are applied by manual sudo install, + # reports are frozen artifacts, and ignore-lists change nothing at + # runtime. They select no focused test; without this case they would + # trip the mapping gate below. + .AGENTTRASH/*|*/.AGENTTRASH/*|dankclaw/udev/*|report/*|.gitignore|.env.example) + continue + ;; + esac + + # Cross-cutting seams. + case "$path" in dankserver/.env.example|dankclaw/.env.example) add_test tests/env_consistency.bats ;; - shared/homepage/*) - add_test tests/homepage_caddy.bats - add_test tests/homepage_config.bats - add_test tests/homepage_deploy.bats - add_test tests/homepage_pages.bats - add_test tests/homepage_checker.bats - add_test tests/deployment_scoping.bats - ;; - shared/searxng/*) - add_test tests/searxng_limiter.bats - add_test tests/searxng_quadlet.bats - add_test tests/deployment_scoping.bats - ;; - # The five observability containers left compose for Quadlet units on - # 2026-09-11. Their seam pins boot persistence and the loopback-only - # publish posture that keeps them off every host interface. This case - # precedes the dankclaw/*/quadlet/*.container fallback below, which - # would otherwise swallow it into unrelated seams. - dankclaw/observability/*) - add_test tests/observability_quadlet.bats + dankserver/Caddyfile|dankclaw/Caddyfile) add_test tests/caddyfile.bats add_test tests/deployment_scoping.bats - add_test tests/docs_completeness.bats - ;; - # Caddy left compose.yaml on 2026-08-26 for a quadlet; caddyfile.bats - # now asserts against the quadlet itself. Route every change under - # dankclaw/caddy/ to both seams (this block must precede the - # dankclaw/*.container glob, which would otherwise swallow the unit). - dankclaw/caddy/*) - add_test tests/caddy_quadlet.bats - add_test tests/caddyfile.bats - add_test tests/docs_completeness.bats - add_test tests/deployment_scoping.bats - ;; - dankclaw/*.container|dankclaw/*.network) - add_test tests/open_webui_quadlet.bats - add_test tests/pihole.bats - add_test tests/searxng_quadlet.bats - add_test tests/deployment_scoping.bats - ;; - # Anubis is a quadlet + Caddyfile pair: the chain (public listener -> - # anubis:8923 -> internal :8016 listener) spans both, so any change - # under the service dir must re-run the caddyfile seam too. This case - # precedes the dankclaw/*/quadlet/*.container fallback below, which - # would otherwise swallow it into unrelated seams. - dankclaw/anubis/*) - add_test tests/anubis.bats - add_test tests/caddyfile.bats - add_test tests/homepage_caddy.bats - add_test tests/deployment_scoping.bats - ;; - # frank-dragoon is a rootless quadlet again since 2026-09-16 (the - # halogen-flash-server restore). Its unit carries the fleet's GPU and - # podman posture, so it gets its own seam rather than the generic - # fallback below. - dankclaw/frank-dragoon/*) - add_test tests/frank_dragoon_unit.bats - add_test tests/deployment_scoping.bats - add_test tests/docs_completeness.bats - ;; - # Quadlets live under dankclaw//quadlet/ (reorganized out of the - # top-level dankclaw/*.container globs above). Map the whole subtree so - # changes to the pihole, open-webui, searxng, and gitea quadlets hit - # their seam tests. The pihole-specific block must come first: shell - # case matches the first pattern, and dankclaw/pihole/quadlet/*.container - # also satisfies the dankclaw/*/quadlet/*.container fallback. - dankclaw/pihole/*) - add_test tests/pihole.bats - add_test tests/docs_completeness.bats - add_test tests/deployment_scoping.bats - ;; - dankclaw/*/quadlet/*.container|dankclaw/*/quadlet/*.network) - add_test tests/open_webui_quadlet.bats - add_test tests/pihole.bats - add_test tests/searxng_quadlet.bats - add_test tests/docs_completeness.bats - add_test tests/deployment_scoping.bats ;; scripts/deploy.sh|scripts/deploy-remote.sh|scripts/deploy-plan.py|scripts/deploy-*-quadlet.sh) add_test tests/deployment_flow.bats add_test tests/deployment_scoping.bats add_test tests/scripts_syntax.bats ;; + scripts/service-map.py|shared/services.yml) + add_test tests/service_inventory.bats + add_test tests/deployment_scoping.bats + add_test tests/scripts_syntax.bats + ;; + scripts/lib/*.sh) + add_test tests/llm_gates.bats + add_test tests/scripts_syntax.bats + ;; + serverAppConfigs-docs/*|README.md|AGENTS.md) + add_test tests/docs_completeness.bats + add_test tests/deployment_scoping.bats + ;; scripts/mount-entropy.sh) add_test tests/mount_entropy.bats - ;; - # Archive material: a path under any .AGENTTRASH/ directory is - # retired-by-policy (deploy-plan.py plans nothing from it), so it - # selects no focused test. It still needs its own case: without one - # the wrapper refuses retirement commits, whose trash moves show up - # as additions, with "no focused-test mapping". - .AGENTTRASH/*|*/.AGENTTRASH/*) - ;; - # frank-archon is llama.cpp (the unsloth b11030-mix prebuilt) on the - # two R9700s as a ROOT unit — the 2026-09-26 swap that replaced the - # Lucebox DS4.1 quadlet on :7770. Its env, unit and serve script all - # live in scripts/, which is why this case has to precede the generic - # scripts/*.service and scripts/* fallbacks below. - scripts/frank-archon.*|scripts/serve-frank-archon.sh) - add_test tests/frank_archon_unit.bats - add_test tests/llm_units.bats - add_test tests/deployment_scoping.bats add_test tests/scripts_syntax.bats ;; - # frank-dragoon's chat template lives in scripts/ (moved there on - # 2026-09-26 when its old home was retired): a template edit changes - # what the live endpoint renders, so it selects the large unit seam. - scripts/qwen-sharp-v22.4.0.jinja) - add_test tests/frank_dragoon_unit.bats - ;; - # frank-zealot is llama.cpp on the 3090 as a ROOT unit: a native CUDA - # binary, so no generated quadlet is involved. Its env, unit and serve - # script all live in scripts/, which is why this case has to precede - # the generic scripts/*.service and scripts/* fallbacks below. - scripts/frank-zealot.*|scripts/serve-frank-zealot.sh) - add_test tests/frank_zealot_unit.bats - add_test tests/llm_units.bats - add_test tests/deployment_scoping.bats + old/scripts/*) add_test tests/scripts_syntax.bats ;; - scripts/*.service) - add_test tests/llm_units.bats - add_test tests/scripts_syntax.bats - add_test tests/deployment_scoping.bats - ;; scripts/*) add_test tests/scripts_syntax.bats ;; - serverAppConfigs-docs/*) - add_test tests/docs_completeness.bats - add_test tests/deployment_scoping.bats - ;; - dankclaw/hermes/*) - add_test tests/docs_completeness.bats - ;; - README.md|AGENTS.md) - # Project guidance changes can affect the documented deployment - # contract; validate the docs vault and scoped deployment seams. - add_test tests/docs_completeness.bats - add_test tests/deployment_scoping.bats - ;; - dankclaw/comfyui/*) - add_test tests/docs_completeness.bats - add_test tests/deployment_scoping.bats - ;; - # loki and alloy images are built from this repo's Containerfiles; the - # loki one carries the healthcheck command (see its comment), so both - # seams read it. - dankclaw/loki/*|dankclaw/alloy/*) - add_test tests/observability_quadlet.bats - add_test tests/caddyfile.bats - add_test tests/deployment_scoping.bats - ;; - dankclaw/udev/*) - # Host-level udev rules are applied by manual sudo install + udevadm - # trigger, not dispatched as a service; nothing here is bats-testable. - ;; - .AGENTTRASH/*) - # Trash moves are not testable; skip. - ;; - .gitignore|.env.example) - # Ignore/ignore-list changes change nothing at runtime; not testable. - ;; - old/scripts/*|old/scripts/**) - add_test tests/scripts_syntax.bats - ;; - # Cutover-era dankserver quadlet units (romm trio, jellyfin, calibre-web, - # copyparty, plex, tautulli): the seam checks boot-persistence + - # v1-generator contract; deploy-flow seams guard the deployer routing. - dankserver/romm/*|dankserver/jellyfin/*|dankserver/calibre-web/*|dankserver/copyparty/*|dankserver/plex/*|dankserver/tautulli/*) - add_test tests/dankserver_quadlet.bats - add_test tests/deployment_flow.bats - add_test tests/deployment_scoping.bats - ;; - # FrankBook-Pro units live inside the podman machine VM; the seam - # pins boot persistence, the gluetun netns invariant, and the - # plan-locked volume layout. Deployer changes route here too. - frankbook-pro/*) - add_test tests/frankbook_quadlet.bats - add_test tests/deployment_flow.bats - add_test tests/deployment_scoping.bats - ;; - # Seerr unit on dankclaw: boot persistence, loopback-only port - # posture, and the deployer routing that installs it. - dankclaw/seerr/*) - add_test tests/seerr_quadlet.bats - add_test tests/deployment_flow.bats - add_test tests/deployment_scoping.bats - ;; - report/*) - # Frozen audit reports are point-in-time artifacts; not testable. - ;; - *) - die "no focused-test mapping for '$path'; add one before committing" - ;; esac + + # The inventory's declared seams for every service this path selects. + while IFS= read -r seam; do + [ -n "$seam" ] && add_test "$seam" + done < <(python3 "$REPO_ROOT/scripts/service-map.py" seams -- "$path" 2>/dev/null || true) + + # Mapping gate: a path that selected no test at all — no service seam, + # no cross-cutting rule — is an unmapped area of the repo; refuse to + # commit it silently (the old catch-all `die`). + if [ "$MAPPED" -eq 0 ]; then + die "no focused-test mapping for '$path'; map it in the inventory (shared/services.yml) before committing" + fi done # Mapped-but-untestable paths (udev rules, trash moves) legitimately produce an diff --git a/serverAppConfigs-docs/Architecture/Service Inventory.md b/serverAppConfigs-docs/Architecture/Service Inventory.md new file mode 100644 index 0000000..db0e469 --- /dev/null +++ b/serverAppConfigs-docs/Architecture/Service Inventory.md @@ -0,0 +1,98 @@ +--- +type: reference +title: Service Inventory +description: shared/services.yml is the one inventory of deployable services; scripts/service-map.py is its only reader. The planner, both deploy allow-lists, the changed-test seam mapper, the Pi-hole hosts render, the Prometheus scrape facts, and the homepage probes all derive from it. +tags: [deployment, inventory, planner, quadlet, prometheus, pihole, homepage] +timestamp: 2026-10-13T00:00:00-07:00 +--- + +# Service Inventory + +Before 2026-10-13 the same facts lived in four hand-maintained lists — the +planner's path rules (`deploy-plan.py`), the `--service` allow-list in +`deploy.sh`, the near-copy in `deploy-remote.sh`, and the changed-test mapper +(`test-changed.sh`) — plus per-fact copies in `prometheus.yml`, +`addn-hosts`, and `services.json`. They drifted on every swap (the planner +still mapped the retired `qwen-sharp` template to frank-dragoon; the allow-list +still listed the *arr stack under Dankserver after it moved to FrankBook-Pro). +Now there is **one inventory and one reader**: + +- `shared/services.yml` — the data: every deployable service, its hosts, unit + dir(s), unit files, path claims, tests, scrape/DNS/probe facts, aliases. +- `scripts/service-map.py` — the only reader. CLI subcommands: + `hosts`, `services`, `allows`, `plan`, `seams`, `owner`, `units`, `stack`, + `get`, `render`, `check`. + +## 1. Consumers + +|Consumer|Call| +|---|---| +|`scripts/deploy-plan.py`|thin wrapper → `plan --host TARGET [paths...]` (keeps the `--repo/--base/--head` and `--paths` CLIs)| +|`scripts/deploy.sh` (`--service` gate)|`allows --host T --service S`| +|`scripts/deploy-remote.sh` (`service_is_allowed`)|same| +|`scripts/test-changed.sh`|`seams [paths...]` — a service's declared `tests` are its seams| +|`dankclaw/pihole/addn-hosts`|`render pihole-hosts` — the tracked file is a rendered artifact| +|Prometheus scrape jobs, LAN DNS, homepage probes|`check` compares the tracked configs against the inventory's `scrape`/`dns`/`probe` fields (drift gate)| + +Selection rules the engine encodes (see `tests/fixtures/service-plan-golden.tsv`, +the pinned characterization of every tracked path × every target): + +- **Host scoping** — a host's top-level directory can only select that host's + services. `frankbook-pro/**` plans nothing on Dankserver (the old planner + had a stale Dankserver *arr list). +- **Quadlet coupling graph** — `Network=x.container` rides x's netns; + `Network=y.network` joins a shared network. Changing the parent (or the + network file) selects every rider transitively, in their own directories + (`dankclaw/searxng/quadlet/gluetun.container` → gluetun + searxng + marreta; + `frankbook-pro/gluetun/quadlet/gluetun.container` → the whole arr stack). +- **Stacks** — a service's `stack` members deploy and restart as one handler + (romm trio, hermes trio). +- **Retirement** — removing a service from `services.yml` removes it from the + planner, both allow-lists, and the seam mapper in one data edit; a pending + deploy-state entry for it is auto-triaged (see [[Deploy State Triage]]). + +## 2. Integrity gates (`check`) + +`python3 scripts/service-map.py check` runs, and +`tests/service_inventory.bats` pins: + +- **schema** — required fields per entry, known keys only; +- **units** — every unit file named by an entry exists under its `unit_dir` + on the right host; each unit name is claimed by exactly one service per host; +- **ownership** — every tracked quadlet `.container`/`.network` file is claimed + by exactly one service (trash excluded); +- **paths** — every claimed `files`/`dirs`/`retires` glob matches ≥1 tracked file; +- **compose** — the container names in each host's `compose.yaml` have exactly + one quadlet owner (the [[Quadlet Lifecycle]] two-owners trap); +- **drift** — `addn-hosts` equals its render; the Prometheus job set, the DNS + names, and the homepage probe ports equal the inventory's fields. + +## 3. Adding, moving, retiring a service + +- **Add**: append an entry (name, hosts, kind, unit_dir, units, paths, tests, + …), add the quadlet file, run `check` — `plan`/`allows`/`seams` pick it up + with zero script edits. +- **Move between hosts**: edit `hosts` (and per-host `unit_dir`/`probe` maps). +- **Retire**: delete the entry, move files to `.AGENTTRASH/` (see + [[Testing Policy]] for the seam the mapper applies to trash paths). + +Field notes: `alias` carries deployable names that differ from the service +name (`calibre-web` deploys the unit `calibre-server`); `probe` for multi-host +services is a `{host: port}` map; `dependents` names services whose config +embeds a reference to this one (config-only coupling — a `.env` edit selects +both, a unit edit selects the graph closure). + +## 4. Known cutover deltas (2026-10-13) + +The 17 deltas against the old planner are all in these classes: anubis paths +now select `anubis` (the old planner had no rule at all); cross-host paths +select nothing; `scripts/qwen-sharp-v22.4.0.jinja` (retired template) plans +nothing while `scripts/qwen38-froggeric-v22.5.jinja` (the live frank-archon +template, previously unmapped) plans frank-archon; +`dankserver/romm/romm.container` (top-level quadlet dir the old unit-file rule +missed) now plans the romm trio. + +--- + +Related: [[Deployment Flow]], [[Testing Policy]], [[Quadlet Lifecycle]], +[[Container Network Namespaces]], [[Deploy State Triage]] diff --git a/serverAppConfigs-docs/index.md b/serverAppConfigs-docs/index.md index c56d0f9..bb10182 100644 --- a/serverAppConfigs-docs/index.md +++ b/serverAppConfigs-docs/index.md @@ -28,6 +28,7 @@ covers both workloads). - [[Network Architecture]] — Caddy reverse proxy, hostname routing, Gluetun VPN path - [[Storage Policy]] — bind mounts, data locations, ownership - [[Deployment Flow]] — service-scoped git deploy, target map, and health checks +- [[Service Inventory]] — shared/services.yml, the one inventory of deployable services, and service-map.py - [[Testing Policy]] — the test seams and how to run them - [[E2E Suite]] — the Playwright suite that drives the live guard chain - [[Container Network Namespaces]] — which local services are addressable from each Podman network namespace on Dankclaw diff --git a/shared/services.yml b/shared/services.yml new file mode 100644 index 0000000..1a7c78b --- /dev/null +++ b/shared/services.yml @@ -0,0 +1,598 @@ +# ============================================================================= +# services.yml — THE service inventory. +# +# One entry per deployable service, in canonical deploy order (dependencies +# before consumers, the LLM fleet last). Four hand-maintained lists used to +# agree with each other by discipline alone; every one of them now READS this +# file: +# +# scripts/deploy-plan.py which services a changed path selects +# scripts/deploy-remote.sh which services a target accepts (allow-list) +# scripts/test-changed.sh which Bats seams a changed path must re-run +# scripts/service-map.py renderers + drift gates (`check`) +# +# `service-map.py check` is the referee: every quadlet unit file and every +# root unit in the tree is claimed by exactly one entry, every repo file is +# owned by a service or listed in `unowned:` with a reason, every seam exists, +# the rendered Pi-hole hosts file equals the render, and Prometheus' scrape +# targets plus the homepage probes equal the `scrape:` / `probe:` facts. +# +# --- fields ------------------------------------------------------------------ +# name the deployer's service word == the systemd unit name for +# quadlet / root-unit / vm-quadlet kinds +# hosts targets that accept it (the deploy-remote.sh allow-list) +# kind quadlet rootless user unit from a .container file +# root-unit systemd system unit symlinked out of scripts/ +# vm-quadlet quadlet inside the FrankBook-Pro podman machine +# host a host-level script/timer, no container +# deployed_by this service deploys as part of another deployable's handler +# (the trio/pair shape). It has no dirs/files of its own; the +# owner names the deployable whose handler installs it. +# unit_dir repo dir holding .container — a scalar, or a per-host map +# when one service carries its unit on two hosts (node-exporter) +# units container names this deployable owns (netns coupling is derived +# from the `Network=` lines in those files, never declared here) +# dirs repo dirs whose every file selects this service +# files exact repo files that select this service (root-unit trios, +# vendored templates, host scripts installed by this handler) +# retires files whose DELETION selects this service (the unit the +# handler must remove before it can claim the ports) +# only Dankserver's media stack still has one) +# health the deployer's readiness URL; `wait` its 2 s poll budget +# scrape Prometheus jobs: {job, target, metrics_path?, labels?} +# dns names this service answers at (rendered into the Pi-hole +# addn-hosts; every vhost resolves at the Dankclaw Caddy edge) +# probe host:port the homepage checker dials (per-host map if on 2 hosts) +# tests the Bats seams a change to this service must re-run +# ============================================================================= + +hosts: + dankclaw: + address: 192.168.0.143 + # Caddy is consolidated on Dankclaw: it fronts every service on every + # host, so every DNS name in the fleet resolves here. + dns_edge: 192.168.0.143 + dankserver: + address: 192.168.0.235 + frankbook-pro: + address: 192.168.0.112 + +# Repo files no service owns. Each entry is a fact about the tree, not a +# fallback: the census gate fails on anything unowned that is NOT listed, so +# retirement work shrinks this list and a new stray file cannot slip in. +unowned: + shared/services.yml: the inventory this tool reads + scripts: repo tooling (build, fetch, bench, deploy, test); the deployers' + files are claimed by name in the service entries + autoresearch.sh: bench scratch at the repo root (retirement pending) + echo_prompt.txt: bench fixture at the repo root (retirement pending) + echo_request.json: bench fixture at the repo root (retirement pending) + prose_prompt.txt: bench fixture at the repo root (retirement pending) + prose_request.json: bench fixture at the repo root (retirement pending) + dankclaw/apply_qwen21_patch.py: retired patch script (its target runtime is gone) + dankclaw/comfyui-start.sh: superseded by the comfyui quadlet + Containerfile + dankclaw/udev: host udev rules, applied by manual sudo install + udevadm trigger + tests: the seams themselves + report: frozen point-in-time audit reports + old: retired scripts, reference only + .githooks: the commit hooks that run the seams + +services: + # --- Search stack: one netns family, gluetun owns it --------------------- + - name: gluetun + hosts: [dankclaw] + kind: quadlet + unit_dir: dankclaw/searxng/quadlet + units: [gluetun] + tests: [tests/searxng_quadlet.bats] + # The tunnel's WireGuard bind — the homepage card for the tunnel itself. + probe: 127.0.0.1:51820 + + - name: searxng-valkey + hosts: [dankclaw] + kind: quadlet + unit_dir: dankclaw/searxng/quadlet + units: [searxng-valkey] + # The app-level law, not a network one: deploy_searxng_valkey restarts + # SearXNG after its cache, so the cache's files select both. + dependents: [searxng] + tests: [tests/searxng_quadlet.bats] + probe: 127.0.0.1:6379 + + - name: searxng + hosts: [dankclaw] + kind: quadlet + unit_dir: dankclaw/searxng/quadlet + units: [searxng] + dirs: [shared/searxng] + tests: [tests/searxng_limiter.bats, tests/searxng_quadlet.bats] + scrape: + - job: services + target: 192.168.0.143:8081 + metrics_path: / + dns: [search.franksdank.cloud] + probe: 127.0.0.1:8081 + + - name: marreta + hosts: [dankclaw] + kind: quadlet + unit_dir: dankclaw/marreta/quadlet + units: [marreta, marreta-browser] + tests: [tests/marreta_quadlet.bats] + dns: [marreta.franksdank.cloud] + probe: 127.0.0.1:8082 + + # --- DNS ------------------------------------------------------------------ + - name: pihole + hosts: [dankclaw] + kind: quadlet + unit_dir: dankclaw/pihole/quadlet + units: [pihole] + dirs: [dankclaw/pihole] + tests: [tests/pihole.bats, tests/docs_completeness.bats] + dns: [pihole.franksdank.cloud] + probe: 192.168.0.143:8083 + + # --- Chat front end ------------------------------------------------------- + - name: open-webui + hosts: [dankclaw] + kind: quadlet + unit_dir: dankclaw/open-webui/quadlet + units: [open-webui] + tests: [tests/open_webui_quadlet.bats, tests/caddyfile.bats] + scrape: + - job: services + target: 192.168.0.143:3005 + metrics_path: / + dns: [ai.franksdank.cloud] + probe: 127.0.0.1:3005 + + # --- Git hosting ---------------------------------------------------------- + - name: gitea-postgres + hosts: [dankclaw] + kind: quadlet + unit_dir: dankclaw/gitea/quadlet + units: [gitea-postgres] + deployed_by: gitea-server + tests: [tests/caddyfile.bats, tests/docs_completeness.bats] + probe: 127.0.0.1:5432 + + - name: gitea-server + hosts: [dankclaw] + kind: quadlet + unit_dir: dankclaw/gitea/quadlet + units: [gitea-server] + tests: [tests/caddyfile.bats, tests/docs_completeness.bats] + scrape: + - job: gitea + target: 192.168.0.143:3000 + metrics_path: /metrics + dns: [gitea.franksdank.cloud] + probe: 127.0.0.1:3000 + + # --- Image generation ----------------------------------------------------- + - name: comfyui + hosts: [dankclaw] + kind: quadlet + unit_dir: dankclaw/comfyui/quadlet + units: [comfyui] + dirs: [dankclaw/comfyui] + files: [dankclaw/Containerfile.comfyui] + health: http://127.0.0.1:8188/ + wait: 300 + tests: [tests/comfyui_rocm.bats, tests/docs_completeness.bats] + scrape: + - job: comfyui + target: comfyui:8188 + metrics_path: / + dns: [comfyui.franksdank.cloud] + probe: 127.0.0.1:8188 + + # --- Requests ------------------------------------------------------------- + - name: seerr + hosts: [dankclaw] + kind: quadlet + unit_dir: dankclaw/seerr/quadlet + units: [seerr] + tests: [tests/seerr_quadlet.bats, tests/deployment_flow.bats] + dns: [seerr.franksdank.cloud] + probe: 127.0.0.1:5055 + + # --- Agent stack: one handler, four containers ---------------------------- + - name: hermes + hosts: [dankclaw] + kind: quadlet + unit_dir: dankclaw/hermes/quadlet + units: [hermes, hermes-dashboard, hermes-webui, chrome-devtools-protocol] + dirs: [dankclaw/hermes] + tests: [tests/hermes_webui.bats, tests/docs_completeness.bats] + dns: [hermes.franksdank.cloud] + probe: 127.0.0.1:8787 + + - name: hermes-dashboard + hosts: [dankclaw] + kind: quadlet + deployed_by: hermes + tests: [tests/hermes_webui.bats, tests/docs_completeness.bats] + dns: [hermesadmin.franksdank.cloud] + probe: 192.168.0.143:9119 + + - name: hermes-webui + hosts: [dankclaw] + kind: quadlet + deployed_by: hermes + tests: [tests/hermes_webui.bats, tests/docs_completeness.bats] + + # --- Observability -------------------------------------------------------- + - name: prometheus + hosts: [dankclaw] + kind: quadlet + unit_dir: dankclaw/observability/quadlet + units: [prometheus] + dirs: [shared/prometheus] + tests: [tests/observability_quadlet.bats, tests/caddyfile.bats, + tests/docs_completeness.bats] + scrape: + - job: prometheus + target: localhost:9090 + dns: [prometheus.franksdank.cloud] + probe: 127.0.0.1:9090 + + - name: grafana + hosts: [dankclaw] + kind: quadlet + unit_dir: dankclaw/observability/quadlet + units: [grafana] + dirs: [shared/grafana] + tests: [tests/observability_quadlet.bats, tests/caddyfile.bats, + tests/docs_completeness.bats] + scrape: + - job: grafana + target: grafana:3000 + metrics_path: /metrics + dns: [grafana.franksdank.cloud] + probe: 127.0.0.1:3002 + + - name: loki + hosts: [dankclaw] + kind: quadlet + unit_dir: dankclaw/observability/quadlet + units: [loki] + dirs: [dankclaw/loki, shared/loki] + tests: [tests/observability_quadlet.bats, tests/caddyfile.bats, + tests/docs_completeness.bats] + scrape: + - job: loki + target: loki:3100 + metrics_path: /metrics + dns: [loki.franksdank.cloud] + probe: 127.0.0.1:3100 + + - name: alloy + hosts: [dankclaw] + kind: quadlet + unit_dir: dankclaw/observability/quadlet + units: [alloy] + dirs: [dankclaw/alloy, shared/alloy] + tests: [tests/observability_quadlet.bats, tests/caddyfile.bats, + tests/docs_completeness.bats] + probe: 127.0.0.1:12345 + + - name: node-exporter + # One service on two hosts: the same exporter, its own unit per host. + hosts: [dankclaw, dankserver] + kind: quadlet + unit_dir: + dankclaw: dankclaw/observability/quadlet + dankserver: dankserver/observability/quadlet + units: [node-exporter] + tests: [tests/observability_quadlet.bats, tests/caddyfile.bats, + tests/docs_completeness.bats] + scrape: + - job: node-dankclaw + target: host.containers.internal:9100 + - job: node-dankserver + target: 192.168.0.235:9100 + probe: 127.0.0.1:9100 + + - name: alertmanager + hosts: [dankclaw] + kind: quadlet + unit_dir: dankclaw/observability/quadlet + units: [alertmanager] + # The Alertmanager config ships from Prometheus' directory, and the Signal + # relay is a HOST unit this handler installs: a change to either reaches + # the machine only through an alertmanager deploy. + files: + - shared/prometheus/alertmanager.yml + - scripts/signal-relay.py + - scripts/signal-relay.service + tests: [tests/observability_quadlet.bats, tests/caddyfile.bats, + tests/docs_completeness.bats] + probe: 127.0.0.1:9093 + + # --- Identity ------------------------------------------------------------- + - name: authentik-postgres + hosts: [dankclaw] + kind: quadlet + unit_dir: dankclaw/authentik/quadlet + units: [authentik-postgres] + deployed_by: authentik-server + tests: [tests/authentik.bats] + + - name: authentik-server + hosts: [dankclaw] + kind: quadlet + unit_dir: dankclaw/authentik/quadlet + units: [authentik-server] + tests: [tests/authentik.bats] + dns: [auth.franksdank.cloud] + + - name: authentik-worker + hosts: [dankclaw] + kind: quadlet + unit_dir: dankclaw/authentik/quadlet + units: [authentik-worker] + deployed_by: authentik-server + tests: [tests/authentik.bats] + + - name: authentik-outpost + hosts: [dankclaw] + kind: quadlet + unit_dir: dankclaw/authentik/quadlet + units: [authentik-outpost] + tests: [tests/authentik.bats] + + - name: authentik-outpost-ldap + hosts: [dankclaw] + kind: quadlet + unit_dir: dankclaw/authentik/quadlet + units: [authentik-outpost-ldap] + tests: [tests/authentik.bats] + + # --- The auth chain's edge, then the edge itself -------------------------- + - name: anubis + hosts: [dankclaw] + kind: quadlet + unit_dir: dankclaw/anubis/quadlet + units: [anubis] + dirs: [dankclaw/anubis] + tests: [tests/anubis.bats, tests/caddyfile.bats, tests/homepage_caddy.bats] + + - name: caddy + hosts: [dankclaw] + kind: quadlet + unit_dir: dankclaw/caddy/quadlet + units: [caddy] + dirs: [dankclaw/caddy] + files: [dankclaw/Caddyfile] + # Retiring the old split proxy selects Caddy: it must remove the installed + # unit and container before it can claim 80/443. + retires: [dankclaw/gitea/quadlet/gitea-caddy.container] + tests: [tests/caddy_quadlet.bats, tests/caddyfile.bats, + tests/homepage_caddy.bats, tests/docs_completeness.bats] + dns: + - franksdank.cloud + - www.franksdank.cloud + - dankclaw.franksdank.cloud + - dankserver.franksdank.cloud + probe: 127.0.0.1:80 + + - name: homepage + hosts: [dankclaw] + kind: host + dirs: [shared/homepage] + files: + - scripts/homepage-status.py + - scripts/homepage-status.service + - scripts/homepage-status.timer + tests: [tests/homepage_caddy.bats, tests/homepage_config.bats, + tests/homepage_deploy.bats, tests/homepage_pages.bats, + tests/homepage_checker.bats] + + # --- Dankserver media stack ---------------------------------------------- + # The romm trio is one handler and one directory: every file in it selects + # all three, exactly as the deployer restarts all three. + - name: romm-mariadb + hosts: [dankserver] + kind: quadlet + unit_dir: dankserver/romm/quadlet + units: [romm-mariadb] + deployed_by: romm + dirs: [dankserver/romm] + tests: [tests/dankserver_quadlet.bats, tests/deployment_flow.bats] + probe: 192.168.0.235:3306 + + - name: romm-redis + hosts: [dankserver] + kind: quadlet + unit_dir: dankserver/romm/quadlet + units: [romm-redis] + deployed_by: romm + dirs: [dankserver/romm] + tests: [tests/dankserver_quadlet.bats, tests/deployment_flow.bats] + + - name: romm + hosts: [dankserver] + kind: quadlet + unit_dir: dankserver/romm/quadlet + units: [romm] + dirs: [dankserver/romm] + tests: [tests/dankserver_quadlet.bats, tests/deployment_flow.bats] + dns: [romm.franksdank.cloud] + probe: 192.168.0.235:8085 + + - name: copyparty + hosts: [dankserver] + kind: quadlet + unit_dir: dankserver/copyparty/quadlet + units: [copyparty] + tests: [tests/dankserver_quadlet.bats, tests/deployment_flow.bats] + dns: [files.franksdank.cloud] + probe: 192.168.0.235:3923 + + - name: jellyfin + hosts: [dankserver] + kind: quadlet + unit_dir: dankserver/jellyfin/quadlet + units: [jellyfin] + tests: [tests/dankserver_quadlet.bats, tests/deployment_flow.bats] + dns: [jelly.franksdank.cloud] + probe: 192.168.0.235:8096 + + - name: calibre-web + hosts: [dankserver] + kind: quadlet + unit_dir: dankserver/calibre-web/quadlet + units: [calibre-web] + tests: [tests/dankserver_quadlet.bats, tests/deployment_flow.bats] + dns: [books.franksdank.cloud] + probe: 192.168.0.235:8083 + + - name: calibre-server + hosts: [dankserver] + kind: quadlet + unit_dir: dankserver/calibre-server/quadlet + units: [calibre-server] + tests: [tests/dankserver_quadlet.bats, tests/deployment_flow.bats] + + - name: plex + hosts: [dankserver] + kind: quadlet + unit_dir: dankserver/plex/quadlet + units: [plex] + tests: [tests/dankserver_quadlet.bats, tests/deployment_flow.bats] + dns: [plex.franksdank.cloud, plex.franksdank.casa] + probe: 192.168.0.235:32400 + + - name: tautulli + hosts: [dankserver] + kind: quadlet + unit_dir: dankserver/tautulli/quadlet + units: [tautulli] + tests: [tests/dankserver_quadlet.bats, tests/deployment_flow.bats] + probe: 192.168.0.235:8181 + + # --- FrankBook-Pro: the *arr stack inside the podman machine -------------- + # Every container rides gluetun's netns, and that coupling is DERIVED: each + # arr's unit file says `Network=gluetun.container`, so a gluetun unit change + # selects the whole stack with no declaration here. + - name: gluetun + hosts: [frankbook-pro] + kind: vm-quadlet + unit_dir: frankbook-pro/gluetun/quadlet + units: [gluetun] + tests: [tests/frankbook_quadlet.bats, tests/deployment_flow.bats] + + - name: prowlarr + hosts: [frankbook-pro] + kind: vm-quadlet + unit_dir: frankbook-pro/prowlarr/quadlet + units: [prowlarr] + tests: [tests/frankbook_quadlet.bats, tests/deployment_flow.bats] + dns: [prowlarr.franksdank.cloud] + probe: 192.168.0.112:9696 + + - name: qbittorrent + hosts: [frankbook-pro] + kind: vm-quadlet + unit_dir: frankbook-pro/qbittorrent/quadlet + units: [qbittorrent] + tests: [tests/frankbook_quadlet.bats, tests/deployment_flow.bats] + dns: [qbittorrent.franksdank.cloud] + probe: 192.168.0.112:8080 + + - name: radarr + hosts: [frankbook-pro] + kind: vm-quadlet + unit_dir: frankbook-pro/radarr/quadlet + units: [radarr] + tests: [tests/frankbook_quadlet.bats, tests/deployment_flow.bats] + dns: [radarr.franksdank.cloud] + probe: 192.168.0.112:7878 + + - name: sonarr + hosts: [frankbook-pro] + kind: vm-quadlet + unit_dir: frankbook-pro/sonarr/quadlet + units: [sonarr] + tests: [tests/frankbook_quadlet.bats, tests/deployment_flow.bats] + probe: 192.168.0.112:8989 + + - name: lidarr + hosts: [frankbook-pro] + kind: vm-quadlet + unit_dir: frankbook-pro/lidarr/quadlet + units: [lidarr] + tests: [tests/frankbook_quadlet.bats, tests/deployment_flow.bats] + dns: [lidarr.franksdank.cloud] + probe: 192.168.0.112:8686 + + - name: readarr + hosts: [frankbook-pro] + kind: vm-quadlet + unit_dir: frankbook-pro/readarr/quadlet + units: [readarr] + tests: [tests/frankbook_quadlet.bats, tests/deployment_flow.bats] + dns: [readarr.franksdank.cloud] + probe: 192.168.0.112:8787 + + # --- The LLM fleet (last: never implied by a generic change) -------------- + - name: frank-dragoon + hosts: [dankclaw] + kind: root-unit + files: + - scripts/frank-dragoon.env + - scripts/frank-dragoon.service + - scripts/serve-frank-dragoon.sh + health: http://127.0.0.1:7770/health + wait: 1200 + tests: [tests/frank_dragoon_unit.bats, tests/llm_units.bats, + tests/llm_deploy_gates.bats, tests/llm_secrets.bats, + tests/scripts_syntax.bats, tests/docs_completeness.bats] + scrape: + - job: frank-dragoon + target: host.containers.internal:7770 + metrics_path: /metrics + labels: {service: frank-dragoon} + probe: 127.0.0.1:7770 + + - name: frank-zealot + hosts: [dankclaw] + kind: root-unit + files: + - scripts/frank-zealot.env + - scripts/frank-zealot.service + - scripts/serve-frank-zealot.sh + health: http://127.0.0.1:7771/health + wait: 900 + tests: [tests/frank_zealot_unit.bats, tests/llm_units.bats, + tests/llm_deploy_gates.bats, tests/llm_secrets.bats, + tests/scripts_syntax.bats] + scrape: + - job: frank-zealot + target: host.containers.internal:7771 + metrics_path: /metrics + labels: {service: frank-zealot} + probe: 127.0.0.1:7771 + + - name: frank-archon + hosts: [dankserver] + kind: root-unit + files: + - scripts/frank-archon.env + - scripts/frank-archon.service + - scripts/serve-frank-archon.sh + # The vendored chat template the engine renders with. + - scripts/qwen38-froggeric-v22.5.jinja + health: http://127.0.0.1:7770/health + wait: 900 + tests: [tests/frank_archon_unit.bats, tests/llm_units.bats, + tests/llm_deploy_gates.bats, tests/llm_secrets.bats, + tests/scripts_syntax.bats] + scrape: + - job: frank-archon + target: 192.168.0.235:7770 + metrics_path: /metrics + labels: {service: frank-archon} + probe: 192.168.0.235:7770 diff --git a/tests/compose_validity.bats b/tests/.AGENTTRASH/20261013-compose-retire/compose_validity.bats similarity index 100% rename from tests/compose_validity.bats rename to tests/.AGENTTRASH/20261013-compose-retire/compose_validity.bats diff --git a/tests/changed_tests.bats b/tests/changed_tests.bats index eec3888..a36d6b8 100644 --- a/tests/changed_tests.bats +++ b/tests/changed_tests.bats @@ -13,11 +13,15 @@ load helpers [ "$status" -eq 0 ] } -@test "changed-test wrapper maps a compose change to compose and environment tests" { - run bash "$REPO_ROOT/scripts/test-changed.sh" --list --paths dankserver/compose.yaml +@test "changed-test wrapper maps a shared config change to its service seams" { + # The compose era is retired (rank 7): the former compose fixture here + # mapped dankserver/compose.yaml, a file that no longer exists. The + # equivalent contract now: a service config change maps to the + # service's seams and nothing unrelated. + run bash "$REPO_ROOT/scripts/test-changed.sh" --list --paths shared/searxng/settings.yml [ "$status" -eq 0 ] - [[ "$output" == *"tests/compose_validity.bats"* ]] - [[ "$output" == *"tests/env_consistency.bats"* ]] + [[ "$output" == *"tests/searxng_limiter.bats"* ]] + [[ "$output" == *"tests/searxng_quadlet.bats"* ]] [[ "$output" != *"tests/homepage_pages.bats"* ]] } @@ -47,3 +51,27 @@ load helpers [[ "$output" != *"tests/frank_medium_unit.bats"* ]] [[ "$output" != *"tests/frank_archon_unit.bats"* ]] } + +@test "changed-test wrapper maps the frankbook unit to the frankbook and deploy-flow seams" { + run bash "$REPO_ROOT/scripts/test-changed.sh" --list --paths frankbook-pro/sonarr/quadlet/sonarr.container + [ "$status" -eq 0 ] + [[ "$output" == *"tests/frankbook_quadlet.bats"* ]] + [[ "$output" == *"tests/deployment_flow.bats"* ]] + [[ "$output" == *"tests/deployment_scoping.bats"* ]] + [[ "$output" != *"tests/frank_archon_unit.bats"* ]] +} + +@test "changed-test wrapper maps the inventory itself to its seam" { + run bash "$REPO_ROOT/scripts/test-changed.sh" --list --paths shared/services.yml + [ "$status" -eq 0 ] + [[ "$output" == *"tests/service_inventory.bats"* ]] + run bash "$REPO_ROOT/scripts/test-changed.sh" --list --paths scripts/service-map.py + [ "$status" -eq 0 ] + [[ "$output" == *"tests/service_inventory.bats"* ]] +} + +@test "changed-test wrapper refuses an unmapped path" { + run bash "$REPO_ROOT/scripts/test-changed.sh" --list --paths newdir/file.txt + [ "$status" -eq 2 ] + [[ "$output" == *"no focused-test mapping"* ]] +} diff --git a/tests/fixtures/service-plan-golden.tsv b/tests/fixtures/service-plan-golden.tsv new file mode 100644 index 0000000..d30e944 --- /dev/null +++ b/tests/fixtures/service-plan-golden.tsv @@ -0,0 +1,1773 @@ +dankclaw .AGENTTRASH/frank-default-small.env.llama.cpp +dankclaw .AGENTTRASH/frank-default-small.service.llama.cpp +dankclaw .AGENTTRASH/frank-dragoon.env.llama.cpp +dankclaw .AGENTTRASH/frank-dragoon.service.llama.cpp +dankclaw .AGENTTRASH/frank-npu.env.flm-npu +dankclaw .AGENTTRASH/frank-npu.service.flm-npu +dankclaw .AGENTTRASH/serve-frank-dragoon.sh.llama.cpp +dankclaw .AGENTTRASH/serve-frank-npu.sh.flm-npu +dankclaw .AGENTTRASH/serve-frank-small.sh.llama.cpp +dankclaw .githooks/pre-commit +dankclaw .gitignore +dankclaw AGENTS.md +dankclaw README.md +dankclaw autoresearch.sh +dankclaw dankclaw/.env.example +dankclaw dankclaw/Caddyfile caddy +dankclaw dankclaw/Containerfile.comfyui comfyui +dankclaw dankclaw/alloy/Containerfile alloy +dankclaw dankclaw/anubis/policy.json anubis +dankclaw dankclaw/anubis/quadlet/anubis.container anubis +dankclaw dankclaw/apply_qwen21_patch.py +dankclaw dankclaw/authentik/quadlet/authentik-outpost-ldap.container authentik-outpost-ldap +dankclaw dankclaw/authentik/quadlet/authentik-outpost.container authentik-outpost +dankclaw dankclaw/authentik/quadlet/authentik-postgres.container authentik-postgres +dankclaw dankclaw/authentik/quadlet/authentik-server.container authentik-server +dankclaw dankclaw/authentik/quadlet/authentik-worker.container authentik-worker +dankclaw dankclaw/caddy/Containerfile caddy +dankclaw dankclaw/caddy/env.example caddy +dankclaw dankclaw/caddy/quadlet/caddy.container caddy +dankclaw dankclaw/comfyui-start.sh +dankclaw dankclaw/comfyui/comfyui-healthcheck.py comfyui +dankclaw dankclaw/comfyui/quadlet/comfyui.container comfyui +dankclaw dankclaw/comfyui/workflows/qwen-image-2.1-t2i-art.json comfyui +dankclaw dankclaw/comfyui/workflows/qwen-image-2.1-t2i-legible-text.json comfyui +dankclaw dankclaw/gitea/.env.example +dankclaw dankclaw/gitea/quadlet/gitea-postgres.container gitea-postgres +dankclaw dankclaw/gitea/quadlet/gitea-server.container gitea-server +dankclaw dankclaw/gitea/quadlet/gitea.network gitea-postgres,gitea-server +dankclaw dankclaw/hermes/config.yaml.example hermes +dankclaw dankclaw/hermes/quadlet/chrome-devtools-protocol.container hermes +dankclaw dankclaw/hermes/quadlet/hermes-dashboard.container hermes +dankclaw dankclaw/hermes/quadlet/hermes-webui.container hermes +dankclaw dankclaw/hermes/quadlet/hermes.container hermes +dankclaw dankclaw/loki/Containerfile loki +dankclaw dankclaw/marreta/quadlet/marreta-browser.container marreta +dankclaw dankclaw/marreta/quadlet/marreta.container marreta +dankclaw dankclaw/observability/quadlet/alertmanager.container alertmanager +dankclaw dankclaw/observability/quadlet/alloy.container alloy +dankclaw dankclaw/observability/quadlet/grafana.container grafana +dankclaw dankclaw/observability/quadlet/loki.container loki +dankclaw dankclaw/observability/quadlet/node-exporter.container node-exporter +dankclaw dankclaw/observability/quadlet/prometheus.container prometheus +dankclaw dankclaw/open-webui/quadlet/open-webui.container open-webui +dankclaw dankclaw/open-webui/quadlet/open-webui.network open-webui +dankclaw dankclaw/pihole/addn-hosts pihole +dankclaw dankclaw/pihole/env.example pihole +dankclaw dankclaw/pihole/quadlet/pihole.container pihole +dankclaw dankclaw/pihole/sysctl/99-dnsmasq.conf pihole +dankclaw dankclaw/searxng/quadlet/gluetun.container gluetun,searxng,marreta +dankclaw dankclaw/searxng/quadlet/searxng-valkey.container searxng-valkey,searxng +dankclaw dankclaw/searxng/quadlet/searxng.container searxng +dankclaw dankclaw/searxng/quadlet/searxng.network gluetun,searxng-valkey,searxng,marreta +dankclaw dankclaw/seerr/quadlet/seerr.container seerr +dankclaw dankclaw/udev/99-comfyui-gpu-access.rules +dankclaw dankserver/.AGENTTRASH/20260916-vllm-radiance/affinity-proxy.py +dankclaw dankserver/.AGENTTRASH/20260916-vllm-radiance/frank-medium-a.container +dankclaw dankserver/.AGENTTRASH/20260916-vllm-radiance/frank-medium-b.container +dankclaw dankserver/.AGENTTRASH/20260916-vllm-radiance/frank-medium-proxy.container +dankclaw dankserver/.AGENTTRASH/20260916-vllm-radiance/routing.py +dankclaw dankserver/.AGENTTRASH/20261013-compose-retire/.env.example +dankclaw dankserver/.AGENTTRASH/20261013-compose-retire/compose.yaml +dankclaw dankserver/calibre-server/quadlet/calibre-server.container +dankclaw dankserver/calibre-web/quadlet/calibre-web.container +dankclaw dankserver/copyparty/quadlet/copyparty.container +dankclaw dankserver/jellyfin/quadlet/jellyfin.container +dankclaw dankserver/observability/quadlet/node-exporter.container +dankclaw dankserver/plex/quadlet/plex.container +dankclaw dankserver/romm/quadlet/romm-mariadb.container +dankclaw dankserver/romm/quadlet/romm-redis.container +dankclaw dankserver/romm/quadlet/romm.container +dankclaw dankserver/tautulli/quadlet/tautulli.container +dankclaw echo_prompt.txt +dankclaw echo_request.json +dankclaw frankbook-pro/gluetun/quadlet/gluetun.container +dankclaw frankbook-pro/lidarr/quadlet/lidarr.container +dankclaw frankbook-pro/prowlarr/quadlet/prowlarr.container +dankclaw frankbook-pro/qbittorrent/quadlet/qbittorrent.container +dankclaw frankbook-pro/radarr/quadlet/radarr.container +dankclaw frankbook-pro/readarr/quadlet/readarr.container +dankclaw frankbook-pro/sonarr/quadlet/sonarr.container +dankclaw old/download-frank-archon-fp8.sh +dankclaw old/download-frank-dragoon-dflash.sh +dankclaw old/download-frank-dragoon-mimo.sh +dankclaw old/download-frank-dragoon-qwen35.sh +dankclaw old/download-frank-zealot-sglang-int8.sh +dankclaw old/qwen3.8-flash-next-fp8.manifest +dankclaw old/scripts/analyze-spec-matrix.py +dankclaw old/scripts/bench-frank-dragoon.sh +dankclaw old/scripts/bench-high-ctx.sh +dankclaw old/scripts/bench-pipeline-64k.sh +dankclaw old/scripts/deploy.sh +dankclaw old/scripts/frank-default-large.env +dankclaw old/scripts/frank-default-large.ling.env +dankclaw old/scripts/frank-default-large.service +dankclaw old/scripts/frank-default-small.env +dankclaw old/scripts/frank-default-small.service +dankclaw old/scripts/frank-dragoon-ling.service +dankclaw old/scripts/frank-pp-worker.env +dankclaw old/scripts/frank-pp-worker.service +dankclaw old/scripts/llama-server.service +dankclaw old/scripts/pp-bench.py +dankclaw old/scripts/pp-test.sh +dankclaw old/scripts/prune-rpc-cache.sh +dankclaw old/scripts/serve-flm-small.sh +dankclaw old/scripts/serve-ling-flash.sh +dankclaw old/scripts/serve-ling3-vllm.sh +dankclaw old/scripts/serve-llama-frank-dragoon.sh +dankclaw old/scripts/serve-llama-frank-small.sh +dankclaw old/scripts/serve-pp-head.sh +dankclaw old/scripts/serve-pp-worker.sh +dankclaw old/scripts/serve-vllm-qwen36-dflash.sh +dankclaw old/scripts/spec-matrix-bench.sh +dankclaw old/scripts/test_dspark_stream.py +dankclaw old/scripts/validate-vllm.sh +dankclaw prose_prompt.txt +dankclaw prose_request.json +dankclaw report/doc-audit-2026-08-23.html +dankclaw report/homepage-grilling-2026-08-22.html +dankclaw scripts/.AGENTTRASH/20260920-llama-layersplit/bench-spec-ab.py.llamacpp-layersplit +dankclaw scripts/.AGENTTRASH/20260920-llama-layersplit/frank-medium.env.llamacpp-layersplit +dankclaw scripts/.AGENTTRASH/20260920-llama-layersplit/frank-medium.service.llamacpp-layersplit +dankclaw scripts/.AGENTTRASH/20260920-llama-layersplit/serve-frank-medium.sh.llamacpp-layersplit +dankclaw scripts/.AGENTTRASH/bench-decode-threads-batch.py.strix-rocmfp4 +dankclaw scripts/.AGENTTRASH/bench-ngram-mod.py.ik-llama.cpp +dankclaw scripts/.AGENTTRASH/bench-ngram-mod.sh.ik-llama.cpp +dankclaw scripts/.AGENTTRASH/bench-spec-decode.py.ik-llama.cpp +dankclaw scripts/.AGENTTRASH/bench-vllm-corpus.py +dankclaw scripts/.AGENTTRASH/bench-vllm-serve.sh +dankclaw scripts/.AGENTTRASH/chat-template.jinja.ik-llama-qwen-sharp +dankclaw scripts/.AGENTTRASH/download-q8agent.sh.rocmfpx +dankclaw scripts/.AGENTTRASH/frank-default-npu.env +dankclaw scripts/.AGENTTRASH/frank-default-npu.service +dankclaw scripts/.AGENTTRASH/frank-dragoon.env.ik-llama.cpp +dankclaw scripts/.AGENTTRASH/frank-dragoon.service.ik-llama.cpp +dankclaw scripts/.AGENTTRASH/frank-medium.env.rocmfpx +dankclaw scripts/.AGENTTRASH/frank-medium.service.rocmfpx +dankclaw scripts/.AGENTTRASH/serve-frank-default-npu.sh +dankclaw scripts/.AGENTTRASH/serve-frank-dragoon.sh.ik-llama.cpp +dankclaw scripts/.AGENTTRASH/serve-frank-medium.sh.rocmfpx +dankclaw scripts/apply-protected-cast.sh +dankclaw scripts/autoresearch-bench.py +dankclaw scripts/autoresearch-frank-archon.sh +dankclaw scripts/autoresearch/frank-archon-arms.sh +dankclaw scripts/autoresearch/frank-archon-measure.py +dankclaw scripts/autoresearch/frank-archon-window.sh +dankclaw scripts/bench-batch-ubatch.sh +dankclaw scripts/bench-conversation.py +dankclaw scripts/bench-decode.sh +dankclaw scripts/bench-llm-soak.py +dankclaw scripts/bench-probe.py +dankclaw scripts/bench-threads-batch-http.py +dankclaw scripts/bench-threads-batch.sh +dankclaw scripts/bench-turn-latency.py +dankclaw scripts/bench-ubatch.sh +dankclaw scripts/bench_tb_fixed.py +dankclaw scripts/build-frank-archon-rad.sh +dankclaw scripts/build-gufo.sh +dankclaw scripts/build-kairic-edge-gfx1151.sh +dankclaw scripts/build-llama-rdna4.sh +dankclaw scripts/build-rocmfpx.sh +dankclaw scripts/calib-push.py +dankclaw scripts/check-vault-links.py +dankclaw scripts/convert-swift15-w4dn8.sh +dankclaw scripts/dankserver-route-primary.sh +dankclaw scripts/dankstatus.sh +dankclaw scripts/deep-probe.py +dankclaw scripts/deploy-open-webui-quadlet.sh +dankclaw scripts/deploy-pihole-quadlet.sh +dankclaw scripts/deploy-plan.py +dankclaw scripts/deploy-remote.sh +dankclaw scripts/deploy-searxng-quadlet.sh +dankclaw scripts/deploy.sh +dankclaw scripts/derive-protected-experts.py +dankclaw scripts/download-frank-archon-swift15.sh +dankclaw scripts/download-frank-archon.sh +dankclaw scripts/download-frank-dragoon-flashnext.sh +dankclaw scripts/download-frank-dragoon-halogen.sh +dankclaw scripts/download-frank-dragoon-kingjones-uncensored.sh +dankclaw scripts/download-frank-dragoon-kingjones.sh +dankclaw scripts/download-frank-dragoon-orca.sh +dankclaw scripts/download-frank-dragoon-q4.sh +dankclaw scripts/download-frank-dragoon-swift.sh +dankclaw scripts/download-frank-dragoon-unsloth.sh +dankclaw scripts/download-frank-dragoon.sh +dankclaw scripts/download-frank-zealot.sh +dankclaw scripts/dump-gguf.py +dankclaw scripts/fleet-probes-swift15.sh +dankclaw scripts/frank-archon-expert-activity.py +dankclaw scripts/frank-archon.env +dankclaw scripts/frank-archon.service +dankclaw scripts/frank-dragoon.env frank-dragoon +dankclaw scripts/frank-dragoon.service frank-dragoon +dankclaw scripts/frank-zealot.env frank-zealot +dankclaw scripts/frank-zealot.service frank-zealot +dankclaw scripts/generate-prompts.py +dankclaw scripts/hf-fetch.sh +dankclaw scripts/hf-manifest.py +dankclaw scripts/hfget.sh +dankclaw scripts/homepage-status.py homepage +dankclaw scripts/homepage-status.service homepage +dankclaw scripts/homepage-status.timer homepage +dankclaw scripts/kld-gate-swift15.sh +dankclaw scripts/kld-then-calib.sh +dankclaw scripts/lib/llm-gates.sh +dankclaw scripts/make-calib-corpus.py +dankclaw scripts/mount-entropy.sh +dankclaw scripts/patches/glm53-compile-01-model-decorator.patch +dankclaw scripts/patches/glm53-compile-02-trace-safe-probes.patch +dankclaw scripts/patches/glm53-compile-03-fullgraph-optout.patch +dankclaw scripts/patches/glm53-compile-04-fused-qkv-rmsnorm-optout.patch +dankclaw scripts/patches/glm53-compile-05-workspace-acquisition-opaque.patch +dankclaw scripts/patches/glm53-dflash-01-aux-hidden-states.patch +dankclaw scripts/patches/glm53-dflash-04-kv-groups-draft.patch +dankclaw scripts/patches/glm53-dflash-04b-kv-registry-dflash.patch +dankclaw scripts/patches/gpu_worker-kv-cap.patch +dankclaw scripts/patches/qwen4exp-qsa-f32-mask.patch +dankclaw scripts/probe-frank-archon-kv.py +dankclaw scripts/qwen-sharp-v22.4.0.jinja +dankclaw scripts/qwen38-froggeric-v22.5.jinja +dankclaw scripts/recipes/qwen4exp-swift15-w4dn8-i8-hc8m.recipe +dankclaw scripts/run-swift15-bf16.sh +dankclaw scripts/searxng-render-settings.py +dankclaw scripts/serve-frank-archon.sh +dankclaw scripts/serve-frank-dragoon.sh frank-dragoon +dankclaw scripts/serve-frank-zealot.sh frank-zealot +dankclaw scripts/signal-relay.py alertmanager +dankclaw scripts/signal-relay.service alertmanager +dankclaw scripts/stack-qwen38-official-fp8.py +dankclaw scripts/test-changed.sh +dankclaw scripts/tmux-resurrect.sh +dankclaw scripts/verify-swift15-ple.py +dankclaw serverAppConfigs-docs/.obsidian/app.json +dankclaw serverAppConfigs-docs/Agent/Agent Docs and Edits.md +dankclaw serverAppConfigs-docs/Agent/Command Center Dotfiles.md +dankclaw serverAppConfigs-docs/Agent/OMP Harness.md +dankclaw serverAppConfigs-docs/Agent/index.md +dankclaw serverAppConfigs-docs/Architecture/Container Network Namespaces.md +dankclaw serverAppConfigs-docs/Architecture/Deploy State Triage.md +dankclaw serverAppConfigs-docs/Architecture/Deployment Flow.md +dankclaw serverAppConfigs-docs/Architecture/E2E Suite.md +dankclaw serverAppConfigs-docs/Architecture/Network Architecture.md +dankclaw serverAppConfigs-docs/Architecture/Quadlet Lifecycle.md +dankclaw serverAppConfigs-docs/Architecture/Stale Signal Audit.md +dankclaw serverAppConfigs-docs/Architecture/Storage Policy.md +dankclaw serverAppConfigs-docs/Architecture/Testing Policy.md +dankclaw serverAppConfigs-docs/Dankclaw/ComfyUI Qwen-Image Workflows.md +dankclaw serverAppConfigs-docs/Dankclaw/ComfyUI.md +dankclaw serverAppConfigs-docs/Dankclaw/Dankclaw Stack.md +dankclaw serverAppConfigs-docs/Dankclaw/Gitea.md +dankclaw serverAppConfigs-docs/Dankclaw/Gluetun.md +dankclaw serverAppConfigs-docs/Dankclaw/Hermes-Dashboard.md +dankclaw serverAppConfigs-docs/Dankclaw/Hermes-WebUI.md +dankclaw serverAppConfigs-docs/Dankclaw/Hermes.md +dankclaw serverAppConfigs-docs/Dankclaw/Marreta.md +dankclaw serverAppConfigs-docs/Dankclaw/OpenWebUI.md +dankclaw serverAppConfigs-docs/Dankclaw/Postgres.md +dankclaw serverAppConfigs-docs/Dankclaw/SearXNG Providers.md +dankclaw serverAppConfigs-docs/Dankclaw/SearXNG.md +dankclaw serverAppConfigs-docs/Dankclaw/Seerr.md +dankclaw serverAppConfigs-docs/Dankclaw/Swap Configuration.md +dankclaw serverAppConfigs-docs/Dankclaw/Valkey.md +dankclaw serverAppConfigs-docs/Dankserver/Calibre-Web.md +dankclaw serverAppConfigs-docs/Dankserver/Calibre.md +dankclaw serverAppConfigs-docs/Dankserver/Copyparty.md +dankclaw serverAppConfigs-docs/Dankserver/Dankserver Stack.md +dankclaw serverAppConfigs-docs/Dankserver/Entropy Volume.md +dankclaw serverAppConfigs-docs/Dankserver/Jellyfin.md +dankclaw serverAppConfigs-docs/Dankserver/Plex.md +dankclaw serverAppConfigs-docs/Dankserver/RomM-Scraping.md +dankclaw serverAppConfigs-docs/Dankserver/RomM.md +dankclaw serverAppConfigs-docs/Dankserver/Tautulli.md +dankclaw serverAppConfigs-docs/Dankserver/mariadb.md +dankclaw serverAppConfigs-docs/Dankserver/redis.md +dankclaw serverAppConfigs-docs/Desktop/Noctalia and Niri.md +dankclaw serverAppConfigs-docs/FrankBook-Pro/ARR-Stack.md +dankclaw serverAppConfigs-docs/FrankBook-Pro/index.md +dankclaw serverAppConfigs-docs/FrankBook-Pro/lidarr.md +dankclaw serverAppConfigs-docs/FrankBook-Pro/prowlarr.md +dankclaw serverAppConfigs-docs/FrankBook-Pro/qbittorrent.md +dankclaw serverAppConfigs-docs/FrankBook-Pro/radarr.md +dankclaw serverAppConfigs-docs/FrankBook-Pro/readarr.md +dankclaw serverAppConfigs-docs/FrankBook-Pro/sonarr.md +dankclaw serverAppConfigs-docs/Infrastructure/Alertmanager.md +dankclaw serverAppConfigs-docs/Infrastructure/Alloy.md +dankclaw serverAppConfigs-docs/Infrastructure/Anubis.md +dankclaw serverAppConfigs-docs/Infrastructure/Authentik.md +dankclaw serverAppConfigs-docs/Infrastructure/Caddy.md +dankclaw serverAppConfigs-docs/Infrastructure/FrankBook-Pro.md +dankclaw serverAppConfigs-docs/Infrastructure/Grafana.md +dankclaw serverAppConfigs-docs/Infrastructure/Homepage Design Grilling.md +dankclaw serverAppConfigs-docs/Infrastructure/Homepage Status Probes.md +dankclaw serverAppConfigs-docs/Infrastructure/Homepage.md +dankclaw serverAppConfigs-docs/Infrastructure/LAN-DNS.md +dankclaw serverAppConfigs-docs/Infrastructure/Loki.md +dankclaw serverAppConfigs-docs/Infrastructure/Node-Exporter.md +dankclaw serverAppConfigs-docs/Infrastructure/Observability Provisioning.md +dankclaw serverAppConfigs-docs/Infrastructure/Observability.md +dankclaw serverAppConfigs-docs/Infrastructure/Pi-hole.md +dankclaw serverAppConfigs-docs/Infrastructure/Prometheus.md +dankclaw serverAppConfigs-docs/Infrastructure/authentik-postgres.md +dankclaw serverAppConfigs-docs/Infrastructure/authentik-server.md +dankclaw serverAppConfigs-docs/Infrastructure/authentik-worker.md +dankclaw serverAppConfigs-docs/LLM/DFlash DSpark Speculative Decoding.md +dankclaw serverAppConfigs-docs/LLM/Dankclaw Improvements.md +dankclaw serverAppConfigs-docs/LLM/Dankclaw System Specs.md +dankclaw serverAppConfigs-docs/LLM/Dankserver System Specs.md +dankclaw serverAppConfigs-docs/LLM/Dual-R9700-Cooperation.md +dankclaw serverAppConfigs-docs/LLM/FastFlowLM NPU Inference.md +dankclaw serverAppConfigs-docs/LLM/FreeToken Builds.md +dankclaw serverAppConfigs-docs/LLM/GGUF Compatibility.md +dankclaw serverAppConfigs-docs/LLM/Heterogeneous Split Triage.md +dankclaw serverAppConfigs-docs/LLM/Hybrid CPU GPU Processing Dankserver.md +dankclaw serverAppConfigs-docs/LLM/LLM Bench Windows and Measurement.md +dankclaw serverAppConfigs-docs/LLM/LLM Performance Diagnosis.md +dankclaw serverAppConfigs-docs/LLM/Llama Cpp Multi-Device Splits.md +dankclaw serverAppConfigs-docs/LLM/Llama Cpp Serving Sizing.md +dankclaw serverAppConfigs-docs/LLM/Model Swap and Rollback.md +dankclaw serverAppConfigs-docs/LLM/PCIe Link and Atomics.md +dankclaw serverAppConfigs-docs/LLM/Pipeline Parallelism/Ethernet Bandwidth Math.md +dankclaw serverAppConfigs-docs/LLM/Pipeline Parallelism/Expected Performance.md +dankclaw serverAppConfigs-docs/LLM/Pipeline Parallelism/Mixed-Vendor Experiments.md +dankclaw serverAppConfigs-docs/LLM/Pipeline Parallelism/Qwen3.6 27B Spec Methods.md +dankclaw serverAppConfigs-docs/LLM/Pipeline Parallelism/Qwen3.8 PP Deployment.md +dankclaw serverAppConfigs-docs/LLM/Pipeline Parallelism/Sample Script Set.md +dankclaw serverAppConfigs-docs/LLM/Pipeline Parallelism/index.md +dankclaw serverAppConfigs-docs/LLM/Pipeline Parallelism/llama.cpp RPC.md +dankclaw serverAppConfigs-docs/LLM/Pipeline Parallelism/vLLM Pipeline Parallelism.md +dankclaw serverAppConfigs-docs/LLM/Quantization Production.md +dankclaw serverAppConfigs-docs/LLM/Quantization by Hardware Architecture.md +dankclaw serverAppConfigs-docs/LLM/RAM CPU Offloading in VLLM.md +dankclaw serverAppConfigs-docs/LLM/RDNA4 Container Serving.md +dankclaw serverAppConfigs-docs/LLM/RDNA4 MoE Expert Offload VRAM Budget.md +dankclaw serverAppConfigs-docs/LLM/Running Qwen3.6 27B FP8 on Dankclaw.md +dankclaw serverAppConfigs-docs/LLM/Running Qwen3.6 35B A3B FP8 on Dankserver.md +dankclaw serverAppConfigs-docs/LLM/Shared Memory Architecture Dankclaw.md +dankclaw serverAppConfigs-docs/LLM/Tcclaviger Fork Ops.md +dankclaw serverAppConfigs-docs/LLM/VLLM Index.md +dankclaw serverAppConfigs-docs/LLM/VLLM Overview.md +dankclaw serverAppConfigs-docs/LLM/agentionai-fast-imatrix.md +dankclaw serverAppConfigs-docs/LLM/ciru-activefpx-promptforge.md +dankclaw serverAppConfigs-docs/LLM/dankclaw-iommu-translation-off.md +dankclaw serverAppConfigs-docs/LLM/dankserver-frontier-moe-feasibility.md +dankclaw serverAppConfigs-docs/LLM/dankserver-qwen38-fp8-expert-offload-tuning.md +dankclaw serverAppConfigs-docs/LLM/dankserver-vram-growth.md +dankclaw serverAppConfigs-docs/LLM/dealign-ornith-crack.md +dankclaw serverAppConfigs-docs/LLM/deployment.md +dankclaw serverAppConfigs-docs/LLM/dflash2-large-vulkan.md +dankclaw serverAppConfigs-docs/LLM/eemin-rocmi4-mtp.md +dankclaw serverAppConfigs-docs/LLM/frank-archon-freetoken-pr587.md +dankclaw serverAppConfigs-docs/LLM/frank-archon-glm53-flash-w4a16.md +dankclaw serverAppConfigs-docs/LLM/frank-archon-llamacpp-qwen38-flash-next-q8.md +dankclaw serverAppConfigs-docs/LLM/frank-archon-lucebox-ds4.md +dankclaw serverAppConfigs-docs/LLM/frank-archon-r9v-radiance-qwen38-q8.md +dankclaw serverAppConfigs-docs/LLM/frank-archon-r9v-radiance-qwen38-uncensored.md +dankclaw serverAppConfigs-docs/LLM/frank-archon-radiance-qwen38-native-fp8.md +dankclaw serverAppConfigs-docs/LLM/frank-archon-radiance-swift15-w4dn8.md +dankclaw serverAppConfigs-docs/LLM/frank-dragoon-benchmark.md +dankclaw serverAppConfigs-docs/LLM/frank-dragoon-ciru-orca.md +dankclaw serverAppConfigs-docs/LLM/frank-dragoon-flash-server-v2-swift15.md +dankclaw serverAppConfigs-docs/LLM/frank-dragoon-gufo-qwen38-27b.md +dankclaw serverAppConfigs-docs/LLM/frank-dragoon-halogen27b-dflash2.md +dankclaw serverAppConfigs-docs/LLM/frank-dragoon-ik-uncensored.md +dankclaw serverAppConfigs-docs/LLM/frank-dragoon-kairic-edge-qwen38-27b.md +dankclaw serverAppConfigs-docs/LLM/frank-dragoon-rocmfpx-q6agent-uncensored-mtp.md +dankclaw serverAppConfigs-docs/LLM/frank-dragoon-rocmfpx-q8agent-mtp.md +dankclaw serverAppConfigs-docs/LLM/frank-dragoon-unsloth-qwen38-27b-q8xl.md +dankclaw serverAppConfigs-docs/LLM/frank-dragoon-unsloth-qwen38-flash-next.md +dankclaw serverAppConfigs-docs/LLM/frank-dragoon-vllm-mimo-v26-bf16.md +dankclaw serverAppConfigs-docs/LLM/frank-medium-batch-ubatch.md +dankclaw serverAppConfigs-docs/LLM/frank-medium-llamacpp-dflash2.md +dankclaw serverAppConfigs-docs/LLM/frank-medium-model-swap.md +dankclaw serverAppConfigs-docs/LLM/frank-medium-radiance-mxfp4.md +dankclaw serverAppConfigs-docs/LLM/frank-medium-rocmfpx.md +dankclaw serverAppConfigs-docs/LLM/frank-npu.md +dankclaw serverAppConfigs-docs/LLM/frank-redo-2026-08-17.md +dankclaw serverAppConfigs-docs/LLM/frank-zealot-benchmark.md +dankclaw serverAppConfigs-docs/LLM/frank-zealot-gemma4-12b.md +dankclaw serverAppConfigs-docs/LLM/frank-zealot-llama-gemma4-q4km-200704.md +dankclaw serverAppConfigs-docs/LLM/frank-zealot-llama-mimo-q5km.md +dankclaw serverAppConfigs-docs/LLM/frank-zealot-llama-mimo-q6k-2x200704.md +dankclaw serverAppConfigs-docs/LLM/frank-zealot-llama-mimo-q6k.md +dankclaw serverAppConfigs-docs/LLM/frank-zealot-llama-qwen35-9b-q6k-2x200704.md +dankclaw serverAppConfigs-docs/LLM/frank-zealot-llama-swift15-unc-iq3xxs-mtp-114688.md +dankclaw serverAppConfigs-docs/LLM/frank-zealot-qwen38-gsq-rco.md +dankclaw serverAppConfigs-docs/LLM/frank-zealot-sglang-mimo-v26.md +dankclaw serverAppConfigs-docs/LLM/frank-zealot-vllm-gemma4.md +dankclaw serverAppConfigs-docs/LLM/freetoken-benchmark-sweep.md +dankclaw serverAppConfigs-docs/LLM/freetoken-frank-small.md +dankclaw serverAppConfigs-docs/LLM/halogen-flash-server.md +dankclaw serverAppConfigs-docs/LLM/ikllama-ornith-dankserver.md +dankclaw serverAppConfigs-docs/LLM/index.md +dankclaw serverAppConfigs-docs/LLM/kairic-edge.md +dankclaw serverAppConfigs-docs/LLM/kingjones-q8agent-rocmfpx.md +dankclaw serverAppConfigs-docs/LLM/kingjones-strix-mtp.md +dankclaw serverAppConfigs-docs/LLM/kingjones-strix-rocmfp4.md +dankclaw serverAppConfigs-docs/LLM/ling-3.0-flash-trial.md +dankclaw serverAppConfigs-docs/LLM/ling-3.0-flash-vllm-plan.md +dankclaw serverAppConfigs-docs/LLM/qwen3.8-spec-matrix.md +dankclaw serverAppConfigs-docs/LLM/qwen3.8-strix-optimizations.md +dankclaw serverAppConfigs-docs/LLM/swift15-campaign-worklog.md +dankclaw serverAppConfigs-docs/LLM/ubatch-benchmark-large.md +dankclaw serverAppConfigs-docs/LLM/vLLM Boot Forensics and Config Interrogation.md +dankclaw serverAppConfigs-docs/LLM/vLLM Porting and Compile Triage.md +dankclaw serverAppConfigs-docs/LLM/vllm_research_findings.md +dankclaw serverAppConfigs-docs/LLM/x4-pair-experiment.md +dankclaw serverAppConfigs-docs/Operations/Container Host Reachability.md +dankclaw serverAppConfigs-docs/Operations/Fleet Audit and Posture.md +dankclaw serverAppConfigs-docs/Operations/Fleet Ops Playbook.md +dankclaw serverAppConfigs-docs/Operations/Payload and Disk Reclaim.md +dankclaw serverAppConfigs-docs/Operations/Remote Job Runner.md +dankclaw serverAppConfigs-docs/Operations/SSH and Remote Access.md +dankclaw serverAppConfigs-docs/Operations/index.md +dankclaw serverAppConfigs-docs/Research/AI Job Search and Resume Tools.md +dankclaw serverAppConfigs-docs/Research/Backblaze Integration.md +dankclaw serverAppConfigs-docs/Research/ComfyUI ROCm and Vulkan.md +dankclaw serverAppConfigs-docs/Research/Lemonade and sd.cpp.md +dankclaw serverAppConfigs-docs/Research/Llama Cpp Code Diff.md +dankclaw serverAppConfigs-docs/Research/Llama Cpp Runtimes.md +dankclaw serverAppConfigs-docs/Research/Local TTS and Voice Cloning.md +dankclaw serverAppConfigs-docs/Research/Most Popular Self-Hosted AI Apps.md +dankclaw serverAppConfigs-docs/Research/PICO-8 Asset Standards.md +dankclaw serverAppConfigs-docs/Research/SGLang.md +dankclaw serverAppConfigs-docs/Research/Service Research Brief.md +dankclaw serverAppConfigs-docs/Research/index.md +dankclaw serverAppConfigs-docs/Research/popular-self-hosted-ai-apps.html +dankclaw serverAppConfigs-docs/index.md +dankclaw serverAppConfigs-docs/log.md +dankclaw shared/alloy/config.alloy alloy +dankclaw shared/grafana/provisioning/dashboards/llm-fleet.json grafana +dankclaw shared/grafana/provisioning/dashboards/provider.yml grafana +dankclaw shared/grafana/provisioning/datasources/prometheus.yml grafana +dankclaw shared/homepage/admin.html homepage +dankclaw shared/homepage/app.js homepage +dankclaw shared/homepage/dankserver.services.json homepage +dankclaw shared/homepage/docs.html homepage +dankclaw shared/homepage/img/apps/app-calibre.svg homepage +dankclaw shared/homepage/img/apps/app-folder.svg homepage +dankclaw shared/homepage/img/apps/app-gameboy.svg homepage +dankclaw shared/homepage/img/apps/app-wall.svg homepage +dankclaw shared/homepage/img/apps/caddy.svg homepage +dankclaw shared/homepage/img/apps/gitea.svg homepage +dankclaw shared/homepage/img/apps/grafana.svg homepage +dankclaw shared/homepage/img/apps/jellyfin.svg homepage +dankclaw shared/homepage/img/apps/mariadb.svg homepage +dankclaw shared/homepage/img/apps/mono-am.svg homepage +dankclaw shared/homepage/img/apps/mono-ay.svg homepage +dankclaw shared/homepage/img/apps/mono-cp.svg homepage +dankclaw shared/homepage/img/apps/mono-cu.svg homepage +dankclaw shared/homepage/img/apps/mono-cw.svg homepage +dankclaw shared/homepage/img/apps/mono-gt.svg homepage +dankclaw shared/homepage/img/apps/mono-hd.svg homepage +dankclaw shared/homepage/img/apps/mono-hm.svg homepage +dankclaw shared/homepage/img/apps/mono-ld.svg homepage +dankclaw shared/homepage/img/apps/mono-lk.svg homepage +dankclaw shared/homepage/img/apps/mono-mr.svg homepage +dankclaw shared/homepage/img/apps/mono-ne.svg homepage +dankclaw shared/homepage/img/apps/mono-ow.svg homepage +dankclaw shared/homepage/img/apps/mono-pw.svg homepage +dankclaw shared/homepage/img/apps/mono-ra.svg homepage +dankclaw shared/homepage/img/apps/mono-rm.svg homepage +dankclaw shared/homepage/img/apps/mono-sr.svg homepage +dankclaw shared/homepage/img/apps/mono-tt.svg homepage +dankclaw shared/homepage/img/apps/mono-vk.svg homepage +dankclaw shared/homepage/img/apps/openwebui.png homepage +dankclaw shared/homepage/img/apps/pihole.svg homepage +dankclaw shared/homepage/img/apps/plex.svg homepage +dankclaw shared/homepage/img/apps/postgresql.svg homepage +dankclaw shared/homepage/img/apps/prometheus.svg homepage +dankclaw shared/homepage/img/apps/qbittorrent.svg homepage +dankclaw shared/homepage/img/apps/radarr.svg homepage +dankclaw shared/homepage/img/apps/searxng.svg homepage +dankclaw shared/homepage/img/apps/seerr.svg homepage +dankclaw shared/homepage/img/apps/sonarr.svg homepage +dankclaw shared/homepage/img/openwebui-api-keys.png homepage +dankclaw shared/homepage/img/openwebui-chat.png homepage +dankclaw shared/homepage/img/openwebui-model-picker.png homepage +dankclaw shared/homepage/img/unit-archon.jpg homepage +dankclaw shared/homepage/img/unit-dragoon.jpg homepage +dankclaw shared/homepage/img/unit-zealot.jpg homepage +dankclaw shared/homepage/index.html homepage +dankclaw shared/homepage/services.json homepage +dankclaw shared/homepage/sfx/archon-burn.ogg homepage +dankclaw shared/homepage/sfx/archon-power.ogg homepage +dankclaw shared/homepage/sfx/dragoon-awaiting.ogg homepage +dankclaw shared/homepage/sfx/dragoon-tagline.ogg homepage +dankclaw shared/homepage/sfx/zealot-combat.ogg homepage +dankclaw shared/homepage/sfx/zealot-honor.ogg homepage +dankclaw shared/homepage/style.css homepage +dankclaw shared/loki/loki-config.yaml loki +dankclaw shared/prometheus/alertmanager.yml prometheus,alertmanager +dankclaw shared/prometheus/prometheus.yml prometheus +dankclaw shared/prometheus/rules/llm-fleet.yml prometheus +dankclaw shared/searxng/limiter.toml searxng +dankclaw shared/searxng/settings.yml searxng +dankclaw tests/.AGENTTRASH/20261013-compose-retire/compose_validity.bats +dankclaw tests/.AGENTTRASH/frank_medium_proxy.bats +dankclaw tests/.AGENTTRASH/frank_medium_quadlet.bats +dankclaw tests/anubis.bats +dankclaw tests/authentik.bats +dankclaw tests/autoresearch_measure.bats +dankclaw tests/caddy_quadlet.bats +dankclaw tests/caddyfile.bats +dankclaw tests/changed_tests.bats +dankclaw tests/comfyui_rocm.bats +dankclaw tests/dankserver_quadlet.bats +dankclaw tests/deploy_sandbox.bash +dankclaw tests/deployment_flow.bats +dankclaw tests/deployment_scoping.bats +dankclaw tests/docs_completeness.bats +dankclaw tests/e2e/.env.local.example +dankclaw tests/e2e/lib/env.ts +dankclaw tests/e2e/lib/login.ts +dankclaw tests/e2e/package-lock.json +dankclaw tests/e2e/package.json +dankclaw tests/e2e/playwright.config.ts +dankclaw tests/e2e/run.sh +dankclaw tests/e2e/specs/00-auth.setup.ts +dankclaw tests/e2e/specs/10-homepage.spec.ts +dankclaw tests/e2e/specs/11-admin.spec.ts +dankclaw tests/e2e/specs/12-crawl.spec.ts +dankclaw tests/e2e/specs/13-sso.spec.ts +dankclaw tests/e2e/specs/14-guard.spec.ts +dankclaw tests/e2e/specs/15-unauth-fetch.spec.ts +dankclaw tests/e2e/specs/16-marreta.spec.ts +dankclaw tests/e2e/specs/17-hermes-vhost.spec.ts +dankclaw tests/e2e/specs/18-openwebui-sso.spec.ts +dankclaw tests/e2e/specs/19-frankbook.spec.ts +dankclaw tests/e2e_scaffold.bats +dankclaw tests/env_consistency.bats +dankclaw tests/frank_archon_unit.bats +dankclaw tests/frank_dragoon_unit.bats +dankclaw tests/frank_zealot_unit.bats +dankclaw tests/frankbook_quadlet.bats +dankclaw tests/helpers.bash +dankclaw tests/hermes_webui.bats +dankclaw tests/homepage_caddy.bats +dankclaw tests/homepage_checker.bats +dankclaw tests/homepage_config.bats +dankclaw tests/homepage_deploy.bats +dankclaw tests/homepage_pages.bats +dankclaw tests/llm_deploy_gates.bats +dankclaw tests/llm_gates.bats +dankclaw tests/llm_secrets.bats +dankclaw tests/llm_units.bats +dankclaw tests/marreta_quadlet.bats +dankclaw tests/mount_entropy.bats +dankclaw tests/observability_quadlet.bats +dankclaw tests/open_webui_quadlet.bats +dankclaw tests/pihole.bats +dankclaw tests/run.sh +dankclaw tests/scripts_syntax.bats +dankclaw tests/searxng_limiter.bats +dankclaw tests/searxng_quadlet.bats +dankclaw tests/seerr_quadlet.bats +dankclaw tests/stubs/curl +dankclaw tests/stubs/podman +dankclaw tests/stubs/sleep +dankclaw tests/stubs/ss +dankclaw tests/stubs/sudo +dankclaw tests/stubs/systemctl +dankclaw dankclaw/searxng/quadlet/gluetun.container+shared/searxng/settings.yml gluetun,searxng,marreta +dankclaw dankclaw/Caddyfile+dankclaw/searxng/quadlet/searxng.container+dankclaw/observability/quadlet/prometheus.container searxng,prometheus,caddy +dankclaw scripts/frank-dragoon.env+dankclaw/pihole/addn-hosts pihole,frank-dragoon +dankclaw dankserver/compose.yaml+dankserver/romm/quadlet/romm.container +dankclaw dankserver/compose.yaml+scripts/frank-archon.env+dankserver/observability/quadlet/node-exporter.container +dankclaw dankclaw/observability/quadlet/alertmanager.container+shared/prometheus/alertmanager.yml+scripts/signal-relay.py prometheus,alertmanager +dankclaw frankbook-pro/gluetun/quadlet/gluetun.container+frankbook-pro/sonarr/quadlet/sonarr.container +dankclaw dankclaw/searxng/quadlet/gluetun.container+shared/searxng/settings.yml gluetun,searxng,marreta +dankclaw dankclaw/Caddyfile+dankclaw/searxng/quadlet/searxng.container+dankclaw/observability/quadlet/prometheus.container searxng,prometheus,caddy +dankclaw scripts/frank-dragoon.env+dankclaw/pihole/addn-hosts pihole,frank-dragoon +dankclaw dankserver/compose.yaml+dankserver/romm/quadlet/romm.container +dankclaw dankserver/compose.yaml+scripts/frank-archon.env+dankserver/observability/quadlet/node-exporter.container +dankclaw dankclaw/observability/quadlet/alertmanager.container+shared/prometheus/alertmanager.yml+scripts/signal-relay.py prometheus,alertmanager +dankclaw frankbook-pro/gluetun/quadlet/gluetun.container+frankbook-pro/sonarr/quadlet/sonarr.container +dankclaw dankclaw/searxng/quadlet/gluetun.container+shared/searxng/settings.yml gluetun,searxng,marreta +dankclaw dankclaw/Caddyfile+dankclaw/searxng/quadlet/searxng.container+dankclaw/observability/quadlet/prometheus.container searxng,prometheus,caddy +dankclaw scripts/frank-dragoon.env+dankclaw/pihole/addn-hosts pihole,frank-dragoon +dankclaw dankserver/compose.yaml+dankserver/romm/quadlet/romm.container +dankclaw dankserver/compose.yaml+scripts/frank-archon.env+dankserver/observability/quadlet/node-exporter.container +dankclaw dankclaw/observability/quadlet/alertmanager.container+shared/prometheus/alertmanager.yml+scripts/signal-relay.py prometheus,alertmanager +dankclaw frankbook-pro/gluetun/quadlet/gluetun.container+frankbook-pro/sonarr/quadlet/sonarr.container +dankserver .AGENTTRASH/frank-default-small.env.llama.cpp +dankserver .AGENTTRASH/frank-default-small.service.llama.cpp +dankserver .AGENTTRASH/frank-dragoon.env.llama.cpp +dankserver .AGENTTRASH/frank-dragoon.service.llama.cpp +dankserver .AGENTTRASH/frank-npu.env.flm-npu +dankserver .AGENTTRASH/frank-npu.service.flm-npu +dankserver .AGENTTRASH/serve-frank-dragoon.sh.llama.cpp +dankserver .AGENTTRASH/serve-frank-npu.sh.flm-npu +dankserver .AGENTTRASH/serve-frank-small.sh.llama.cpp +dankserver .githooks/pre-commit +dankserver .gitignore +dankserver AGENTS.md +dankserver README.md +dankserver autoresearch.sh +dankserver dankclaw/.env.example +dankserver dankclaw/Caddyfile +dankserver dankclaw/Containerfile.comfyui +dankserver dankclaw/alloy/Containerfile +dankserver dankclaw/anubis/policy.json +dankserver dankclaw/anubis/quadlet/anubis.container +dankserver dankclaw/apply_qwen21_patch.py +dankserver dankclaw/authentik/quadlet/authentik-outpost-ldap.container +dankserver dankclaw/authentik/quadlet/authentik-outpost.container +dankserver dankclaw/authentik/quadlet/authentik-postgres.container +dankserver dankclaw/authentik/quadlet/authentik-server.container +dankserver dankclaw/authentik/quadlet/authentik-worker.container +dankserver dankclaw/caddy/Containerfile +dankserver dankclaw/caddy/env.example +dankserver dankclaw/caddy/quadlet/caddy.container +dankserver dankclaw/comfyui-start.sh +dankserver dankclaw/comfyui/comfyui-healthcheck.py +dankserver dankclaw/comfyui/quadlet/comfyui.container +dankserver dankclaw/comfyui/workflows/qwen-image-2.1-t2i-art.json +dankserver dankclaw/comfyui/workflows/qwen-image-2.1-t2i-legible-text.json +dankserver dankclaw/gitea/.env.example +dankserver dankclaw/gitea/quadlet/gitea-postgres.container +dankserver dankclaw/gitea/quadlet/gitea-server.container +dankserver dankclaw/gitea/quadlet/gitea.network +dankserver dankclaw/hermes/config.yaml.example +dankserver dankclaw/hermes/quadlet/chrome-devtools-protocol.container +dankserver dankclaw/hermes/quadlet/hermes-dashboard.container +dankserver dankclaw/hermes/quadlet/hermes-webui.container +dankserver dankclaw/hermes/quadlet/hermes.container +dankserver dankclaw/loki/Containerfile +dankserver dankclaw/marreta/quadlet/marreta-browser.container +dankserver dankclaw/marreta/quadlet/marreta.container +dankserver dankclaw/observability/quadlet/alertmanager.container +dankserver dankclaw/observability/quadlet/alloy.container +dankserver dankclaw/observability/quadlet/grafana.container +dankserver dankclaw/observability/quadlet/loki.container +dankserver dankclaw/observability/quadlet/node-exporter.container +dankserver dankclaw/observability/quadlet/prometheus.container +dankserver dankclaw/open-webui/quadlet/open-webui.container +dankserver dankclaw/open-webui/quadlet/open-webui.network +dankserver dankclaw/pihole/addn-hosts +dankserver dankclaw/pihole/env.example +dankserver dankclaw/pihole/quadlet/pihole.container +dankserver dankclaw/pihole/sysctl/99-dnsmasq.conf +dankserver dankclaw/searxng/quadlet/gluetun.container +dankserver dankclaw/searxng/quadlet/searxng-valkey.container +dankserver dankclaw/searxng/quadlet/searxng.container +dankserver dankclaw/searxng/quadlet/searxng.network +dankserver dankclaw/seerr/quadlet/seerr.container +dankserver dankclaw/udev/99-comfyui-gpu-access.rules +dankserver dankserver/.AGENTTRASH/20260916-vllm-radiance/affinity-proxy.py +dankserver dankserver/.AGENTTRASH/20260916-vllm-radiance/frank-medium-a.container +dankserver dankserver/.AGENTTRASH/20260916-vllm-radiance/frank-medium-b.container +dankserver dankserver/.AGENTTRASH/20260916-vllm-radiance/frank-medium-proxy.container +dankserver dankserver/.AGENTTRASH/20260916-vllm-radiance/routing.py +dankserver dankserver/.AGENTTRASH/20261013-compose-retire/.env.example +dankserver dankserver/.AGENTTRASH/20261013-compose-retire/compose.yaml +dankserver dankserver/calibre-server/quadlet/calibre-server.container calibre-server +dankserver dankserver/calibre-web/quadlet/calibre-web.container calibre-web +dankserver dankserver/copyparty/quadlet/copyparty.container copyparty +dankserver dankserver/jellyfin/quadlet/jellyfin.container jellyfin +dankserver dankserver/observability/quadlet/node-exporter.container node-exporter +dankserver dankserver/plex/quadlet/plex.container plex +dankserver dankserver/romm/quadlet/romm-mariadb.container romm-mariadb,romm-redis,romm +dankserver dankserver/romm/quadlet/romm-redis.container romm-mariadb,romm-redis,romm +dankserver dankserver/romm/quadlet/romm.container romm-mariadb,romm-redis,romm +dankserver dankserver/tautulli/quadlet/tautulli.container tautulli +dankserver echo_prompt.txt +dankserver echo_request.json +dankserver frankbook-pro/gluetun/quadlet/gluetun.container +dankserver frankbook-pro/lidarr/quadlet/lidarr.container +dankserver frankbook-pro/prowlarr/quadlet/prowlarr.container +dankserver frankbook-pro/qbittorrent/quadlet/qbittorrent.container +dankserver frankbook-pro/radarr/quadlet/radarr.container +dankserver frankbook-pro/readarr/quadlet/readarr.container +dankserver frankbook-pro/sonarr/quadlet/sonarr.container +dankserver old/download-frank-archon-fp8.sh +dankserver old/download-frank-dragoon-dflash.sh +dankserver old/download-frank-dragoon-mimo.sh +dankserver old/download-frank-dragoon-qwen35.sh +dankserver old/download-frank-zealot-sglang-int8.sh +dankserver old/qwen3.8-flash-next-fp8.manifest +dankserver old/scripts/analyze-spec-matrix.py +dankserver old/scripts/bench-frank-dragoon.sh +dankserver old/scripts/bench-high-ctx.sh +dankserver old/scripts/bench-pipeline-64k.sh +dankserver old/scripts/deploy.sh +dankserver old/scripts/frank-default-large.env +dankserver old/scripts/frank-default-large.ling.env +dankserver old/scripts/frank-default-large.service +dankserver old/scripts/frank-default-small.env +dankserver old/scripts/frank-default-small.service +dankserver old/scripts/frank-dragoon-ling.service +dankserver old/scripts/frank-pp-worker.env +dankserver old/scripts/frank-pp-worker.service +dankserver old/scripts/llama-server.service +dankserver old/scripts/pp-bench.py +dankserver old/scripts/pp-test.sh +dankserver old/scripts/prune-rpc-cache.sh +dankserver old/scripts/serve-flm-small.sh +dankserver old/scripts/serve-ling-flash.sh +dankserver old/scripts/serve-ling3-vllm.sh +dankserver old/scripts/serve-llama-frank-dragoon.sh +dankserver old/scripts/serve-llama-frank-small.sh +dankserver old/scripts/serve-pp-head.sh +dankserver old/scripts/serve-pp-worker.sh +dankserver old/scripts/serve-vllm-qwen36-dflash.sh +dankserver old/scripts/spec-matrix-bench.sh +dankserver old/scripts/test_dspark_stream.py +dankserver old/scripts/validate-vllm.sh +dankserver prose_prompt.txt +dankserver prose_request.json +dankserver report/doc-audit-2026-08-23.html +dankserver report/homepage-grilling-2026-08-22.html +dankserver scripts/.AGENTTRASH/20260920-llama-layersplit/bench-spec-ab.py.llamacpp-layersplit +dankserver scripts/.AGENTTRASH/20260920-llama-layersplit/frank-medium.env.llamacpp-layersplit +dankserver scripts/.AGENTTRASH/20260920-llama-layersplit/frank-medium.service.llamacpp-layersplit +dankserver scripts/.AGENTTRASH/20260920-llama-layersplit/serve-frank-medium.sh.llamacpp-layersplit +dankserver scripts/.AGENTTRASH/bench-decode-threads-batch.py.strix-rocmfp4 +dankserver scripts/.AGENTTRASH/bench-ngram-mod.py.ik-llama.cpp +dankserver scripts/.AGENTTRASH/bench-ngram-mod.sh.ik-llama.cpp +dankserver scripts/.AGENTTRASH/bench-spec-decode.py.ik-llama.cpp +dankserver scripts/.AGENTTRASH/bench-vllm-corpus.py +dankserver scripts/.AGENTTRASH/bench-vllm-serve.sh +dankserver scripts/.AGENTTRASH/chat-template.jinja.ik-llama-qwen-sharp +dankserver scripts/.AGENTTRASH/download-q8agent.sh.rocmfpx +dankserver scripts/.AGENTTRASH/frank-default-npu.env +dankserver scripts/.AGENTTRASH/frank-default-npu.service +dankserver scripts/.AGENTTRASH/frank-dragoon.env.ik-llama.cpp +dankserver scripts/.AGENTTRASH/frank-dragoon.service.ik-llama.cpp +dankserver scripts/.AGENTTRASH/frank-medium.env.rocmfpx +dankserver scripts/.AGENTTRASH/frank-medium.service.rocmfpx +dankserver scripts/.AGENTTRASH/serve-frank-default-npu.sh +dankserver scripts/.AGENTTRASH/serve-frank-dragoon.sh.ik-llama.cpp +dankserver scripts/.AGENTTRASH/serve-frank-medium.sh.rocmfpx +dankserver scripts/apply-protected-cast.sh +dankserver scripts/autoresearch-bench.py +dankserver scripts/autoresearch-frank-archon.sh +dankserver scripts/autoresearch/frank-archon-arms.sh +dankserver scripts/autoresearch/frank-archon-measure.py +dankserver scripts/autoresearch/frank-archon-window.sh +dankserver scripts/bench-batch-ubatch.sh +dankserver scripts/bench-conversation.py +dankserver scripts/bench-decode.sh +dankserver scripts/bench-llm-soak.py +dankserver scripts/bench-probe.py +dankserver scripts/bench-threads-batch-http.py +dankserver scripts/bench-threads-batch.sh +dankserver scripts/bench-turn-latency.py +dankserver scripts/bench-ubatch.sh +dankserver scripts/bench_tb_fixed.py +dankserver scripts/build-frank-archon-rad.sh +dankserver scripts/build-gufo.sh +dankserver scripts/build-kairic-edge-gfx1151.sh +dankserver scripts/build-llama-rdna4.sh +dankserver scripts/build-rocmfpx.sh +dankserver scripts/calib-push.py +dankserver scripts/check-vault-links.py +dankserver scripts/convert-swift15-w4dn8.sh +dankserver scripts/dankserver-route-primary.sh +dankserver scripts/dankstatus.sh +dankserver scripts/deep-probe.py +dankserver scripts/deploy-open-webui-quadlet.sh +dankserver scripts/deploy-pihole-quadlet.sh +dankserver scripts/deploy-plan.py +dankserver scripts/deploy-remote.sh +dankserver scripts/deploy-searxng-quadlet.sh +dankserver scripts/deploy.sh +dankserver scripts/derive-protected-experts.py +dankserver scripts/download-frank-archon-swift15.sh +dankserver scripts/download-frank-archon.sh +dankserver scripts/download-frank-dragoon-flashnext.sh +dankserver scripts/download-frank-dragoon-halogen.sh +dankserver scripts/download-frank-dragoon-kingjones-uncensored.sh +dankserver scripts/download-frank-dragoon-kingjones.sh +dankserver scripts/download-frank-dragoon-orca.sh +dankserver scripts/download-frank-dragoon-q4.sh +dankserver scripts/download-frank-dragoon-swift.sh +dankserver scripts/download-frank-dragoon-unsloth.sh +dankserver scripts/download-frank-dragoon.sh +dankserver scripts/download-frank-zealot.sh +dankserver scripts/dump-gguf.py +dankserver scripts/fleet-probes-swift15.sh +dankserver scripts/frank-archon-expert-activity.py +dankserver scripts/frank-archon.env frank-archon +dankserver scripts/frank-archon.service frank-archon +dankserver scripts/frank-dragoon.env +dankserver scripts/frank-dragoon.service +dankserver scripts/frank-zealot.env +dankserver scripts/frank-zealot.service +dankserver scripts/generate-prompts.py +dankserver scripts/hf-fetch.sh +dankserver scripts/hf-manifest.py +dankserver scripts/hfget.sh +dankserver scripts/homepage-status.py +dankserver scripts/homepage-status.service +dankserver scripts/homepage-status.timer +dankserver scripts/kld-gate-swift15.sh +dankserver scripts/kld-then-calib.sh +dankserver scripts/lib/llm-gates.sh +dankserver scripts/make-calib-corpus.py +dankserver scripts/mount-entropy.sh +dankserver scripts/patches/glm53-compile-01-model-decorator.patch +dankserver scripts/patches/glm53-compile-02-trace-safe-probes.patch +dankserver scripts/patches/glm53-compile-03-fullgraph-optout.patch +dankserver scripts/patches/glm53-compile-04-fused-qkv-rmsnorm-optout.patch +dankserver scripts/patches/glm53-compile-05-workspace-acquisition-opaque.patch +dankserver scripts/patches/glm53-dflash-01-aux-hidden-states.patch +dankserver scripts/patches/glm53-dflash-04-kv-groups-draft.patch +dankserver scripts/patches/glm53-dflash-04b-kv-registry-dflash.patch +dankserver scripts/patches/gpu_worker-kv-cap.patch +dankserver scripts/patches/qwen4exp-qsa-f32-mask.patch +dankserver scripts/probe-frank-archon-kv.py +dankserver scripts/qwen-sharp-v22.4.0.jinja +dankserver scripts/qwen38-froggeric-v22.5.jinja frank-archon +dankserver scripts/recipes/qwen4exp-swift15-w4dn8-i8-hc8m.recipe +dankserver scripts/run-swift15-bf16.sh +dankserver scripts/searxng-render-settings.py +dankserver scripts/serve-frank-archon.sh frank-archon +dankserver scripts/serve-frank-dragoon.sh +dankserver scripts/serve-frank-zealot.sh +dankserver scripts/signal-relay.py +dankserver scripts/signal-relay.service +dankserver scripts/stack-qwen38-official-fp8.py +dankserver scripts/test-changed.sh +dankserver scripts/tmux-resurrect.sh +dankserver scripts/verify-swift15-ple.py +dankserver serverAppConfigs-docs/.obsidian/app.json +dankserver serverAppConfigs-docs/Agent/Agent Docs and Edits.md +dankserver serverAppConfigs-docs/Agent/Command Center Dotfiles.md +dankserver serverAppConfigs-docs/Agent/OMP Harness.md +dankserver serverAppConfigs-docs/Agent/index.md +dankserver serverAppConfigs-docs/Architecture/Container Network Namespaces.md +dankserver serverAppConfigs-docs/Architecture/Deploy State Triage.md +dankserver serverAppConfigs-docs/Architecture/Deployment Flow.md +dankserver serverAppConfigs-docs/Architecture/E2E Suite.md +dankserver serverAppConfigs-docs/Architecture/Network Architecture.md +dankserver serverAppConfigs-docs/Architecture/Quadlet Lifecycle.md +dankserver serverAppConfigs-docs/Architecture/Stale Signal Audit.md +dankserver serverAppConfigs-docs/Architecture/Storage Policy.md +dankserver serverAppConfigs-docs/Architecture/Testing Policy.md +dankserver serverAppConfigs-docs/Dankclaw/ComfyUI Qwen-Image Workflows.md +dankserver serverAppConfigs-docs/Dankclaw/ComfyUI.md +dankserver serverAppConfigs-docs/Dankclaw/Dankclaw Stack.md +dankserver serverAppConfigs-docs/Dankclaw/Gitea.md +dankserver serverAppConfigs-docs/Dankclaw/Gluetun.md +dankserver serverAppConfigs-docs/Dankclaw/Hermes-Dashboard.md +dankserver serverAppConfigs-docs/Dankclaw/Hermes-WebUI.md +dankserver serverAppConfigs-docs/Dankclaw/Hermes.md +dankserver serverAppConfigs-docs/Dankclaw/Marreta.md +dankserver serverAppConfigs-docs/Dankclaw/OpenWebUI.md +dankserver serverAppConfigs-docs/Dankclaw/Postgres.md +dankserver serverAppConfigs-docs/Dankclaw/SearXNG Providers.md +dankserver serverAppConfigs-docs/Dankclaw/SearXNG.md +dankserver serverAppConfigs-docs/Dankclaw/Seerr.md +dankserver serverAppConfigs-docs/Dankclaw/Swap Configuration.md +dankserver serverAppConfigs-docs/Dankclaw/Valkey.md +dankserver serverAppConfigs-docs/Dankserver/Calibre-Web.md +dankserver serverAppConfigs-docs/Dankserver/Calibre.md +dankserver serverAppConfigs-docs/Dankserver/Copyparty.md +dankserver serverAppConfigs-docs/Dankserver/Dankserver Stack.md +dankserver serverAppConfigs-docs/Dankserver/Entropy Volume.md +dankserver serverAppConfigs-docs/Dankserver/Jellyfin.md +dankserver serverAppConfigs-docs/Dankserver/Plex.md +dankserver serverAppConfigs-docs/Dankserver/RomM-Scraping.md +dankserver serverAppConfigs-docs/Dankserver/RomM.md +dankserver serverAppConfigs-docs/Dankserver/Tautulli.md +dankserver serverAppConfigs-docs/Dankserver/mariadb.md +dankserver serverAppConfigs-docs/Dankserver/redis.md +dankserver serverAppConfigs-docs/Desktop/Noctalia and Niri.md +dankserver serverAppConfigs-docs/FrankBook-Pro/ARR-Stack.md +dankserver serverAppConfigs-docs/FrankBook-Pro/index.md +dankserver serverAppConfigs-docs/FrankBook-Pro/lidarr.md +dankserver serverAppConfigs-docs/FrankBook-Pro/prowlarr.md +dankserver serverAppConfigs-docs/FrankBook-Pro/qbittorrent.md +dankserver serverAppConfigs-docs/FrankBook-Pro/radarr.md +dankserver serverAppConfigs-docs/FrankBook-Pro/readarr.md +dankserver serverAppConfigs-docs/FrankBook-Pro/sonarr.md +dankserver serverAppConfigs-docs/Infrastructure/Alertmanager.md +dankserver serverAppConfigs-docs/Infrastructure/Alloy.md +dankserver serverAppConfigs-docs/Infrastructure/Anubis.md +dankserver serverAppConfigs-docs/Infrastructure/Authentik.md +dankserver serverAppConfigs-docs/Infrastructure/Caddy.md +dankserver serverAppConfigs-docs/Infrastructure/FrankBook-Pro.md +dankserver serverAppConfigs-docs/Infrastructure/Grafana.md +dankserver serverAppConfigs-docs/Infrastructure/Homepage Design Grilling.md +dankserver serverAppConfigs-docs/Infrastructure/Homepage Status Probes.md +dankserver serverAppConfigs-docs/Infrastructure/Homepage.md +dankserver serverAppConfigs-docs/Infrastructure/LAN-DNS.md +dankserver serverAppConfigs-docs/Infrastructure/Loki.md +dankserver serverAppConfigs-docs/Infrastructure/Node-Exporter.md +dankserver serverAppConfigs-docs/Infrastructure/Observability Provisioning.md +dankserver serverAppConfigs-docs/Infrastructure/Observability.md +dankserver serverAppConfigs-docs/Infrastructure/Pi-hole.md +dankserver serverAppConfigs-docs/Infrastructure/Prometheus.md +dankserver serverAppConfigs-docs/Infrastructure/authentik-postgres.md +dankserver serverAppConfigs-docs/Infrastructure/authentik-server.md +dankserver serverAppConfigs-docs/Infrastructure/authentik-worker.md +dankserver serverAppConfigs-docs/LLM/DFlash DSpark Speculative Decoding.md +dankserver serverAppConfigs-docs/LLM/Dankclaw Improvements.md +dankserver serverAppConfigs-docs/LLM/Dankclaw System Specs.md +dankserver serverAppConfigs-docs/LLM/Dankserver System Specs.md +dankserver serverAppConfigs-docs/LLM/Dual-R9700-Cooperation.md +dankserver serverAppConfigs-docs/LLM/FastFlowLM NPU Inference.md +dankserver serverAppConfigs-docs/LLM/FreeToken Builds.md +dankserver serverAppConfigs-docs/LLM/GGUF Compatibility.md +dankserver serverAppConfigs-docs/LLM/Heterogeneous Split Triage.md +dankserver serverAppConfigs-docs/LLM/Hybrid CPU GPU Processing Dankserver.md +dankserver serverAppConfigs-docs/LLM/LLM Bench Windows and Measurement.md +dankserver serverAppConfigs-docs/LLM/LLM Performance Diagnosis.md +dankserver serverAppConfigs-docs/LLM/Llama Cpp Multi-Device Splits.md +dankserver serverAppConfigs-docs/LLM/Llama Cpp Serving Sizing.md +dankserver serverAppConfigs-docs/LLM/Model Swap and Rollback.md +dankserver serverAppConfigs-docs/LLM/PCIe Link and Atomics.md +dankserver serverAppConfigs-docs/LLM/Pipeline Parallelism/Ethernet Bandwidth Math.md +dankserver serverAppConfigs-docs/LLM/Pipeline Parallelism/Expected Performance.md +dankserver serverAppConfigs-docs/LLM/Pipeline Parallelism/Mixed-Vendor Experiments.md +dankserver serverAppConfigs-docs/LLM/Pipeline Parallelism/Qwen3.6 27B Spec Methods.md +dankserver serverAppConfigs-docs/LLM/Pipeline Parallelism/Qwen3.8 PP Deployment.md +dankserver serverAppConfigs-docs/LLM/Pipeline Parallelism/Sample Script Set.md +dankserver serverAppConfigs-docs/LLM/Pipeline Parallelism/index.md +dankserver serverAppConfigs-docs/LLM/Pipeline Parallelism/llama.cpp RPC.md +dankserver serverAppConfigs-docs/LLM/Pipeline Parallelism/vLLM Pipeline Parallelism.md +dankserver serverAppConfigs-docs/LLM/Quantization Production.md +dankserver serverAppConfigs-docs/LLM/Quantization by Hardware Architecture.md +dankserver serverAppConfigs-docs/LLM/RAM CPU Offloading in VLLM.md +dankserver serverAppConfigs-docs/LLM/RDNA4 Container Serving.md +dankserver serverAppConfigs-docs/LLM/RDNA4 MoE Expert Offload VRAM Budget.md +dankserver serverAppConfigs-docs/LLM/Running Qwen3.6 27B FP8 on Dankclaw.md +dankserver serverAppConfigs-docs/LLM/Running Qwen3.6 35B A3B FP8 on Dankserver.md +dankserver serverAppConfigs-docs/LLM/Shared Memory Architecture Dankclaw.md +dankserver serverAppConfigs-docs/LLM/Tcclaviger Fork Ops.md +dankserver serverAppConfigs-docs/LLM/VLLM Index.md +dankserver serverAppConfigs-docs/LLM/VLLM Overview.md +dankserver serverAppConfigs-docs/LLM/agentionai-fast-imatrix.md +dankserver serverAppConfigs-docs/LLM/ciru-activefpx-promptforge.md +dankserver serverAppConfigs-docs/LLM/dankclaw-iommu-translation-off.md +dankserver serverAppConfigs-docs/LLM/dankserver-frontier-moe-feasibility.md +dankserver serverAppConfigs-docs/LLM/dankserver-qwen38-fp8-expert-offload-tuning.md +dankserver serverAppConfigs-docs/LLM/dankserver-vram-growth.md +dankserver serverAppConfigs-docs/LLM/dealign-ornith-crack.md +dankserver serverAppConfigs-docs/LLM/deployment.md +dankserver serverAppConfigs-docs/LLM/dflash2-large-vulkan.md +dankserver serverAppConfigs-docs/LLM/eemin-rocmi4-mtp.md +dankserver serverAppConfigs-docs/LLM/frank-archon-freetoken-pr587.md +dankserver serverAppConfigs-docs/LLM/frank-archon-glm53-flash-w4a16.md +dankserver serverAppConfigs-docs/LLM/frank-archon-llamacpp-qwen38-flash-next-q8.md +dankserver serverAppConfigs-docs/LLM/frank-archon-lucebox-ds4.md +dankserver serverAppConfigs-docs/LLM/frank-archon-r9v-radiance-qwen38-q8.md +dankserver serverAppConfigs-docs/LLM/frank-archon-r9v-radiance-qwen38-uncensored.md +dankserver serverAppConfigs-docs/LLM/frank-archon-radiance-qwen38-native-fp8.md +dankserver serverAppConfigs-docs/LLM/frank-archon-radiance-swift15-w4dn8.md +dankserver serverAppConfigs-docs/LLM/frank-dragoon-benchmark.md +dankserver serverAppConfigs-docs/LLM/frank-dragoon-ciru-orca.md +dankserver serverAppConfigs-docs/LLM/frank-dragoon-flash-server-v2-swift15.md +dankserver serverAppConfigs-docs/LLM/frank-dragoon-gufo-qwen38-27b.md +dankserver serverAppConfigs-docs/LLM/frank-dragoon-halogen27b-dflash2.md +dankserver serverAppConfigs-docs/LLM/frank-dragoon-ik-uncensored.md +dankserver serverAppConfigs-docs/LLM/frank-dragoon-kairic-edge-qwen38-27b.md +dankserver serverAppConfigs-docs/LLM/frank-dragoon-rocmfpx-q6agent-uncensored-mtp.md +dankserver serverAppConfigs-docs/LLM/frank-dragoon-rocmfpx-q8agent-mtp.md +dankserver serverAppConfigs-docs/LLM/frank-dragoon-unsloth-qwen38-27b-q8xl.md +dankserver serverAppConfigs-docs/LLM/frank-dragoon-unsloth-qwen38-flash-next.md +dankserver serverAppConfigs-docs/LLM/frank-dragoon-vllm-mimo-v26-bf16.md +dankserver serverAppConfigs-docs/LLM/frank-medium-batch-ubatch.md +dankserver serverAppConfigs-docs/LLM/frank-medium-llamacpp-dflash2.md +dankserver serverAppConfigs-docs/LLM/frank-medium-model-swap.md +dankserver serverAppConfigs-docs/LLM/frank-medium-radiance-mxfp4.md +dankserver serverAppConfigs-docs/LLM/frank-medium-rocmfpx.md +dankserver serverAppConfigs-docs/LLM/frank-npu.md +dankserver serverAppConfigs-docs/LLM/frank-redo-2026-08-17.md +dankserver serverAppConfigs-docs/LLM/frank-zealot-benchmark.md +dankserver serverAppConfigs-docs/LLM/frank-zealot-gemma4-12b.md +dankserver serverAppConfigs-docs/LLM/frank-zealot-llama-gemma4-q4km-200704.md +dankserver serverAppConfigs-docs/LLM/frank-zealot-llama-mimo-q5km.md +dankserver serverAppConfigs-docs/LLM/frank-zealot-llama-mimo-q6k-2x200704.md +dankserver serverAppConfigs-docs/LLM/frank-zealot-llama-mimo-q6k.md +dankserver serverAppConfigs-docs/LLM/frank-zealot-llama-qwen35-9b-q6k-2x200704.md +dankserver serverAppConfigs-docs/LLM/frank-zealot-llama-swift15-unc-iq3xxs-mtp-114688.md +dankserver serverAppConfigs-docs/LLM/frank-zealot-qwen38-gsq-rco.md +dankserver serverAppConfigs-docs/LLM/frank-zealot-sglang-mimo-v26.md +dankserver serverAppConfigs-docs/LLM/frank-zealot-vllm-gemma4.md +dankserver serverAppConfigs-docs/LLM/freetoken-benchmark-sweep.md +dankserver serverAppConfigs-docs/LLM/freetoken-frank-small.md +dankserver serverAppConfigs-docs/LLM/halogen-flash-server.md +dankserver serverAppConfigs-docs/LLM/ikllama-ornith-dankserver.md +dankserver serverAppConfigs-docs/LLM/index.md +dankserver serverAppConfigs-docs/LLM/kairic-edge.md +dankserver serverAppConfigs-docs/LLM/kingjones-q8agent-rocmfpx.md +dankserver serverAppConfigs-docs/LLM/kingjones-strix-mtp.md +dankserver serverAppConfigs-docs/LLM/kingjones-strix-rocmfp4.md +dankserver serverAppConfigs-docs/LLM/ling-3.0-flash-trial.md +dankserver serverAppConfigs-docs/LLM/ling-3.0-flash-vllm-plan.md +dankserver serverAppConfigs-docs/LLM/qwen3.8-spec-matrix.md +dankserver serverAppConfigs-docs/LLM/qwen3.8-strix-optimizations.md +dankserver serverAppConfigs-docs/LLM/swift15-campaign-worklog.md +dankserver serverAppConfigs-docs/LLM/ubatch-benchmark-large.md +dankserver serverAppConfigs-docs/LLM/vLLM Boot Forensics and Config Interrogation.md +dankserver serverAppConfigs-docs/LLM/vLLM Porting and Compile Triage.md +dankserver serverAppConfigs-docs/LLM/vllm_research_findings.md +dankserver serverAppConfigs-docs/LLM/x4-pair-experiment.md +dankserver serverAppConfigs-docs/Operations/Container Host Reachability.md +dankserver serverAppConfigs-docs/Operations/Fleet Audit and Posture.md +dankserver serverAppConfigs-docs/Operations/Fleet Ops Playbook.md +dankserver serverAppConfigs-docs/Operations/Payload and Disk Reclaim.md +dankserver serverAppConfigs-docs/Operations/Remote Job Runner.md +dankserver serverAppConfigs-docs/Operations/SSH and Remote Access.md +dankserver serverAppConfigs-docs/Operations/index.md +dankserver serverAppConfigs-docs/Research/AI Job Search and Resume Tools.md +dankserver serverAppConfigs-docs/Research/Backblaze Integration.md +dankserver serverAppConfigs-docs/Research/ComfyUI ROCm and Vulkan.md +dankserver serverAppConfigs-docs/Research/Lemonade and sd.cpp.md +dankserver serverAppConfigs-docs/Research/Llama Cpp Code Diff.md +dankserver serverAppConfigs-docs/Research/Llama Cpp Runtimes.md +dankserver serverAppConfigs-docs/Research/Local TTS and Voice Cloning.md +dankserver serverAppConfigs-docs/Research/Most Popular Self-Hosted AI Apps.md +dankserver serverAppConfigs-docs/Research/PICO-8 Asset Standards.md +dankserver serverAppConfigs-docs/Research/SGLang.md +dankserver serverAppConfigs-docs/Research/Service Research Brief.md +dankserver serverAppConfigs-docs/Research/index.md +dankserver serverAppConfigs-docs/Research/popular-self-hosted-ai-apps.html +dankserver serverAppConfigs-docs/index.md +dankserver serverAppConfigs-docs/log.md +dankserver shared/alloy/config.alloy +dankserver shared/grafana/provisioning/dashboards/llm-fleet.json +dankserver shared/grafana/provisioning/dashboards/provider.yml +dankserver shared/grafana/provisioning/datasources/prometheus.yml +dankserver shared/homepage/admin.html +dankserver shared/homepage/app.js +dankserver shared/homepage/dankserver.services.json +dankserver shared/homepage/docs.html +dankserver shared/homepage/img/apps/app-calibre.svg +dankserver shared/homepage/img/apps/app-folder.svg +dankserver shared/homepage/img/apps/app-gameboy.svg +dankserver shared/homepage/img/apps/app-wall.svg +dankserver shared/homepage/img/apps/caddy.svg +dankserver shared/homepage/img/apps/gitea.svg +dankserver shared/homepage/img/apps/grafana.svg +dankserver shared/homepage/img/apps/jellyfin.svg +dankserver shared/homepage/img/apps/mariadb.svg +dankserver shared/homepage/img/apps/mono-am.svg +dankserver shared/homepage/img/apps/mono-ay.svg +dankserver shared/homepage/img/apps/mono-cp.svg +dankserver shared/homepage/img/apps/mono-cu.svg +dankserver shared/homepage/img/apps/mono-cw.svg +dankserver shared/homepage/img/apps/mono-gt.svg +dankserver shared/homepage/img/apps/mono-hd.svg +dankserver shared/homepage/img/apps/mono-hm.svg +dankserver shared/homepage/img/apps/mono-ld.svg +dankserver shared/homepage/img/apps/mono-lk.svg +dankserver shared/homepage/img/apps/mono-mr.svg +dankserver shared/homepage/img/apps/mono-ne.svg +dankserver shared/homepage/img/apps/mono-ow.svg +dankserver shared/homepage/img/apps/mono-pw.svg +dankserver shared/homepage/img/apps/mono-ra.svg +dankserver shared/homepage/img/apps/mono-rm.svg +dankserver shared/homepage/img/apps/mono-sr.svg +dankserver shared/homepage/img/apps/mono-tt.svg +dankserver shared/homepage/img/apps/mono-vk.svg +dankserver shared/homepage/img/apps/openwebui.png +dankserver shared/homepage/img/apps/pihole.svg +dankserver shared/homepage/img/apps/plex.svg +dankserver shared/homepage/img/apps/postgresql.svg +dankserver shared/homepage/img/apps/prometheus.svg +dankserver shared/homepage/img/apps/qbittorrent.svg +dankserver shared/homepage/img/apps/radarr.svg +dankserver shared/homepage/img/apps/searxng.svg +dankserver shared/homepage/img/apps/seerr.svg +dankserver shared/homepage/img/apps/sonarr.svg +dankserver shared/homepage/img/openwebui-api-keys.png +dankserver shared/homepage/img/openwebui-chat.png +dankserver shared/homepage/img/openwebui-model-picker.png +dankserver shared/homepage/img/unit-archon.jpg +dankserver shared/homepage/img/unit-dragoon.jpg +dankserver shared/homepage/img/unit-zealot.jpg +dankserver shared/homepage/index.html +dankserver shared/homepage/services.json +dankserver shared/homepage/sfx/archon-burn.ogg +dankserver shared/homepage/sfx/archon-power.ogg +dankserver shared/homepage/sfx/dragoon-awaiting.ogg +dankserver shared/homepage/sfx/dragoon-tagline.ogg +dankserver shared/homepage/sfx/zealot-combat.ogg +dankserver shared/homepage/sfx/zealot-honor.ogg +dankserver shared/homepage/style.css +dankserver shared/loki/loki-config.yaml +dankserver shared/prometheus/alertmanager.yml +dankserver shared/prometheus/prometheus.yml +dankserver shared/prometheus/rules/llm-fleet.yml +dankserver shared/searxng/limiter.toml +dankserver shared/searxng/settings.yml +dankserver tests/.AGENTTRASH/20261013-compose-retire/compose_validity.bats +dankserver tests/.AGENTTRASH/frank_medium_proxy.bats +dankserver tests/.AGENTTRASH/frank_medium_quadlet.bats +dankserver tests/anubis.bats +dankserver tests/authentik.bats +dankserver tests/autoresearch_measure.bats +dankserver tests/caddy_quadlet.bats +dankserver tests/caddyfile.bats +dankserver tests/changed_tests.bats +dankserver tests/comfyui_rocm.bats +dankserver tests/dankserver_quadlet.bats +dankserver tests/deploy_sandbox.bash +dankserver tests/deployment_flow.bats +dankserver tests/deployment_scoping.bats +dankserver tests/docs_completeness.bats +dankserver tests/e2e/.env.local.example +dankserver tests/e2e/lib/env.ts +dankserver tests/e2e/lib/login.ts +dankserver tests/e2e/package-lock.json +dankserver tests/e2e/package.json +dankserver tests/e2e/playwright.config.ts +dankserver tests/e2e/run.sh +dankserver tests/e2e/specs/00-auth.setup.ts +dankserver tests/e2e/specs/10-homepage.spec.ts +dankserver tests/e2e/specs/11-admin.spec.ts +dankserver tests/e2e/specs/12-crawl.spec.ts +dankserver tests/e2e/specs/13-sso.spec.ts +dankserver tests/e2e/specs/14-guard.spec.ts +dankserver tests/e2e/specs/15-unauth-fetch.spec.ts +dankserver tests/e2e/specs/16-marreta.spec.ts +dankserver tests/e2e/specs/17-hermes-vhost.spec.ts +dankserver tests/e2e/specs/18-openwebui-sso.spec.ts +dankserver tests/e2e/specs/19-frankbook.spec.ts +dankserver tests/e2e_scaffold.bats +dankserver tests/env_consistency.bats +dankserver tests/frank_archon_unit.bats +dankserver tests/frank_dragoon_unit.bats +dankserver tests/frank_zealot_unit.bats +dankserver tests/frankbook_quadlet.bats +dankserver tests/helpers.bash +dankserver tests/hermes_webui.bats +dankserver tests/homepage_caddy.bats +dankserver tests/homepage_checker.bats +dankserver tests/homepage_config.bats +dankserver tests/homepage_deploy.bats +dankserver tests/homepage_pages.bats +dankserver tests/llm_deploy_gates.bats +dankserver tests/llm_gates.bats +dankserver tests/llm_secrets.bats +dankserver tests/llm_units.bats +dankserver tests/marreta_quadlet.bats +dankserver tests/mount_entropy.bats +dankserver tests/observability_quadlet.bats +dankserver tests/open_webui_quadlet.bats +dankserver tests/pihole.bats +dankserver tests/run.sh +dankserver tests/scripts_syntax.bats +dankserver tests/searxng_limiter.bats +dankserver tests/searxng_quadlet.bats +dankserver tests/seerr_quadlet.bats +dankserver tests/stubs/curl +dankserver tests/stubs/podman +dankserver tests/stubs/sleep +dankserver tests/stubs/ss +dankserver tests/stubs/sudo +dankserver tests/stubs/systemctl +dankserver dankclaw/searxng/quadlet/gluetun.container+shared/searxng/settings.yml +dankserver dankclaw/Caddyfile+dankclaw/searxng/quadlet/searxng.container+dankclaw/observability/quadlet/prometheus.container +dankserver scripts/frank-dragoon.env+dankclaw/pihole/addn-hosts +dankserver dankserver/compose.yaml+dankserver/romm/quadlet/romm.container romm-mariadb,romm-redis,romm +dankserver dankserver/compose.yaml+scripts/frank-archon.env+dankserver/observability/quadlet/node-exporter.container node-exporter,frank-archon +dankserver dankclaw/observability/quadlet/alertmanager.container+shared/prometheus/alertmanager.yml+scripts/signal-relay.py +dankserver frankbook-pro/gluetun/quadlet/gluetun.container+frankbook-pro/sonarr/quadlet/sonarr.container +dankserver dankclaw/searxng/quadlet/gluetun.container+shared/searxng/settings.yml +dankserver dankclaw/Caddyfile+dankclaw/searxng/quadlet/searxng.container+dankclaw/observability/quadlet/prometheus.container +dankserver scripts/frank-dragoon.env+dankclaw/pihole/addn-hosts +dankserver dankserver/compose.yaml+dankserver/romm/quadlet/romm.container romm-mariadb,romm-redis,romm +dankserver dankserver/compose.yaml+scripts/frank-archon.env+dankserver/observability/quadlet/node-exporter.container node-exporter,frank-archon +dankserver dankclaw/observability/quadlet/alertmanager.container+shared/prometheus/alertmanager.yml+scripts/signal-relay.py +dankserver frankbook-pro/gluetun/quadlet/gluetun.container+frankbook-pro/sonarr/quadlet/sonarr.container +dankserver dankclaw/searxng/quadlet/gluetun.container+shared/searxng/settings.yml +dankserver dankclaw/Caddyfile+dankclaw/searxng/quadlet/searxng.container+dankclaw/observability/quadlet/prometheus.container +dankserver scripts/frank-dragoon.env+dankclaw/pihole/addn-hosts +dankserver dankserver/compose.yaml+dankserver/romm/quadlet/romm.container romm-mariadb,romm-redis,romm +dankserver dankserver/compose.yaml+scripts/frank-archon.env+dankserver/observability/quadlet/node-exporter.container node-exporter,frank-archon +dankserver dankclaw/observability/quadlet/alertmanager.container+shared/prometheus/alertmanager.yml+scripts/signal-relay.py +dankserver frankbook-pro/gluetun/quadlet/gluetun.container+frankbook-pro/sonarr/quadlet/sonarr.container +frankbook-pro .AGENTTRASH/frank-default-small.env.llama.cpp +frankbook-pro .AGENTTRASH/frank-default-small.service.llama.cpp +frankbook-pro .AGENTTRASH/frank-dragoon.env.llama.cpp +frankbook-pro .AGENTTRASH/frank-dragoon.service.llama.cpp +frankbook-pro .AGENTTRASH/frank-npu.env.flm-npu +frankbook-pro .AGENTTRASH/frank-npu.service.flm-npu +frankbook-pro .AGENTTRASH/serve-frank-dragoon.sh.llama.cpp +frankbook-pro .AGENTTRASH/serve-frank-npu.sh.flm-npu +frankbook-pro .AGENTTRASH/serve-frank-small.sh.llama.cpp +frankbook-pro .githooks/pre-commit +frankbook-pro .gitignore +frankbook-pro AGENTS.md +frankbook-pro README.md +frankbook-pro autoresearch.sh +frankbook-pro dankclaw/.env.example +frankbook-pro dankclaw/Caddyfile +frankbook-pro dankclaw/Containerfile.comfyui +frankbook-pro dankclaw/alloy/Containerfile +frankbook-pro dankclaw/anubis/policy.json +frankbook-pro dankclaw/anubis/quadlet/anubis.container +frankbook-pro dankclaw/apply_qwen21_patch.py +frankbook-pro dankclaw/authentik/quadlet/authentik-outpost-ldap.container +frankbook-pro dankclaw/authentik/quadlet/authentik-outpost.container +frankbook-pro dankclaw/authentik/quadlet/authentik-postgres.container +frankbook-pro dankclaw/authentik/quadlet/authentik-server.container +frankbook-pro dankclaw/authentik/quadlet/authentik-worker.container +frankbook-pro dankclaw/caddy/Containerfile +frankbook-pro dankclaw/caddy/env.example +frankbook-pro dankclaw/caddy/quadlet/caddy.container +frankbook-pro dankclaw/comfyui-start.sh +frankbook-pro dankclaw/comfyui/comfyui-healthcheck.py +frankbook-pro dankclaw/comfyui/quadlet/comfyui.container +frankbook-pro dankclaw/comfyui/workflows/qwen-image-2.1-t2i-art.json +frankbook-pro dankclaw/comfyui/workflows/qwen-image-2.1-t2i-legible-text.json +frankbook-pro dankclaw/gitea/.env.example +frankbook-pro dankclaw/gitea/quadlet/gitea-postgres.container +frankbook-pro dankclaw/gitea/quadlet/gitea-server.container +frankbook-pro dankclaw/gitea/quadlet/gitea.network +frankbook-pro dankclaw/hermes/config.yaml.example +frankbook-pro dankclaw/hermes/quadlet/chrome-devtools-protocol.container +frankbook-pro dankclaw/hermes/quadlet/hermes-dashboard.container +frankbook-pro dankclaw/hermes/quadlet/hermes-webui.container +frankbook-pro dankclaw/hermes/quadlet/hermes.container +frankbook-pro dankclaw/loki/Containerfile +frankbook-pro dankclaw/marreta/quadlet/marreta-browser.container +frankbook-pro dankclaw/marreta/quadlet/marreta.container +frankbook-pro dankclaw/observability/quadlet/alertmanager.container +frankbook-pro dankclaw/observability/quadlet/alloy.container +frankbook-pro dankclaw/observability/quadlet/grafana.container +frankbook-pro dankclaw/observability/quadlet/loki.container +frankbook-pro dankclaw/observability/quadlet/node-exporter.container +frankbook-pro dankclaw/observability/quadlet/prometheus.container +frankbook-pro dankclaw/open-webui/quadlet/open-webui.container +frankbook-pro dankclaw/open-webui/quadlet/open-webui.network +frankbook-pro dankclaw/pihole/addn-hosts +frankbook-pro dankclaw/pihole/env.example +frankbook-pro dankclaw/pihole/quadlet/pihole.container +frankbook-pro dankclaw/pihole/sysctl/99-dnsmasq.conf +frankbook-pro dankclaw/searxng/quadlet/gluetun.container +frankbook-pro dankclaw/searxng/quadlet/searxng-valkey.container +frankbook-pro dankclaw/searxng/quadlet/searxng.container +frankbook-pro dankclaw/searxng/quadlet/searxng.network +frankbook-pro dankclaw/seerr/quadlet/seerr.container +frankbook-pro dankclaw/udev/99-comfyui-gpu-access.rules +frankbook-pro dankserver/.AGENTTRASH/20260916-vllm-radiance/affinity-proxy.py +frankbook-pro dankserver/.AGENTTRASH/20260916-vllm-radiance/frank-medium-a.container +frankbook-pro dankserver/.AGENTTRASH/20260916-vllm-radiance/frank-medium-b.container +frankbook-pro dankserver/.AGENTTRASH/20260916-vllm-radiance/frank-medium-proxy.container +frankbook-pro dankserver/.AGENTTRASH/20260916-vllm-radiance/routing.py +frankbook-pro dankserver/.AGENTTRASH/20261013-compose-retire/.env.example +frankbook-pro dankserver/.AGENTTRASH/20261013-compose-retire/compose.yaml +frankbook-pro dankserver/calibre-server/quadlet/calibre-server.container +frankbook-pro dankserver/calibre-web/quadlet/calibre-web.container +frankbook-pro dankserver/copyparty/quadlet/copyparty.container +frankbook-pro dankserver/jellyfin/quadlet/jellyfin.container +frankbook-pro dankserver/observability/quadlet/node-exporter.container +frankbook-pro dankserver/plex/quadlet/plex.container +frankbook-pro dankserver/romm/quadlet/romm-mariadb.container +frankbook-pro dankserver/romm/quadlet/romm-redis.container +frankbook-pro dankserver/romm/quadlet/romm.container +frankbook-pro dankserver/tautulli/quadlet/tautulli.container +frankbook-pro echo_prompt.txt +frankbook-pro echo_request.json +frankbook-pro frankbook-pro/gluetun/quadlet/gluetun.container gluetun,prowlarr,qbittorrent,radarr,sonarr,lidarr,readarr +frankbook-pro frankbook-pro/lidarr/quadlet/lidarr.container lidarr +frankbook-pro frankbook-pro/prowlarr/quadlet/prowlarr.container prowlarr +frankbook-pro frankbook-pro/qbittorrent/quadlet/qbittorrent.container qbittorrent +frankbook-pro frankbook-pro/radarr/quadlet/radarr.container radarr +frankbook-pro frankbook-pro/readarr/quadlet/readarr.container readarr +frankbook-pro frankbook-pro/sonarr/quadlet/sonarr.container sonarr +frankbook-pro old/download-frank-archon-fp8.sh +frankbook-pro old/download-frank-dragoon-dflash.sh +frankbook-pro old/download-frank-dragoon-mimo.sh +frankbook-pro old/download-frank-dragoon-qwen35.sh +frankbook-pro old/download-frank-zealot-sglang-int8.sh +frankbook-pro old/qwen3.8-flash-next-fp8.manifest +frankbook-pro old/scripts/analyze-spec-matrix.py +frankbook-pro old/scripts/bench-frank-dragoon.sh +frankbook-pro old/scripts/bench-high-ctx.sh +frankbook-pro old/scripts/bench-pipeline-64k.sh +frankbook-pro old/scripts/deploy.sh +frankbook-pro old/scripts/frank-default-large.env +frankbook-pro old/scripts/frank-default-large.ling.env +frankbook-pro old/scripts/frank-default-large.service +frankbook-pro old/scripts/frank-default-small.env +frankbook-pro old/scripts/frank-default-small.service +frankbook-pro old/scripts/frank-dragoon-ling.service +frankbook-pro old/scripts/frank-pp-worker.env +frankbook-pro old/scripts/frank-pp-worker.service +frankbook-pro old/scripts/llama-server.service +frankbook-pro old/scripts/pp-bench.py +frankbook-pro old/scripts/pp-test.sh +frankbook-pro old/scripts/prune-rpc-cache.sh +frankbook-pro old/scripts/serve-flm-small.sh +frankbook-pro old/scripts/serve-ling-flash.sh +frankbook-pro old/scripts/serve-ling3-vllm.sh +frankbook-pro old/scripts/serve-llama-frank-dragoon.sh +frankbook-pro old/scripts/serve-llama-frank-small.sh +frankbook-pro old/scripts/serve-pp-head.sh +frankbook-pro old/scripts/serve-pp-worker.sh +frankbook-pro old/scripts/serve-vllm-qwen36-dflash.sh +frankbook-pro old/scripts/spec-matrix-bench.sh +frankbook-pro old/scripts/test_dspark_stream.py +frankbook-pro old/scripts/validate-vllm.sh +frankbook-pro prose_prompt.txt +frankbook-pro prose_request.json +frankbook-pro report/doc-audit-2026-08-23.html +frankbook-pro report/homepage-grilling-2026-08-22.html +frankbook-pro scripts/.AGENTTRASH/20260920-llama-layersplit/bench-spec-ab.py.llamacpp-layersplit +frankbook-pro scripts/.AGENTTRASH/20260920-llama-layersplit/frank-medium.env.llamacpp-layersplit +frankbook-pro scripts/.AGENTTRASH/20260920-llama-layersplit/frank-medium.service.llamacpp-layersplit +frankbook-pro scripts/.AGENTTRASH/20260920-llama-layersplit/serve-frank-medium.sh.llamacpp-layersplit +frankbook-pro scripts/.AGENTTRASH/bench-decode-threads-batch.py.strix-rocmfp4 +frankbook-pro scripts/.AGENTTRASH/bench-ngram-mod.py.ik-llama.cpp +frankbook-pro scripts/.AGENTTRASH/bench-ngram-mod.sh.ik-llama.cpp +frankbook-pro scripts/.AGENTTRASH/bench-spec-decode.py.ik-llama.cpp +frankbook-pro scripts/.AGENTTRASH/bench-vllm-corpus.py +frankbook-pro scripts/.AGENTTRASH/bench-vllm-serve.sh +frankbook-pro scripts/.AGENTTRASH/chat-template.jinja.ik-llama-qwen-sharp +frankbook-pro scripts/.AGENTTRASH/download-q8agent.sh.rocmfpx +frankbook-pro scripts/.AGENTTRASH/frank-default-npu.env +frankbook-pro scripts/.AGENTTRASH/frank-default-npu.service +frankbook-pro scripts/.AGENTTRASH/frank-dragoon.env.ik-llama.cpp +frankbook-pro scripts/.AGENTTRASH/frank-dragoon.service.ik-llama.cpp +frankbook-pro scripts/.AGENTTRASH/frank-medium.env.rocmfpx +frankbook-pro scripts/.AGENTTRASH/frank-medium.service.rocmfpx +frankbook-pro scripts/.AGENTTRASH/serve-frank-default-npu.sh +frankbook-pro scripts/.AGENTTRASH/serve-frank-dragoon.sh.ik-llama.cpp +frankbook-pro scripts/.AGENTTRASH/serve-frank-medium.sh.rocmfpx +frankbook-pro scripts/apply-protected-cast.sh +frankbook-pro scripts/autoresearch-bench.py +frankbook-pro scripts/autoresearch-frank-archon.sh +frankbook-pro scripts/autoresearch/frank-archon-arms.sh +frankbook-pro scripts/autoresearch/frank-archon-measure.py +frankbook-pro scripts/autoresearch/frank-archon-window.sh +frankbook-pro scripts/bench-batch-ubatch.sh +frankbook-pro scripts/bench-conversation.py +frankbook-pro scripts/bench-decode.sh +frankbook-pro scripts/bench-llm-soak.py +frankbook-pro scripts/bench-probe.py +frankbook-pro scripts/bench-threads-batch-http.py +frankbook-pro scripts/bench-threads-batch.sh +frankbook-pro scripts/bench-turn-latency.py +frankbook-pro scripts/bench-ubatch.sh +frankbook-pro scripts/bench_tb_fixed.py +frankbook-pro scripts/build-frank-archon-rad.sh +frankbook-pro scripts/build-gufo.sh +frankbook-pro scripts/build-kairic-edge-gfx1151.sh +frankbook-pro scripts/build-llama-rdna4.sh +frankbook-pro scripts/build-rocmfpx.sh +frankbook-pro scripts/calib-push.py +frankbook-pro scripts/check-vault-links.py +frankbook-pro scripts/convert-swift15-w4dn8.sh +frankbook-pro scripts/dankserver-route-primary.sh +frankbook-pro scripts/dankstatus.sh +frankbook-pro scripts/deep-probe.py +frankbook-pro scripts/deploy-open-webui-quadlet.sh +frankbook-pro scripts/deploy-pihole-quadlet.sh +frankbook-pro scripts/deploy-plan.py +frankbook-pro scripts/deploy-remote.sh +frankbook-pro scripts/deploy-searxng-quadlet.sh +frankbook-pro scripts/deploy.sh +frankbook-pro scripts/derive-protected-experts.py +frankbook-pro scripts/download-frank-archon-swift15.sh +frankbook-pro scripts/download-frank-archon.sh +frankbook-pro scripts/download-frank-dragoon-flashnext.sh +frankbook-pro scripts/download-frank-dragoon-halogen.sh +frankbook-pro scripts/download-frank-dragoon-kingjones-uncensored.sh +frankbook-pro scripts/download-frank-dragoon-kingjones.sh +frankbook-pro scripts/download-frank-dragoon-orca.sh +frankbook-pro scripts/download-frank-dragoon-q4.sh +frankbook-pro scripts/download-frank-dragoon-swift.sh +frankbook-pro scripts/download-frank-dragoon-unsloth.sh +frankbook-pro scripts/download-frank-dragoon.sh +frankbook-pro scripts/download-frank-zealot.sh +frankbook-pro scripts/dump-gguf.py +frankbook-pro scripts/fleet-probes-swift15.sh +frankbook-pro scripts/frank-archon-expert-activity.py +frankbook-pro scripts/frank-archon.env +frankbook-pro scripts/frank-archon.service +frankbook-pro scripts/frank-dragoon.env +frankbook-pro scripts/frank-dragoon.service +frankbook-pro scripts/frank-zealot.env +frankbook-pro scripts/frank-zealot.service +frankbook-pro scripts/generate-prompts.py +frankbook-pro scripts/hf-fetch.sh +frankbook-pro scripts/hf-manifest.py +frankbook-pro scripts/hfget.sh +frankbook-pro scripts/homepage-status.py +frankbook-pro scripts/homepage-status.service +frankbook-pro scripts/homepage-status.timer +frankbook-pro scripts/kld-gate-swift15.sh +frankbook-pro scripts/kld-then-calib.sh +frankbook-pro scripts/lib/llm-gates.sh +frankbook-pro scripts/make-calib-corpus.py +frankbook-pro scripts/mount-entropy.sh +frankbook-pro scripts/patches/glm53-compile-01-model-decorator.patch +frankbook-pro scripts/patches/glm53-compile-02-trace-safe-probes.patch +frankbook-pro scripts/patches/glm53-compile-03-fullgraph-optout.patch +frankbook-pro scripts/patches/glm53-compile-04-fused-qkv-rmsnorm-optout.patch +frankbook-pro scripts/patches/glm53-compile-05-workspace-acquisition-opaque.patch +frankbook-pro scripts/patches/glm53-dflash-01-aux-hidden-states.patch +frankbook-pro scripts/patches/glm53-dflash-04-kv-groups-draft.patch +frankbook-pro scripts/patches/glm53-dflash-04b-kv-registry-dflash.patch +frankbook-pro scripts/patches/gpu_worker-kv-cap.patch +frankbook-pro scripts/patches/qwen4exp-qsa-f32-mask.patch +frankbook-pro scripts/probe-frank-archon-kv.py +frankbook-pro scripts/qwen-sharp-v22.4.0.jinja +frankbook-pro scripts/qwen38-froggeric-v22.5.jinja +frankbook-pro scripts/recipes/qwen4exp-swift15-w4dn8-i8-hc8m.recipe +frankbook-pro scripts/run-swift15-bf16.sh +frankbook-pro scripts/searxng-render-settings.py +frankbook-pro scripts/serve-frank-archon.sh +frankbook-pro scripts/serve-frank-dragoon.sh +frankbook-pro scripts/serve-frank-zealot.sh +frankbook-pro scripts/signal-relay.py +frankbook-pro scripts/signal-relay.service +frankbook-pro scripts/stack-qwen38-official-fp8.py +frankbook-pro scripts/test-changed.sh +frankbook-pro scripts/tmux-resurrect.sh +frankbook-pro scripts/verify-swift15-ple.py +frankbook-pro serverAppConfigs-docs/.obsidian/app.json +frankbook-pro serverAppConfigs-docs/Agent/Agent Docs and Edits.md +frankbook-pro serverAppConfigs-docs/Agent/Command Center Dotfiles.md +frankbook-pro serverAppConfigs-docs/Agent/OMP Harness.md +frankbook-pro serverAppConfigs-docs/Agent/index.md +frankbook-pro serverAppConfigs-docs/Architecture/Container Network Namespaces.md +frankbook-pro serverAppConfigs-docs/Architecture/Deploy State Triage.md +frankbook-pro serverAppConfigs-docs/Architecture/Deployment Flow.md +frankbook-pro serverAppConfigs-docs/Architecture/E2E Suite.md +frankbook-pro serverAppConfigs-docs/Architecture/Network Architecture.md +frankbook-pro serverAppConfigs-docs/Architecture/Quadlet Lifecycle.md +frankbook-pro serverAppConfigs-docs/Architecture/Stale Signal Audit.md +frankbook-pro serverAppConfigs-docs/Architecture/Storage Policy.md +frankbook-pro serverAppConfigs-docs/Architecture/Testing Policy.md +frankbook-pro serverAppConfigs-docs/Dankclaw/ComfyUI Qwen-Image Workflows.md +frankbook-pro serverAppConfigs-docs/Dankclaw/ComfyUI.md +frankbook-pro serverAppConfigs-docs/Dankclaw/Dankclaw Stack.md +frankbook-pro serverAppConfigs-docs/Dankclaw/Gitea.md +frankbook-pro serverAppConfigs-docs/Dankclaw/Gluetun.md +frankbook-pro serverAppConfigs-docs/Dankclaw/Hermes-Dashboard.md +frankbook-pro serverAppConfigs-docs/Dankclaw/Hermes-WebUI.md +frankbook-pro serverAppConfigs-docs/Dankclaw/Hermes.md +frankbook-pro serverAppConfigs-docs/Dankclaw/Marreta.md +frankbook-pro serverAppConfigs-docs/Dankclaw/OpenWebUI.md +frankbook-pro serverAppConfigs-docs/Dankclaw/Postgres.md +frankbook-pro serverAppConfigs-docs/Dankclaw/SearXNG Providers.md +frankbook-pro serverAppConfigs-docs/Dankclaw/SearXNG.md +frankbook-pro serverAppConfigs-docs/Dankclaw/Seerr.md +frankbook-pro serverAppConfigs-docs/Dankclaw/Swap Configuration.md +frankbook-pro serverAppConfigs-docs/Dankclaw/Valkey.md +frankbook-pro serverAppConfigs-docs/Dankserver/Calibre-Web.md +frankbook-pro serverAppConfigs-docs/Dankserver/Calibre.md +frankbook-pro serverAppConfigs-docs/Dankserver/Copyparty.md +frankbook-pro serverAppConfigs-docs/Dankserver/Dankserver Stack.md +frankbook-pro serverAppConfigs-docs/Dankserver/Entropy Volume.md +frankbook-pro serverAppConfigs-docs/Dankserver/Jellyfin.md +frankbook-pro serverAppConfigs-docs/Dankserver/Plex.md +frankbook-pro serverAppConfigs-docs/Dankserver/RomM-Scraping.md +frankbook-pro serverAppConfigs-docs/Dankserver/RomM.md +frankbook-pro serverAppConfigs-docs/Dankserver/Tautulli.md +frankbook-pro serverAppConfigs-docs/Dankserver/mariadb.md +frankbook-pro serverAppConfigs-docs/Dankserver/redis.md +frankbook-pro serverAppConfigs-docs/Desktop/Noctalia and Niri.md +frankbook-pro serverAppConfigs-docs/FrankBook-Pro/ARR-Stack.md +frankbook-pro serverAppConfigs-docs/FrankBook-Pro/index.md +frankbook-pro serverAppConfigs-docs/FrankBook-Pro/lidarr.md +frankbook-pro serverAppConfigs-docs/FrankBook-Pro/prowlarr.md +frankbook-pro serverAppConfigs-docs/FrankBook-Pro/qbittorrent.md +frankbook-pro serverAppConfigs-docs/FrankBook-Pro/radarr.md +frankbook-pro serverAppConfigs-docs/FrankBook-Pro/readarr.md +frankbook-pro serverAppConfigs-docs/FrankBook-Pro/sonarr.md +frankbook-pro serverAppConfigs-docs/Infrastructure/Alertmanager.md +frankbook-pro serverAppConfigs-docs/Infrastructure/Alloy.md +frankbook-pro serverAppConfigs-docs/Infrastructure/Anubis.md +frankbook-pro serverAppConfigs-docs/Infrastructure/Authentik.md +frankbook-pro serverAppConfigs-docs/Infrastructure/Caddy.md +frankbook-pro serverAppConfigs-docs/Infrastructure/FrankBook-Pro.md +frankbook-pro serverAppConfigs-docs/Infrastructure/Grafana.md +frankbook-pro serverAppConfigs-docs/Infrastructure/Homepage Design Grilling.md +frankbook-pro serverAppConfigs-docs/Infrastructure/Homepage Status Probes.md +frankbook-pro serverAppConfigs-docs/Infrastructure/Homepage.md +frankbook-pro serverAppConfigs-docs/Infrastructure/LAN-DNS.md +frankbook-pro serverAppConfigs-docs/Infrastructure/Loki.md +frankbook-pro serverAppConfigs-docs/Infrastructure/Node-Exporter.md +frankbook-pro serverAppConfigs-docs/Infrastructure/Observability Provisioning.md +frankbook-pro serverAppConfigs-docs/Infrastructure/Observability.md +frankbook-pro serverAppConfigs-docs/Infrastructure/Pi-hole.md +frankbook-pro serverAppConfigs-docs/Infrastructure/Prometheus.md +frankbook-pro serverAppConfigs-docs/Infrastructure/authentik-postgres.md +frankbook-pro serverAppConfigs-docs/Infrastructure/authentik-server.md +frankbook-pro serverAppConfigs-docs/Infrastructure/authentik-worker.md +frankbook-pro serverAppConfigs-docs/LLM/DFlash DSpark Speculative Decoding.md +frankbook-pro serverAppConfigs-docs/LLM/Dankclaw Improvements.md +frankbook-pro serverAppConfigs-docs/LLM/Dankclaw System Specs.md +frankbook-pro serverAppConfigs-docs/LLM/Dankserver System Specs.md +frankbook-pro serverAppConfigs-docs/LLM/Dual-R9700-Cooperation.md +frankbook-pro serverAppConfigs-docs/LLM/FastFlowLM NPU Inference.md +frankbook-pro serverAppConfigs-docs/LLM/FreeToken Builds.md +frankbook-pro serverAppConfigs-docs/LLM/GGUF Compatibility.md +frankbook-pro serverAppConfigs-docs/LLM/Heterogeneous Split Triage.md +frankbook-pro serverAppConfigs-docs/LLM/Hybrid CPU GPU Processing Dankserver.md +frankbook-pro serverAppConfigs-docs/LLM/LLM Bench Windows and Measurement.md +frankbook-pro serverAppConfigs-docs/LLM/LLM Performance Diagnosis.md +frankbook-pro serverAppConfigs-docs/LLM/Llama Cpp Multi-Device Splits.md +frankbook-pro serverAppConfigs-docs/LLM/Llama Cpp Serving Sizing.md +frankbook-pro serverAppConfigs-docs/LLM/Model Swap and Rollback.md +frankbook-pro serverAppConfigs-docs/LLM/PCIe Link and Atomics.md +frankbook-pro serverAppConfigs-docs/LLM/Pipeline Parallelism/Ethernet Bandwidth Math.md +frankbook-pro serverAppConfigs-docs/LLM/Pipeline Parallelism/Expected Performance.md +frankbook-pro serverAppConfigs-docs/LLM/Pipeline Parallelism/Mixed-Vendor Experiments.md +frankbook-pro serverAppConfigs-docs/LLM/Pipeline Parallelism/Qwen3.6 27B Spec Methods.md +frankbook-pro serverAppConfigs-docs/LLM/Pipeline Parallelism/Qwen3.8 PP Deployment.md +frankbook-pro serverAppConfigs-docs/LLM/Pipeline Parallelism/Sample Script Set.md +frankbook-pro serverAppConfigs-docs/LLM/Pipeline Parallelism/index.md +frankbook-pro serverAppConfigs-docs/LLM/Pipeline Parallelism/llama.cpp RPC.md +frankbook-pro serverAppConfigs-docs/LLM/Pipeline Parallelism/vLLM Pipeline Parallelism.md +frankbook-pro serverAppConfigs-docs/LLM/Quantization Production.md +frankbook-pro serverAppConfigs-docs/LLM/Quantization by Hardware Architecture.md +frankbook-pro serverAppConfigs-docs/LLM/RAM CPU Offloading in VLLM.md +frankbook-pro serverAppConfigs-docs/LLM/RDNA4 Container Serving.md +frankbook-pro serverAppConfigs-docs/LLM/RDNA4 MoE Expert Offload VRAM Budget.md +frankbook-pro serverAppConfigs-docs/LLM/Running Qwen3.6 27B FP8 on Dankclaw.md +frankbook-pro serverAppConfigs-docs/LLM/Running Qwen3.6 35B A3B FP8 on Dankserver.md +frankbook-pro serverAppConfigs-docs/LLM/Shared Memory Architecture Dankclaw.md +frankbook-pro serverAppConfigs-docs/LLM/Tcclaviger Fork Ops.md +frankbook-pro serverAppConfigs-docs/LLM/VLLM Index.md +frankbook-pro serverAppConfigs-docs/LLM/VLLM Overview.md +frankbook-pro serverAppConfigs-docs/LLM/agentionai-fast-imatrix.md +frankbook-pro serverAppConfigs-docs/LLM/ciru-activefpx-promptforge.md +frankbook-pro serverAppConfigs-docs/LLM/dankclaw-iommu-translation-off.md +frankbook-pro serverAppConfigs-docs/LLM/dankserver-frontier-moe-feasibility.md +frankbook-pro serverAppConfigs-docs/LLM/dankserver-qwen38-fp8-expert-offload-tuning.md +frankbook-pro serverAppConfigs-docs/LLM/dankserver-vram-growth.md +frankbook-pro serverAppConfigs-docs/LLM/dealign-ornith-crack.md +frankbook-pro serverAppConfigs-docs/LLM/deployment.md +frankbook-pro serverAppConfigs-docs/LLM/dflash2-large-vulkan.md +frankbook-pro serverAppConfigs-docs/LLM/eemin-rocmi4-mtp.md +frankbook-pro serverAppConfigs-docs/LLM/frank-archon-freetoken-pr587.md +frankbook-pro serverAppConfigs-docs/LLM/frank-archon-glm53-flash-w4a16.md +frankbook-pro serverAppConfigs-docs/LLM/frank-archon-llamacpp-qwen38-flash-next-q8.md +frankbook-pro serverAppConfigs-docs/LLM/frank-archon-lucebox-ds4.md +frankbook-pro serverAppConfigs-docs/LLM/frank-archon-r9v-radiance-qwen38-q8.md +frankbook-pro serverAppConfigs-docs/LLM/frank-archon-r9v-radiance-qwen38-uncensored.md +frankbook-pro serverAppConfigs-docs/LLM/frank-archon-radiance-qwen38-native-fp8.md +frankbook-pro serverAppConfigs-docs/LLM/frank-archon-radiance-swift15-w4dn8.md +frankbook-pro serverAppConfigs-docs/LLM/frank-dragoon-benchmark.md +frankbook-pro serverAppConfigs-docs/LLM/frank-dragoon-ciru-orca.md +frankbook-pro serverAppConfigs-docs/LLM/frank-dragoon-flash-server-v2-swift15.md +frankbook-pro serverAppConfigs-docs/LLM/frank-dragoon-gufo-qwen38-27b.md +frankbook-pro serverAppConfigs-docs/LLM/frank-dragoon-halogen27b-dflash2.md +frankbook-pro serverAppConfigs-docs/LLM/frank-dragoon-ik-uncensored.md +frankbook-pro serverAppConfigs-docs/LLM/frank-dragoon-kairic-edge-qwen38-27b.md +frankbook-pro serverAppConfigs-docs/LLM/frank-dragoon-rocmfpx-q6agent-uncensored-mtp.md +frankbook-pro serverAppConfigs-docs/LLM/frank-dragoon-rocmfpx-q8agent-mtp.md +frankbook-pro serverAppConfigs-docs/LLM/frank-dragoon-unsloth-qwen38-27b-q8xl.md +frankbook-pro serverAppConfigs-docs/LLM/frank-dragoon-unsloth-qwen38-flash-next.md +frankbook-pro serverAppConfigs-docs/LLM/frank-dragoon-vllm-mimo-v26-bf16.md +frankbook-pro serverAppConfigs-docs/LLM/frank-medium-batch-ubatch.md +frankbook-pro serverAppConfigs-docs/LLM/frank-medium-llamacpp-dflash2.md +frankbook-pro serverAppConfigs-docs/LLM/frank-medium-model-swap.md +frankbook-pro serverAppConfigs-docs/LLM/frank-medium-radiance-mxfp4.md +frankbook-pro serverAppConfigs-docs/LLM/frank-medium-rocmfpx.md +frankbook-pro serverAppConfigs-docs/LLM/frank-npu.md +frankbook-pro serverAppConfigs-docs/LLM/frank-redo-2026-08-17.md +frankbook-pro serverAppConfigs-docs/LLM/frank-zealot-benchmark.md +frankbook-pro serverAppConfigs-docs/LLM/frank-zealot-gemma4-12b.md +frankbook-pro serverAppConfigs-docs/LLM/frank-zealot-llama-gemma4-q4km-200704.md +frankbook-pro serverAppConfigs-docs/LLM/frank-zealot-llama-mimo-q5km.md +frankbook-pro serverAppConfigs-docs/LLM/frank-zealot-llama-mimo-q6k-2x200704.md +frankbook-pro serverAppConfigs-docs/LLM/frank-zealot-llama-mimo-q6k.md +frankbook-pro serverAppConfigs-docs/LLM/frank-zealot-llama-qwen35-9b-q6k-2x200704.md +frankbook-pro serverAppConfigs-docs/LLM/frank-zealot-llama-swift15-unc-iq3xxs-mtp-114688.md +frankbook-pro serverAppConfigs-docs/LLM/frank-zealot-qwen38-gsq-rco.md +frankbook-pro serverAppConfigs-docs/LLM/frank-zealot-sglang-mimo-v26.md +frankbook-pro serverAppConfigs-docs/LLM/frank-zealot-vllm-gemma4.md +frankbook-pro serverAppConfigs-docs/LLM/freetoken-benchmark-sweep.md +frankbook-pro serverAppConfigs-docs/LLM/freetoken-frank-small.md +frankbook-pro serverAppConfigs-docs/LLM/halogen-flash-server.md +frankbook-pro serverAppConfigs-docs/LLM/ikllama-ornith-dankserver.md +frankbook-pro serverAppConfigs-docs/LLM/index.md +frankbook-pro serverAppConfigs-docs/LLM/kairic-edge.md +frankbook-pro serverAppConfigs-docs/LLM/kingjones-q8agent-rocmfpx.md +frankbook-pro serverAppConfigs-docs/LLM/kingjones-strix-mtp.md +frankbook-pro serverAppConfigs-docs/LLM/kingjones-strix-rocmfp4.md +frankbook-pro serverAppConfigs-docs/LLM/ling-3.0-flash-trial.md +frankbook-pro serverAppConfigs-docs/LLM/ling-3.0-flash-vllm-plan.md +frankbook-pro serverAppConfigs-docs/LLM/qwen3.8-spec-matrix.md +frankbook-pro serverAppConfigs-docs/LLM/qwen3.8-strix-optimizations.md +frankbook-pro serverAppConfigs-docs/LLM/swift15-campaign-worklog.md +frankbook-pro serverAppConfigs-docs/LLM/ubatch-benchmark-large.md +frankbook-pro serverAppConfigs-docs/LLM/vLLM Boot Forensics and Config Interrogation.md +frankbook-pro serverAppConfigs-docs/LLM/vLLM Porting and Compile Triage.md +frankbook-pro serverAppConfigs-docs/LLM/vllm_research_findings.md +frankbook-pro serverAppConfigs-docs/LLM/x4-pair-experiment.md +frankbook-pro serverAppConfigs-docs/Operations/Container Host Reachability.md +frankbook-pro serverAppConfigs-docs/Operations/Fleet Audit and Posture.md +frankbook-pro serverAppConfigs-docs/Operations/Fleet Ops Playbook.md +frankbook-pro serverAppConfigs-docs/Operations/Payload and Disk Reclaim.md +frankbook-pro serverAppConfigs-docs/Operations/Remote Job Runner.md +frankbook-pro serverAppConfigs-docs/Operations/SSH and Remote Access.md +frankbook-pro serverAppConfigs-docs/Operations/index.md +frankbook-pro serverAppConfigs-docs/Research/AI Job Search and Resume Tools.md +frankbook-pro serverAppConfigs-docs/Research/Backblaze Integration.md +frankbook-pro serverAppConfigs-docs/Research/ComfyUI ROCm and Vulkan.md +frankbook-pro serverAppConfigs-docs/Research/Lemonade and sd.cpp.md +frankbook-pro serverAppConfigs-docs/Research/Llama Cpp Code Diff.md +frankbook-pro serverAppConfigs-docs/Research/Llama Cpp Runtimes.md +frankbook-pro serverAppConfigs-docs/Research/Local TTS and Voice Cloning.md +frankbook-pro serverAppConfigs-docs/Research/Most Popular Self-Hosted AI Apps.md +frankbook-pro serverAppConfigs-docs/Research/PICO-8 Asset Standards.md +frankbook-pro serverAppConfigs-docs/Research/SGLang.md +frankbook-pro serverAppConfigs-docs/Research/Service Research Brief.md +frankbook-pro serverAppConfigs-docs/Research/index.md +frankbook-pro serverAppConfigs-docs/Research/popular-self-hosted-ai-apps.html +frankbook-pro serverAppConfigs-docs/index.md +frankbook-pro serverAppConfigs-docs/log.md +frankbook-pro shared/alloy/config.alloy +frankbook-pro shared/grafana/provisioning/dashboards/llm-fleet.json +frankbook-pro shared/grafana/provisioning/dashboards/provider.yml +frankbook-pro shared/grafana/provisioning/datasources/prometheus.yml +frankbook-pro shared/homepage/admin.html +frankbook-pro shared/homepage/app.js +frankbook-pro shared/homepage/dankserver.services.json +frankbook-pro shared/homepage/docs.html +frankbook-pro shared/homepage/img/apps/app-calibre.svg +frankbook-pro shared/homepage/img/apps/app-folder.svg +frankbook-pro shared/homepage/img/apps/app-gameboy.svg +frankbook-pro shared/homepage/img/apps/app-wall.svg +frankbook-pro shared/homepage/img/apps/caddy.svg +frankbook-pro shared/homepage/img/apps/gitea.svg +frankbook-pro shared/homepage/img/apps/grafana.svg +frankbook-pro shared/homepage/img/apps/jellyfin.svg +frankbook-pro shared/homepage/img/apps/mariadb.svg +frankbook-pro shared/homepage/img/apps/mono-am.svg +frankbook-pro shared/homepage/img/apps/mono-ay.svg +frankbook-pro shared/homepage/img/apps/mono-cp.svg +frankbook-pro shared/homepage/img/apps/mono-cu.svg +frankbook-pro shared/homepage/img/apps/mono-cw.svg +frankbook-pro shared/homepage/img/apps/mono-gt.svg +frankbook-pro shared/homepage/img/apps/mono-hd.svg +frankbook-pro shared/homepage/img/apps/mono-hm.svg +frankbook-pro shared/homepage/img/apps/mono-ld.svg +frankbook-pro shared/homepage/img/apps/mono-lk.svg +frankbook-pro shared/homepage/img/apps/mono-mr.svg +frankbook-pro shared/homepage/img/apps/mono-ne.svg +frankbook-pro shared/homepage/img/apps/mono-ow.svg +frankbook-pro shared/homepage/img/apps/mono-pw.svg +frankbook-pro shared/homepage/img/apps/mono-ra.svg +frankbook-pro shared/homepage/img/apps/mono-rm.svg +frankbook-pro shared/homepage/img/apps/mono-sr.svg +frankbook-pro shared/homepage/img/apps/mono-tt.svg +frankbook-pro shared/homepage/img/apps/mono-vk.svg +frankbook-pro shared/homepage/img/apps/openwebui.png +frankbook-pro shared/homepage/img/apps/pihole.svg +frankbook-pro shared/homepage/img/apps/plex.svg +frankbook-pro shared/homepage/img/apps/postgresql.svg +frankbook-pro shared/homepage/img/apps/prometheus.svg +frankbook-pro shared/homepage/img/apps/qbittorrent.svg +frankbook-pro shared/homepage/img/apps/radarr.svg +frankbook-pro shared/homepage/img/apps/searxng.svg +frankbook-pro shared/homepage/img/apps/seerr.svg +frankbook-pro shared/homepage/img/apps/sonarr.svg +frankbook-pro shared/homepage/img/openwebui-api-keys.png +frankbook-pro shared/homepage/img/openwebui-chat.png +frankbook-pro shared/homepage/img/openwebui-model-picker.png +frankbook-pro shared/homepage/img/unit-archon.jpg +frankbook-pro shared/homepage/img/unit-dragoon.jpg +frankbook-pro shared/homepage/img/unit-zealot.jpg +frankbook-pro shared/homepage/index.html +frankbook-pro shared/homepage/services.json +frankbook-pro shared/homepage/sfx/archon-burn.ogg +frankbook-pro shared/homepage/sfx/archon-power.ogg +frankbook-pro shared/homepage/sfx/dragoon-awaiting.ogg +frankbook-pro shared/homepage/sfx/dragoon-tagline.ogg +frankbook-pro shared/homepage/sfx/zealot-combat.ogg +frankbook-pro shared/homepage/sfx/zealot-honor.ogg +frankbook-pro shared/homepage/style.css +frankbook-pro shared/loki/loki-config.yaml +frankbook-pro shared/prometheus/alertmanager.yml +frankbook-pro shared/prometheus/prometheus.yml +frankbook-pro shared/prometheus/rules/llm-fleet.yml +frankbook-pro shared/searxng/limiter.toml +frankbook-pro shared/searxng/settings.yml +frankbook-pro tests/.AGENTTRASH/20261013-compose-retire/compose_validity.bats +frankbook-pro tests/.AGENTTRASH/frank_medium_proxy.bats +frankbook-pro tests/.AGENTTRASH/frank_medium_quadlet.bats +frankbook-pro tests/anubis.bats +frankbook-pro tests/authentik.bats +frankbook-pro tests/autoresearch_measure.bats +frankbook-pro tests/caddy_quadlet.bats +frankbook-pro tests/caddyfile.bats +frankbook-pro tests/changed_tests.bats +frankbook-pro tests/comfyui_rocm.bats +frankbook-pro tests/dankserver_quadlet.bats +frankbook-pro tests/deploy_sandbox.bash +frankbook-pro tests/deployment_flow.bats +frankbook-pro tests/deployment_scoping.bats +frankbook-pro tests/docs_completeness.bats +frankbook-pro tests/e2e/.env.local.example +frankbook-pro tests/e2e/lib/env.ts +frankbook-pro tests/e2e/lib/login.ts +frankbook-pro tests/e2e/package-lock.json +frankbook-pro tests/e2e/package.json +frankbook-pro tests/e2e/playwright.config.ts +frankbook-pro tests/e2e/run.sh +frankbook-pro tests/e2e/specs/00-auth.setup.ts +frankbook-pro tests/e2e/specs/10-homepage.spec.ts +frankbook-pro tests/e2e/specs/11-admin.spec.ts +frankbook-pro tests/e2e/specs/12-crawl.spec.ts +frankbook-pro tests/e2e/specs/13-sso.spec.ts +frankbook-pro tests/e2e/specs/14-guard.spec.ts +frankbook-pro tests/e2e/specs/15-unauth-fetch.spec.ts +frankbook-pro tests/e2e/specs/16-marreta.spec.ts +frankbook-pro tests/e2e/specs/17-hermes-vhost.spec.ts +frankbook-pro tests/e2e/specs/18-openwebui-sso.spec.ts +frankbook-pro tests/e2e/specs/19-frankbook.spec.ts +frankbook-pro tests/e2e_scaffold.bats +frankbook-pro tests/env_consistency.bats +frankbook-pro tests/frank_archon_unit.bats +frankbook-pro tests/frank_dragoon_unit.bats +frankbook-pro tests/frank_zealot_unit.bats +frankbook-pro tests/frankbook_quadlet.bats +frankbook-pro tests/helpers.bash +frankbook-pro tests/hermes_webui.bats +frankbook-pro tests/homepage_caddy.bats +frankbook-pro tests/homepage_checker.bats +frankbook-pro tests/homepage_config.bats +frankbook-pro tests/homepage_deploy.bats +frankbook-pro tests/homepage_pages.bats +frankbook-pro tests/llm_deploy_gates.bats +frankbook-pro tests/llm_gates.bats +frankbook-pro tests/llm_secrets.bats +frankbook-pro tests/llm_units.bats +frankbook-pro tests/marreta_quadlet.bats +frankbook-pro tests/mount_entropy.bats +frankbook-pro tests/observability_quadlet.bats +frankbook-pro tests/open_webui_quadlet.bats +frankbook-pro tests/pihole.bats +frankbook-pro tests/run.sh +frankbook-pro tests/scripts_syntax.bats +frankbook-pro tests/searxng_limiter.bats +frankbook-pro tests/searxng_quadlet.bats +frankbook-pro tests/seerr_quadlet.bats +frankbook-pro tests/stubs/curl +frankbook-pro tests/stubs/podman +frankbook-pro tests/stubs/sleep +frankbook-pro tests/stubs/ss +frankbook-pro tests/stubs/sudo +frankbook-pro tests/stubs/systemctl +frankbook-pro dankclaw/searxng/quadlet/gluetun.container+shared/searxng/settings.yml +frankbook-pro dankclaw/Caddyfile+dankclaw/searxng/quadlet/searxng.container+dankclaw/observability/quadlet/prometheus.container +frankbook-pro scripts/frank-dragoon.env+dankclaw/pihole/addn-hosts +frankbook-pro dankserver/compose.yaml+dankserver/romm/quadlet/romm.container +frankbook-pro dankserver/compose.yaml+scripts/frank-archon.env+dankserver/observability/quadlet/node-exporter.container +frankbook-pro dankclaw/observability/quadlet/alertmanager.container+shared/prometheus/alertmanager.yml+scripts/signal-relay.py +frankbook-pro frankbook-pro/gluetun/quadlet/gluetun.container+frankbook-pro/sonarr/quadlet/sonarr.container gluetun,prowlarr,qbittorrent,radarr,sonarr,lidarr,readarr +frankbook-pro dankclaw/searxng/quadlet/gluetun.container+shared/searxng/settings.yml +frankbook-pro dankclaw/Caddyfile+dankclaw/searxng/quadlet/searxng.container+dankclaw/observability/quadlet/prometheus.container +frankbook-pro scripts/frank-dragoon.env+dankclaw/pihole/addn-hosts +frankbook-pro dankserver/compose.yaml+dankserver/romm/quadlet/romm.container +frankbook-pro dankserver/compose.yaml+scripts/frank-archon.env+dankserver/observability/quadlet/node-exporter.container +frankbook-pro dankclaw/observability/quadlet/alertmanager.container+shared/prometheus/alertmanager.yml+scripts/signal-relay.py +frankbook-pro frankbook-pro/gluetun/quadlet/gluetun.container+frankbook-pro/sonarr/quadlet/sonarr.container gluetun,prowlarr,qbittorrent,radarr,sonarr,lidarr,readarr +frankbook-pro dankclaw/searxng/quadlet/gluetun.container+shared/searxng/settings.yml +frankbook-pro dankclaw/Caddyfile+dankclaw/searxng/quadlet/searxng.container+dankclaw/observability/quadlet/prometheus.container +frankbook-pro scripts/frank-dragoon.env+dankclaw/pihole/addn-hosts +frankbook-pro dankserver/compose.yaml+dankserver/romm/quadlet/romm.container +frankbook-pro dankserver/compose.yaml+scripts/frank-archon.env+dankserver/observability/quadlet/node-exporter.container +frankbook-pro dankclaw/observability/quadlet/alertmanager.container+shared/prometheus/alertmanager.yml+scripts/signal-relay.py +frankbook-pro frankbook-pro/gluetun/quadlet/gluetun.container+frankbook-pro/sonarr/quadlet/sonarr.container gluetun,prowlarr,qbittorrent,radarr,sonarr,lidarr,readarr diff --git a/tests/hermes_webui.bats b/tests/hermes_webui.bats index 9018302..640ea1c 100644 --- a/tests/hermes_webui.bats +++ b/tests/hermes_webui.bats @@ -101,7 +101,10 @@ webui_service_yaml() { } @test "remote deployer accepts and dispatches hermes-webui" { - grep -q 'dankclaw:hermes-webui' "$REMOTE_DEPLOY" + # Acceptance is the inventory (rank 3): the old literal allow-list in + # the remote runner is retired. + run python3 "$REPO_ROOT/scripts/service-map.py" allows --host dankclaw --service hermes-webui + [ "$status" -eq 0 ] # Dispatched to the unit handler, never to compose: deploy_hermes installs # the quadlets and retires compose-era containers. grep -q 'hermes|hermes-dashboard|hermes-webui) deploy_hermes' "$REMOTE_DEPLOY" @@ -111,5 +114,7 @@ webui_service_yaml() { } @test "top-level deployer accepts hermes-webui as a Dankclaw service" { - grep -q 'dankclaw:hermes-webui' "$DEPLOY" + # deploy.sh validates --service through the inventory's allows. + run python3 "$REPO_ROOT/scripts/service-map.py" allows --host dankclaw --service hermes-webui + [ "$status" -eq 0 ] } diff --git a/tests/observability_quadlet.bats b/tests/observability_quadlet.bats index ff2d312..78ad75f 100755 --- a/tests/observability_quadlet.bats +++ b/tests/observability_quadlet.bats @@ -433,11 +433,14 @@ assert any('ALERTS' in e for e in exprs), exprs grep -q -- '--web.listen-address=127.0.0.1:9100' "$unit" grep -q -- '--web.listen-address=192.168.0.235:9100' "$unit" ! grep -qE -- '--web\.listen-address=(0\.0\.0\.0|:9100|\[::\])' "$unit" - # One owner per container name: the compose service must be gone, or two - # definitions fight over the same container. - ! grep -qE '^\s+node-exporter:' "$REPO_ROOT/dankserver/compose.yaml" - # The deploy path has to know about it on this target. - grep -q 'dankserver:node-exporter' "$REPO_ROOT/scripts/deploy-remote.sh" + # One owner per container name: Dankserver has no compose file at all + # since the 2026-10-13 retirement (it lived beside diverged quadlet + # mounts for weeks — the two-owners trap by construction). + [ ! -e "$REPO_ROOT/dankserver/compose.yaml" ] + # The deploy path has to know about it on this target — acceptance + # is the inventory (rank 3), not a literal list in the runner. + run python3 "$REPO_ROOT/scripts/service-map.py" allows --host dankserver --service node-exporter + [ "$status" -eq 0 ] run python3 "$REPO_ROOT/scripts/deploy-plan.py" --target dankserver \ --paths dankserver/observability/quadlet/node-exporter.container [ "$status" -eq 0 ] diff --git a/tests/service_inventory.bats b/tests/service_inventory.bats new file mode 100755 index 0000000..0c3d52d --- /dev/null +++ b/tests/service_inventory.bats @@ -0,0 +1,125 @@ +#!/usr/bin/env bats +# Seam: the service inventory (rank 3). shared/services.yml is the one +# inventory of deployable services; scripts/service-map.py is its only +# reader, and every consumer (deploy-plan.py, deploy-remote.sh's +# allow-list, deploy.sh's --service gate, test-changed.sh's seam mapper, +# the Pi-hole hosts render, the Prometheus scrape facts, the homepage +# probes) goes through it. These tests pin the inventory's integrity, +# the planner's selection against a captured fixture, and the rendered +# artifacts' drift against the tracked copies. + +load helpers + +@test "inventory: schema, unit claims, path claims, compose ownership, and render drift are sound" { + run python3 "$REPO_ROOT/scripts/service-map.py" check + [ "$status" -eq 0 ] +} + +@test "inventory: the deployers' allow-list is the inventory" { + # Every service the inventory places on a host must pass "allows" for + # that host, and a name the inventory never places must be refused. + for host in dankclaw dankserver frankbook-pro; do + while IFS= read -r svc; do + [ -n "$svc" ] || continue + run python3 "$REPO_ROOT/scripts/service-map.py" allows --host "$host" --service "$svc" + [ "$status" -eq 0 ] || { echo "allows refused $host:$svc"; return 1; } + done < <(python3 "$REPO_ROOT/scripts/service-map.py" services --host "$host") + done + run python3 "$REPO_ROOT/scripts/service-map.py" allows --host dankserver --service sonarr + [ "$status" -ne 0 ] # the *arr stack lives on frankbook-pro, not dankserver + run python3 "$REPO_ROOT/scripts/service-map.py" allows --host dankclaw --service frank-archon + [ "$status" -ne 0 ] # frank-archon lives on dankserver +} + +@test "inventory: the planner reproduces the pinned selection fixture" { + # tests/fixtures/service-plan-golden.tsv is the characterization of + # path -> service selection across every tracked path, every target, + # and the multi-path combos the old hand-written planner was fed. + python3 - "$REPO_ROOT" <<'PYEOF' +import importlib.util +import sys + +root = sys.argv[1] +spec = importlib.util.spec_from_file_location("sm", root + "/scripts/service-map.py") +sm = importlib.util.module_from_spec(spec) +spec.loader.exec_module(sm) +inv = sm.Inventory.load(root + "/shared/services.yml") +bad = [] +with open(root + "/tests/fixtures/service-plan-golden.tsv") as fh: + for line in fh: + host, paths, want = line.rstrip("\n").split("\t") + got = ",".join(sm.select(inv, host, paths.split("+"))) + if got != want: + bad.append(f"{host} {paths}\n want: {want}\n got: {got}") +if bad: + print(f"{len(bad)} selection mismatches:", *bad[:20], sep="\n") + sys.exit(1) +PYEOF +} + +@test "inventory: deploy-plan.py keeps its CLI contract on top of service-map" { + # stdin paths, positional paths, and the --repo/--base/--head diff mode + # all route to the same selection. + run bash -c "cd '$REPO_ROOT' && printf '%s\n' dankclaw/searxng/quadlet/gluetun.container | \ + scripts/deploy-plan.py --target dankclaw" + [ "$status" -eq 0 ] + [ "${lines[0]}" = "gluetun" ] + run "$REPO_ROOT/scripts/deploy-plan.py" --target dankserver scripts/frank-archon.env + [ "$status" -eq 0 ] + [ "${lines[0]}" = "frank-archon" ] + run "$REPO_ROOT/scripts/deploy-plan.py" --target dankclaw --repo "$REPO_ROOT" --base HEAD --head HEAD + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "inventory: the Pi-hole hosts file is a rendered artifact" { + # The tracked addn-hosts must be byte-identical to the render; editing + # it by hand is drift the inventory check also catches. + run bash -c "cd '$REPO_ROOT' && python3 scripts/service-map.py render pihole-hosts | \ + diff -u dankclaw/pihole/addn-hosts -" + [ "$status" -eq 0 ] +} + +@test "inventory: changed-test seams for a service unit are the service's own" { + # The rank-3 contract: a unit change selects the OWNING service's + # declared seams, not the old shotgun of unrelated quadlet seams. + run bash "$REPO_ROOT/scripts/test-changed.sh" --list --paths dankclaw/gitea/quadlet/gitea.network + [ "$status" -eq 0 ] + [[ "$output" == *"tests/caddyfile.bats"* ]] + [[ "$output" == *"tests/deployment_scoping.bats"* ]] + [[ "$output" != *"tests/open_webui_quadlet.bats"* ]] + [[ "$output" != *"tests/searxng_quadlet.bats"* ]] +} + +@test "inventory: every tracked quadlet unit is claimed by exactly one service" { + # check units covers claim conflicts; this pins the census size so a + # unit added WITHOUT an inventory entry (unclaimed) fails the seam. + python3 - "$REPO_ROOT" <<'PYEOF' +import importlib.util +import subprocess +import sys +from pathlib import PurePosixPath + +root = sys.argv[1] +spec = importlib.util.spec_from_file_location("sm", root + "/scripts/service-map.py") +sm = importlib.util.module_from_spec(spec) +spec.loader.exec_module(sm) +inv = sm.Inventory.load(root + "/shared/services.yml") +graph = sm.graph_for(tuple(inv.hosts)) +owned = {u for u in graph.units} +problems = [] +tracked_units = [ + p for p in sm.tracked_files() + if PurePosixPath(p).name.endswith((".container", ".network")) and not sm.is_trash(p) +] +for path in tracked_units: + parts = PurePosixPath(path).parts + name = parts[-1] + key = (parts[0], name.rsplit(".", 1)[0]) + if key not in graph.units and key not in graph.networks: + problems.append(f"unclaimed unit-file path: {path}") +if problems: + print("\n".join(problems)) + sys.exit(1) +PYEOF +}