Skip to content

ADR-0059: Service catalog — auto-generated per-service home pages from the Alloy probe inventory

A docs service catalog: one auto-generated home page per homelab service, published under docs.pitbun.com/services/<domain>/<service>/. Each page is a full service card (identity, endpoints, observability model, dependencies, live dashboard + rollup links) generated from the same Alloy blackbox probe inventory that already defines the Grafana per-service dashboards and the dynamic service-health rollup. One declarative inventory, three renderers (dashboards, rollup, catalog). This is the homelab’s lightweight take on the service-catalog / Backstage pattern — a single source of truth fanned out to every consumer.

StatusAccepted
Date2026-06-27
EpicADO #1252
Extended byADR-0063 (umbrella/parent pages over duplicated or grouped components)

Per-service dashboards (ADR-0045) and the dynamic rollup gave us a single observability plane keyed on the probe_success{service,domain} blackbox inventory in config.alloy. But there was no knowledge plane: no canonical page per service stating what it is, where it lives, how it’s reached, and how it’s observed. Pillar 5 (docs-as-code) and Pillar 2 (a conforming page per service) were unmet for the fleet as a whole.

The constraints that shaped the design:

  • The service set must not be re-declared. A second hand-maintained list of services would drift from the Alloy inventory immediately. Whatever drives the catalog must read the same inventory the dashboards and rollup read.
  • New services must self-onboard. A page must appear when a service comes online (i.e. when its probe target lands in config.alloy) with no manual step — “ensure they’re created as new services come online” was the explicit requirement.
  • claude -p cannot run in the docs pipeline. The narrative blurb (Prime Directive 9) needs an LLM, but the Anthropic API key lives on control01, not on the ADO build agent. The six existing doc_gen generators that use claude -p (e.g. generate_ansible_docs.py) all run on control01 and commit their output; the docs pipeline only ever publishes committed files.
  1. Source of truth = the Alloy probe inventory. generate_service_homepages.py (a seventh doc_gen generator, house shape: argparse --src/--out, md5 content-hash cache, claude -p narrative, --no-llm fallback) parses config.alloy, groups probe targets by service, and emits one card per service plus a generated landing index and .pages nav.

  2. Fully automatic, no hand-authored layer. Every field — including the Prime-Directive-9 blurb — is generated. The blurb is produced by claude -p from the service’s facts and content-hash cached, so it regenerates only when the facts change. There is no per-service override file: if a blurb is wrong, fix the facts or the generator prompt.

  3. control01 cron + commit mechanism. The generator runs on control01 (where the key lives), commits the rendered pages into pitlab-docs with [skip ci], and pushes. The docs pipeline then publishes them like any committed section (a wholesale per-section copy in docs.yml + a root.pages entry). Pages are git-visible and diffable. Regeneration is triggered by the docker-stacks observability deploy (the flow that ships a new probe target) plus a nightly cron safety net.

  4. Full bidirectional linking. Pages link to /d/<service> and the rollup; every catalog dashboard links back to its doc page (see ADR-0045, amended). The bulk-inject and the enforcing conformance gate live in docker-stacks.

  5. Domain-grouped, vocab-canonicalised tags. Pages live at services/<domain>/<service>.md (the 8 rollup domains). Tagged kind: generated + the domain; the short Alloy codes infra and network map to the existing vocab tags infrastructure/networking so the tag index stays cohesive (only app was added to the vocabulary).

commit + pushbidirectional linksindex linkenforcesenforcesconfig.alloyprobe inventory(service, domain,endpoints)Grafana per-servicedashboards (uid=service)Dynamic service-healthrollup (probe_success)generate_service_homepages.pycontrol01 · claude -p ·cachedpitlab-docs/docs/services/docs pipelinepublishdocs.pitbun.com/services/service catalogconformance gate(docker-stacks CI)
commit + pushbidirectional linksindex linkenforcesenforcesconfig.alloyprobe inventory(service, domain,endpoints)Grafana per-servicedashboards (uid=service)Dynamic service-healthrollup (probe_success)generate_service_homepages.pycontrol01 · claude -p ·cachedpitlab-docs/docs/services/docs pipelinepublishdocs.pitbun.com/services/service catalogconformance gate(docker-stacks CI)
  • A new dedicated services.yml registry. Richer per-service fields, but a second source to keep in sync with Alloy — exactly the drift we refused. Rejected.
  • Scan docker-compose stacks for the service set. Captures Docker workloads but misses host-based services (plex, pve, zabbix, samba) that have probe targets but no compose file. Rejected.
  • Query Prometheus probe_success labels (like the rollup) instead of parsing the file. Truest mirror of the rollup, but the labels carry no endpoints/module, so the full card can’t be built from them, and it adds a live Prometheus dependency in the generator. Rejected in favour of parsing the committed desired-state.
  • Hand-authored blurb, layered over generated card. Was the initial choice; reversed mid-design — the blurb had to be automatic so new services need zero human step. With an auto blurb the override layer lost its only justification and was dropped.
  • Build-time generation in the docs pipeline (ephemeral pages). Was the initial choice; reversed once it was confirmed claude -p can’t run on the build agent (no key). The control01 cron + commit model is the established house pattern for LLM-using generators.
  • Put an Anthropic key on the build agent. Would allow build-time generation but adds an LLM secret to the build agent’s blast radius and diverges from the house pattern. Rejected on DevSecOps grounds.
  • A complete service catalog ships for all 44 Alloy services with real claude -p blurbs; new services self-onboard with no manual step. The docs site is Cloudflare-Access-gated, so the full internal detail (endpoints, deps) is an internal surface and safe to publish.
  • The generator’s .doc-cache.json is committed (persists blurbs across cron runs); the docs build strips it from the published tree.
  • The build is governed by an explicit per-section copy; a future top-level section still needs its copy block + root.pages entry (the standing docs-pipeline gotcha).
  • Pillar-2 gaps surfaced by the work (4 services with no dashboard, 6 dashboards with no probe) are tracked as ADO debt (#1258, #1260), not silently ignored.