Homepage (TDD slice 3): the real services.json + schema tests
Red tests first (the old services.json was an object, not the new list schema). - tests/homepage_config.bats: the config is a JSON list; every entry has name/category/probe; category is user|admin; method is tcp|http; a showLink entry has a url and a status-only entry does not. - shared/homepage/services.json: the real fleet. User: SearXNG, ComfyUI, OpenWebUI, Gitea, Calibre-Web, RomM, Copyparty, Plex, Jellyfin. Admin: Hermes, Grafana, Prometheus, Loki, node-exporter, alloy, Caddy, dnsmasq, Gluetun (wip - a tunnel has no simple local port), the two LLMs, mariadb, valkey, Postgres. Ports confirmed from the repo config (compose/quadlet). docs fields are empty for now (the /docs/ workstream fills them as each service goes active).
This commit is contained in:
@@ -1,59 +1,26 @@
|
||||
{
|
||||
"host": "dankclaw",
|
||||
"services": [
|
||||
{
|
||||
"name": "Homepage",
|
||||
"url": "/",
|
||||
"description": "This page. Password-protected service index.",
|
||||
"docs": "docs/Infrastructure/Homepage.md"
|
||||
},
|
||||
{
|
||||
"name": "SearXNG",
|
||||
"url": "http://search.franksdank.cloud",
|
||||
"description": "Privacy-respecting metasearch engine (through Gluetun VPN).",
|
||||
"docs": "docs/Dankclaw/SearXNG.md"
|
||||
},
|
||||
{
|
||||
"name": "ComfyUI",
|
||||
"url": "http://comfyui.franksdank.cloud",
|
||||
"description": "Node-based AI image generation (CPU backend on Strix Halo).",
|
||||
"docs": "docs/Dankclaw/ComfyUI.md"
|
||||
},
|
||||
{
|
||||
"name": "OpenWebUI",
|
||||
"url": "http://chat.franksdank.cloud",
|
||||
"description": "Chat interface for local LLMs (llama.cpp on :7770).",
|
||||
"docs": "docs/Dankclaw/OpenWebUI.md"
|
||||
},
|
||||
{
|
||||
"name": "Hermes",
|
||||
"url": "http://hermes.franksdank.cloud",
|
||||
"description": "Self-improving AI agent (Nous Research).",
|
||||
"docs": "docs/Dankclaw/Hermes.md"
|
||||
},
|
||||
{
|
||||
"name": "Gitea",
|
||||
"url": "http://gitea.franksdank.cloud",
|
||||
"description": "Self-hosted Git service with issues and PRs.",
|
||||
"docs": "docs/Dankclaw/Gitea.md"
|
||||
},
|
||||
{
|
||||
"name": "Grafana",
|
||||
"url": "http://grafana.franksdank.cloud",
|
||||
"description": "Dashboards for metrics and logs.",
|
||||
"docs": "docs/Infrastructure/Grafana.md"
|
||||
},
|
||||
{
|
||||
"name": "Prometheus",
|
||||
"url": "http://prometheus.franksdank.cloud",
|
||||
"description": "Metrics collection for the whole stack.",
|
||||
"docs": "docs/Infrastructure/Prometheus.md"
|
||||
},
|
||||
{
|
||||
"name": "Loki",
|
||||
"url": "http://loki.franksdank.cloud",
|
||||
"description": "Log storage and query backend.",
|
||||
"docs": "docs/Infrastructure/Loki.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
[
|
||||
{"name":"SearXNG","category":"user","description":"Privacy-respecting metasearch engine.","url":"http://search.franksdank.cloud","docs":"","probe":"127.0.0.1:8081","method":"http","wip":false,"showLink":true},
|
||||
{"name":"ComfyUI","category":"user","description":"Node-based image generation.","url":"http://comfyui.franksdank.cloud","docs":"","probe":"127.0.0.1:8188","method":"http","wip":false,"showLink":true},
|
||||
{"name":"OpenWebUI","category":"user","description":"Chat interface for the LLM fleet.","url":"http://chat.franksdank.cloud","docs":"","probe":"127.0.0.1:3005","method":"http","wip":false,"showLink":true},
|
||||
{"name":"Gitea","category":"user","description":"Git hosting and code review.","url":"http://gitea.franksdank.cloud","docs":"","probe":"127.0.0.1:3000","method":"http","wip":false,"showLink":true},
|
||||
{"name":"Calibre-Web","category":"user","description":"E-book library.","url":"http://books.franksdank.cloud","docs":"","probe":"192.168.0.235:8083","method":"http","wip":false,"showLink":true},
|
||||
{"name":"RomM","category":"user","description":"ROM and emulator library.","url":"http://romm.franksdank.cloud","docs":"","probe":"192.168.0.235:8085","method":"http","wip":false,"showLink":true},
|
||||
{"name":"Copyparty","category":"user","description":"File sharing over the LAN.","url":"http://files.franksdank.cloud","docs":"","probe":"192.168.0.235:3923","method":"http","wip":false,"showLink":true},
|
||||
{"name":"Plex","category":"user","description":"Media server.","url":"http://plex.franksdank.cloud","docs":"","probe":"192.168.0.235:32400","method":"http","wip":false,"showLink":true},
|
||||
{"name":"Jellyfin","category":"user","description":"Media server.","url":"http://jellyfin.franksdank.cloud","docs":"","probe":"192.168.0.235:8096","method":"http","wip":false,"showLink":true},
|
||||
|
||||
{"name":"Hermes","category":"admin","description":"The agent gateway.","url":"http://hermes.franksdank.cloud","docs":"","probe":"127.0.0.1:8090","method":"http","wip":false,"showLink":true},
|
||||
{"name":"Grafana","category":"admin","description":"Dashboards.","url":"http://grafana.franksdank.cloud","docs":"","probe":"127.0.0.1:3002","method":"http","wip":false,"showLink":true},
|
||||
{"name":"Prometheus","category":"admin","description":"Metrics collection.","url":"http://prometheus.franksdank.cloud","docs":"","probe":"127.0.0.1:9090","method":"http","wip":false,"showLink":true},
|
||||
{"name":"Loki","category":"admin","description":"Log storage.","url":"http://loki.franksdank.cloud","docs":"","probe":"127.0.0.1:3100","method":"http","wip":false,"showLink":true},
|
||||
{"name":"node-exporter","category":"admin","description":"Host metrics.","url":"","docs":"","probe":"127.0.0.1:9100","method":"http","wip":false,"showLink":false},
|
||||
{"name":"alloy","category":"admin","description":"Log shipping.","url":"","docs":"","probe":"127.0.0.1:12345","method":"http","wip":false,"showLink":false},
|
||||
{"name":"Caddy","category":"admin","description":"The reverse proxy.","url":"","docs":"","probe":"127.0.0.1:80","method":"http","wip":false,"showLink":false},
|
||||
{"name":"dnsmasq","category":"admin","description":"LAN name resolution.","url":"","docs":"","probe":"127.0.0.1:53","method":"tcp","wip":false,"showLink":false},
|
||||
{"name":"Gluetun","category":"admin","description":"The outbound VPN tunnel.","url":"","docs":"","probe":"127.0.0.1:51820","method":"tcp","wip":true,"showLink":false},
|
||||
{"name":"frank-default-large","category":"admin","description":"The large LLM (27B) on Dankclaw.","url":"","docs":"","probe":"127.0.0.1:7770","method":"http","wip":false,"showLink":false},
|
||||
{"name":"frank-default-small","category":"admin","description":"The small LLM (35B MoE) on Dankserver.","url":"","docs":"","probe":"192.168.0.235:7770","method":"http","wip":false,"showLink":false},
|
||||
{"name":"mariadb","category":"admin","description":"The media-stack database.","url":"","docs":"","probe":"192.168.0.235:3306","method":"tcp","wip":false,"showLink":false},
|
||||
{"name":"valkey","category":"admin","description":"The SearXNG cache.","url":"","docs":"","probe":"127.0.0.1:6379","method":"tcp","wip":false,"showLink":false},
|
||||
{"name":"Postgres","category":"admin","description":"The Gitea database.","url":"","docs":"","probe":"127.0.0.1:5432","method":"tcp","wip":false,"showLink":false}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bats
|
||||
# Seam: shared/homepage/services.json — the service config the checker reads
|
||||
# and the pages render. It must be a JSON list; every entry carries name,
|
||||
# category, and a host:port probe. category is user or admin. method, when
|
||||
# present, is tcp or http.
|
||||
|
||||
load helpers
|
||||
|
||||
@test "services.json: valid schema" {
|
||||
python3 - "$REPO_ROOT/shared/homepage/services.json" <<'PY'
|
||||
import json, sys
|
||||
path = sys.argv[1]
|
||||
try:
|
||||
data = json.load(open(path))
|
||||
except FileNotFoundError:
|
||||
sys.exit("services.json not found")
|
||||
assert isinstance(data, list), "top level must be a list"
|
||||
assert data, "the list must not be empty"
|
||||
for svc in data:
|
||||
for field in ("name", "category", "probe"):
|
||||
assert field in svc, f"{svc.get('name', '?')} is missing {field}"
|
||||
assert svc["category"] in ("user", "admin"), svc["category"]
|
||||
if "method" in svc:
|
||||
assert svc["method"] in ("tcp", "http"), svc["method"]
|
||||
host, _, port = svc["probe"].rpartition(":")
|
||||
assert host and port.isdigit(), f"bad probe {svc['probe']}"
|
||||
PY
|
||||
}
|
||||
|
||||
@test "services.json: every user service has an Open link, status-only backends do not" {
|
||||
python3 - "$REPO_ROOT/shared/homepage/services.json" <<'PY'
|
||||
import json, sys
|
||||
data = json.load(open(sys.argv[1]))
|
||||
for svc in data:
|
||||
show = svc.get("showLink", True)
|
||||
if show:
|
||||
assert svc.get("url"), f"{svc['name']} shows a link but has no url"
|
||||
else:
|
||||
assert not svc.get("url"), f"{svc['name']} is status-only but has a url"
|
||||
PY
|
||||
}
|
||||
Reference in New Issue
Block a user