Add paired specification.json and specification.html generator
Single SPEC source regenerates machine-readable JSON for agents and dark HTML for humans so architecture survives lost chat sessions.
This commit is contained in:
parent
0108868a2c
commit
1a6f915f04
5 changed files with 1265 additions and 0 deletions
413
specification.json
Normal file
413
specification.json
Normal file
|
|
@ -0,0 +1,413 @@
|
|||
{
|
||||
"meta": {
|
||||
"name": "arr-stack",
|
||||
"title": "Homelab media + local AI stack",
|
||||
"version": "1.2.0",
|
||||
"generated_at": "2026-07-23T02:54:38Z",
|
||||
"generator": "scripts/generate-specification.py",
|
||||
"repository": {
|
||||
"forgejo": "http://192.168.8.123:3002/tim/stack",
|
||||
"clone_ssh": "ssh://git@192.168.8.123:2222/tim/stack.git",
|
||||
"default_branch": "main"
|
||||
},
|
||||
"primary_host": {
|
||||
"hostname": "stack",
|
||||
"lan_ip": "192.168.8.123",
|
||||
"user": "tim",
|
||||
"os": "Ubuntu Server 24.04",
|
||||
"hardware": "AMD Ryzen 7 5800H + Radeon Graphics (shared RAM), ~28–32 GB RAM",
|
||||
"stack_root": "/opt/stack",
|
||||
"media_root": "/storage"
|
||||
},
|
||||
"demo_credentials": {
|
||||
"username": "tim",
|
||||
"password": "asdfasdf",
|
||||
"note": "Tech demo only; used for local UIs when configured that way"
|
||||
},
|
||||
"docs_for_humans": [
|
||||
"specification.html",
|
||||
"quickstart.md",
|
||||
"README.md"
|
||||
],
|
||||
"docs_for_agents": [
|
||||
"specification.json",
|
||||
"AGENTS.md",
|
||||
"quickstart.md",
|
||||
"profiles/*.env",
|
||||
"docker-compose.yml"
|
||||
],
|
||||
"never_commit": [
|
||||
".env",
|
||||
"secrets/.env",
|
||||
"wireguard/wg0.conf",
|
||||
"config/",
|
||||
"models/**/*.gguf",
|
||||
"data/"
|
||||
]
|
||||
},
|
||||
"architecture": {
|
||||
"summary": "Docker Compose stack combining VPN-locked torrents (*arr + qBittorrent via gluetun), Usenet (SABnzbd), media (Jellyfin), local LLM inference (Prism llama-server + Open WebUI), Forgejo git, OpenHands agent, Caddy LAN landing page, and Cloudflare DDNS.",
|
||||
"networks": {
|
||||
"arr-net": "Bridge network for most services",
|
||||
"gluetun_namespace": "qbittorrent uses network_mode: service:gluetun (VPN killswitch)",
|
||||
"host_network": [
|
||||
"cloudflare-ddns"
|
||||
]
|
||||
},
|
||||
"data_layout": {
|
||||
"CONFIG_DIR": "App configs (default /opt/stack/config on server)",
|
||||
"DATA_DIR": "Shared media tree (default /storage on server)",
|
||||
"paths": {
|
||||
"media": "/data/media/{movies,tv,music}",
|
||||
"torrents": "/data/torrents/{movies,tv,music}",
|
||||
"usenet": "/data/usenet/incomplete and /data/usenet/complete/{movies,tv,music}",
|
||||
"models": "models/bonsai/*.gguf mounted read-only at /models in bonsai",
|
||||
"workspace": "AI_WORKSPACE_DIR for Open WebUI + OpenHands"
|
||||
}
|
||||
},
|
||||
"security_model": {
|
||||
"torrents": "Only via WireGuard through gluetun; no BitTorrent port on host",
|
||||
"usenet": "Direct SSL to provider (not through gluetun)",
|
||||
"lan_firewall": "UFW allow service ports from LAN_SUBNET only",
|
||||
"landing_and_uis": "Plain HTTP on LAN (no TLS certs yet)"
|
||||
}
|
||||
},
|
||||
"hardware_profiles": {
|
||||
"how_to_apply": "./scripts/apply-profile.sh mini|mid|gaming",
|
||||
"files": {
|
||||
"mini": "profiles/mini.env",
|
||||
"mid": "profiles/mid.env",
|
||||
"gaming": "profiles/gaming.env"
|
||||
},
|
||||
"profiles": {
|
||||
"mini": {
|
||||
"hardware": "Beelink / SER5-class mini PC",
|
||||
"inference": "cpu",
|
||||
"ngl": 0,
|
||||
"default_ctx": 32768,
|
||||
"threads": 12,
|
||||
"default_model": "Ternary-Bonsai-27B-Q2_0.gguf",
|
||||
"model_repo": "prism-ml/Ternary-Bonsai-27B-gguf",
|
||||
"notes": "CPU preferred on APU; do not force NGL in docker-compose.override.yml"
|
||||
},
|
||||
"mid": {
|
||||
"hardware": "~8GB AMD discrete GPU, ~16GB system RAM",
|
||||
"inference": "vulkan",
|
||||
"ngl": 99,
|
||||
"default_ctx": 8192,
|
||||
"threads": 8,
|
||||
"kv4": true,
|
||||
"default_model": "Qwen3.5-9B-abliterated-v2-MAX.Q4_K_M.gguf",
|
||||
"model_repo": "mradermacher/Qwen3.5-9B-abliterated-v2-MAX-GGUF",
|
||||
"notes": "Fits VRAM; use KV4 for memory"
|
||||
},
|
||||
"gaming": {
|
||||
"hardware": "~16GB AMD GPU, ~64GB system RAM",
|
||||
"inference": "vulkan",
|
||||
"ngl": 99,
|
||||
"default_ctx": 32768,
|
||||
"threads": 16,
|
||||
"default_model": "Ternary-Bonsai-27B-Q2_0.gguf",
|
||||
"also_download": [
|
||||
"Qwen3.5-9B Q4_K_M"
|
||||
],
|
||||
"notes": "Full Vulkan + large context"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
"bonsai27": {
|
||||
"file": "Ternary-Bonsai-27B-Q2_0.gguf",
|
||||
"repo": "prism-ml/Ternary-Bonsai-27B-gguf",
|
||||
"runtime": "PrismML llama.cpp fork (custom Q2_0)",
|
||||
"size_approx": "~7.2 GB"
|
||||
},
|
||||
"qwen9_abliterated_q4km": {
|
||||
"file": "Qwen3.5-9B-abliterated-v2-MAX.Q4_K_M.gguf",
|
||||
"repo": "mradermacher/Qwen3.5-9B-abliterated-v2-MAX-GGUF",
|
||||
"runtime": "Same Prism llama-server image (standard GGUF)",
|
||||
"size_approx": "~5.6 GB"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": [
|
||||
{
|
||||
"id": "landing",
|
||||
"name": "Landing (Caddy)",
|
||||
"image": "caddy:2-alpine",
|
||||
"port": 80,
|
||||
"url": "http://HOST/",
|
||||
"purpose": "Dark-mode LAN resource map",
|
||||
"network": "arr-net",
|
||||
"category": "gateway"
|
||||
},
|
||||
{
|
||||
"id": "gluetun",
|
||||
"name": "gluetun",
|
||||
"image": "qmcgaw/gluetun:v3",
|
||||
"port": null,
|
||||
"published_ports": [
|
||||
"8080:8080 (qBittorrent WebUI)"
|
||||
],
|
||||
"purpose": "WireGuard VPN + killswitch",
|
||||
"network": "arr-net",
|
||||
"category": "vpn",
|
||||
"config": "wireguard/wg0.conf"
|
||||
},
|
||||
{
|
||||
"id": "qbittorrent",
|
||||
"name": "qBittorrent",
|
||||
"image": "lscr.io/linuxserver/qbittorrent:latest",
|
||||
"port": 8080,
|
||||
"url": "http://HOST:8080/",
|
||||
"purpose": "Torrent client (VPN-only via gluetun)",
|
||||
"network": "service:gluetun",
|
||||
"category": "download"
|
||||
},
|
||||
{
|
||||
"id": "sabnzbd",
|
||||
"name": "SABnzbd",
|
||||
"image": "lscr.io/linuxserver/sabnzbd:latest",
|
||||
"port": 8085,
|
||||
"url": "http://HOST:8085/",
|
||||
"purpose": "Usenet NZB downloader",
|
||||
"network": "arr-net",
|
||||
"category": "download"
|
||||
},
|
||||
{
|
||||
"id": "prowlarr",
|
||||
"name": "Prowlarr",
|
||||
"image": "lscr.io/linuxserver/prowlarr:latest",
|
||||
"port": 9696,
|
||||
"url": "http://HOST:9696/",
|
||||
"purpose": "Indexer manager (torrents + NZB)",
|
||||
"network": "arr-net",
|
||||
"category": "download"
|
||||
},
|
||||
{
|
||||
"id": "flaresolverr",
|
||||
"name": "FlareSolverr",
|
||||
"image": "ghcr.io/flaresolverr/flaresolverr:latest",
|
||||
"port": 8191,
|
||||
"url": "http://flaresolverr:8191 (internal)",
|
||||
"purpose": "Cloudflare JS challenge proxy for Prowlarr",
|
||||
"network": "arr-net",
|
||||
"category": "download",
|
||||
"published": false
|
||||
},
|
||||
{
|
||||
"id": "sonarr",
|
||||
"name": "Sonarr",
|
||||
"image": "lscr.io/linuxserver/sonarr:latest",
|
||||
"port": 8989,
|
||||
"url": "http://HOST:8989/",
|
||||
"purpose": "TV automation",
|
||||
"network": "arr-net",
|
||||
"category": "media"
|
||||
},
|
||||
{
|
||||
"id": "radarr",
|
||||
"name": "Radarr",
|
||||
"image": "lscr.io/linuxserver/radarr:latest",
|
||||
"port": 7878,
|
||||
"url": "http://HOST:7878/",
|
||||
"purpose": "Movie automation",
|
||||
"network": "arr-net",
|
||||
"category": "media"
|
||||
},
|
||||
{
|
||||
"id": "lidarr",
|
||||
"name": "Lidarr",
|
||||
"image": "lscr.io/linuxserver/lidarr:latest",
|
||||
"port": 8686,
|
||||
"url": "http://HOST:8686/",
|
||||
"purpose": "Music automation",
|
||||
"network": "arr-net",
|
||||
"category": "media"
|
||||
},
|
||||
{
|
||||
"id": "bazarr",
|
||||
"name": "Bazarr",
|
||||
"image": "lscr.io/linuxserver/bazarr:latest",
|
||||
"port": 6767,
|
||||
"url": "http://HOST:6767/",
|
||||
"purpose": "Subtitles",
|
||||
"network": "arr-net",
|
||||
"category": "media"
|
||||
},
|
||||
{
|
||||
"id": "jellyfin",
|
||||
"name": "Jellyfin",
|
||||
"image": "lscr.io/linuxserver/jellyfin:latest",
|
||||
"port": 8096,
|
||||
"url": "http://HOST:8096/",
|
||||
"purpose": "Media server / playback",
|
||||
"network": "arr-net",
|
||||
"category": "media"
|
||||
},
|
||||
{
|
||||
"id": "bonsai",
|
||||
"name": "Bonsai (llama-server)",
|
||||
"image": "arr-stack/bonsai:local (ai/Dockerfile)",
|
||||
"port": 8081,
|
||||
"url": "http://HOST:8081/v1",
|
||||
"purpose": "Local LLM OpenAI-compatible API (Prism build)",
|
||||
"network": "arr-net",
|
||||
"category": "ai"
|
||||
},
|
||||
{
|
||||
"id": "open-webui",
|
||||
"name": "Open WebUI",
|
||||
"image": "ghcr.io/open-webui/open-webui:main",
|
||||
"port": 3000,
|
||||
"url": "http://HOST:3000/",
|
||||
"purpose": "Chat UI + RAG + tools",
|
||||
"network": "arr-net",
|
||||
"category": "ai",
|
||||
"llm_base": "http://bonsai:8080/v1"
|
||||
},
|
||||
{
|
||||
"id": "searxng",
|
||||
"name": "SearXNG",
|
||||
"image": "docker.io/searxng/searxng:latest",
|
||||
"port": 8888,
|
||||
"url": "http://127.0.0.1:8888/ (localhost only)",
|
||||
"purpose": "Private search for Open WebUI RAG",
|
||||
"network": "arr-net",
|
||||
"category": "ai",
|
||||
"published": "loopback"
|
||||
},
|
||||
{
|
||||
"id": "openhands",
|
||||
"name": "OpenHands",
|
||||
"image": "ghcr.io/all-hands-ai/openhands:0.54",
|
||||
"port": 3001,
|
||||
"url": "http://HOST:3001/",
|
||||
"purpose": "AI coding agent (always deployed)",
|
||||
"network": "arr-net",
|
||||
"category": "ai",
|
||||
"note": "No Forgejo integration in UI — clone repos into workspace"
|
||||
},
|
||||
{
|
||||
"id": "forgejo",
|
||||
"name": "Forgejo",
|
||||
"image": "codeberg.org/forgejo/forgejo:11",
|
||||
"port": 3002,
|
||||
"url": "http://HOST:3002/",
|
||||
"ssh_port": 2222,
|
||||
"purpose": "Self-hosted Git",
|
||||
"network": "arr-net",
|
||||
"category": "code"
|
||||
},
|
||||
{
|
||||
"id": "cloudflare-ddns",
|
||||
"name": "Cloudflare DDNS",
|
||||
"image": "timothyjmiller/cloudflare-ddns:latest",
|
||||
"port": null,
|
||||
"url": null,
|
||||
"purpose": "Update public IPv4 A records for domain + subdomains",
|
||||
"network": "host",
|
||||
"category": "dns",
|
||||
"domain": "devopshomelab.com",
|
||||
"subdomains": [
|
||||
"mc",
|
||||
"minecraft",
|
||||
"vpn",
|
||||
"www",
|
||||
"forgejo",
|
||||
"jellyfin"
|
||||
]
|
||||
}
|
||||
],
|
||||
"scripts": {
|
||||
"generate-specification.py": "Regenerate specification.json + specification.html (this file)",
|
||||
"apply-profile.sh": "Merge profiles/{mini,mid,gaming}.env into .env",
|
||||
"bootstrap-stack.sh": "Fresh bring-up: dirs, models, compose up all, heal gluetun, usenet, cloudflare",
|
||||
"download-models.sh": "Download Bonsai 27B and/or Qwen 9B GGUF into models/bonsai",
|
||||
"download-bonsai-model.sh": "Wrapper → download-models.sh",
|
||||
"configure-usenet.sh": "SABnzbd folders + Newshosting/Tweaknews from secrets/.env",
|
||||
"render-cloudflare-ddns-env.sh": "Build config/cloudflare-ddns/ddns.env from secrets",
|
||||
"gluetun-endpoint-watch.sh": "If gluetun unhealthy: dig WG host, update Endpoint, recreate gluetun+qbit"
|
||||
},
|
||||
"ansible": {
|
||||
"entry": "ansible/site.yml",
|
||||
"inventory": "ansible/inventory/hosts.ini",
|
||||
"roles": [
|
||||
"common",
|
||||
"docker",
|
||||
"amd_vulkan",
|
||||
"firewall",
|
||||
"stack",
|
||||
"users"
|
||||
],
|
||||
"stack_deploy_method": "git (default) or rsync",
|
||||
"server_repo": "/opt/stack tracks Forgejo via git pull",
|
||||
"timers": [
|
||||
"arr-stack.service",
|
||||
"gluetun-endpoint-watch.timer"
|
||||
]
|
||||
},
|
||||
"workflows": {
|
||||
"fresh_install": [
|
||||
"cp .env.example .env; fill STACK_HOST, LAN_SUBNET, usenet, cloudflare keys",
|
||||
"Place wireguard/wg0.conf (Endpoint IP + # EndpointHost = hostname)",
|
||||
"./scripts/apply-profile.sh mini|mid|gaming",
|
||||
"./scripts/bootstrap-stack.sh",
|
||||
"One-time UI: Prowlarr apps, Jellyfin libraries, Forgejo/Open WebUI admin (demo user/pass)"
|
||||
],
|
||||
"day_to_day_server": [
|
||||
"cd /opt/stack && git pull",
|
||||
"docker compose up -d",
|
||||
"rebuild bonsai if Dockerfile/profile backend changed"
|
||||
],
|
||||
"after_secrets_change": {
|
||||
"cloudflare": [
|
||||
"./scripts/render-cloudflare-ddns-env.sh",
|
||||
"docker compose up -d cloudflare-ddns --force-recreate"
|
||||
],
|
||||
"usenet_servers": [
|
||||
"./scripts/configure-usenet.sh",
|
||||
"Test servers in SABnzbd UI"
|
||||
],
|
||||
"nzbgeek": [
|
||||
"Update API key in Prowlarr UI"
|
||||
],
|
||||
"wireguard": [
|
||||
"Edit wireguard/wg0.conf",
|
||||
"docker compose up -d gluetun --force-recreate",
|
||||
"or ./scripts/gluetun-endpoint-watch.sh"
|
||||
],
|
||||
"stack_env": [
|
||||
"Edit .env",
|
||||
"docker compose up -d (+ build bonsai if backend changes)"
|
||||
]
|
||||
},
|
||||
"switch_profile": [
|
||||
"./scripts/apply-profile.sh mid",
|
||||
"docker compose build bonsai && docker compose up -d"
|
||||
],
|
||||
"agent_git": [
|
||||
"Commit meaningful changes with complete sentences",
|
||||
"Push to origin main (Forgejo)",
|
||||
"Never commit secrets",
|
||||
"After architecture changes: run generate-specification.py and commit both outputs"
|
||||
]
|
||||
},
|
||||
"compose": {
|
||||
"file": "docker-compose.yml",
|
||||
"override": "docker-compose.override.yml",
|
||||
"always_deploy_all": true,
|
||||
"note": "OpenHands is not behind a compose profile; full stack always starts",
|
||||
"override_rules": [
|
||||
"Do not put group_add names (video/render) — use host GIDs or 0666 /dev/dri",
|
||||
"Do not force bonsai NGL in override on mini (overrides .env)",
|
||||
"mini: jellyfin /dev/dri only; mid/gaming: add bonsai devices for Vulkan"
|
||||
]
|
||||
},
|
||||
"env_and_secrets": {
|
||||
"stack_env": ".env (from .env.example) — ports, paths, AI, bootstrap flags, demo login",
|
||||
"secrets_env": "secrets/.env — usenet + cloudflare API (gitignored)",
|
||||
"approx_env_keys": 80,
|
||||
"demo_user": "tim",
|
||||
"demo_pass": "asdfasdf"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue