From ce55d1dc3a975869da2ce2cd39575e125b3f3ce3 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 23 Aug 2026 12:11:21 -0700 Subject: [PATCH] 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). --- shared/homepage/services.json | 85 +++++++++++------------------------ tests/homepage_config.bats | 41 +++++++++++++++++ 2 files changed, 67 insertions(+), 59 deletions(-) create mode 100644 tests/homepage_config.bats diff --git a/shared/homepage/services.json b/shared/homepage/services.json index 8657806..c31f413 100644 --- a/shared/homepage/services.json +++ b/shared/homepage/services.json @@ -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} +] diff --git a/tests/homepage_config.bats b/tests/homepage_config.bats new file mode 100644 index 0000000..dfea0d1 --- /dev/null +++ b/tests/homepage_config.bats @@ -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 +}