MCPcopy Index your code
hub / github.com/crzykidd/service-tracker-dashboard

github.com/crzykidd/service-tracker-dashboard @v0.6.6

Chat with this repo
repository ↗ · DeepWiki ↗ · release v0.6.6 ↗ · + Follow
162 symbols 569 edges 33 files 40 documented · 25%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

🧭 Service Tracker Dashboard (STD)

Python Flask SQLite License

A homelab-scale Flask dashboard that tracks Docker services across multiple hosts, runs background URL health checks, and renders a small set of dashboard views (full, tiled, compact). Service entries are populated and kept current by a sidecar process — typically docker-api-notifier — posting to STD's register endpoint, but you can also add entries by hand through the web UI or directly via API.

v0.6.0 removes the legacy /api/register endpoint. All registers must use /api/v1/register with canonical key names. If you're running docker-api-notifier, v0.3.0 or later is required for basic registration; v0.4.0 is the paired release that populates the new network / port / exposure-observation fields.


Table of Contents

  1. What It Does
  2. Screenshots
  3. Configuration
  4. Docker Compose Setup
  5. Database Migrations
  6. API: Registering Services
  7. Container Labels (via docker-api-notifier)
  8. Files and Paths
  9. Health Checks
  10. Routes
  11. Widgets
  12. Behavior Notes
  13. Versioning & Releases

What It Does

  • Three dashboard views (table, tiled, compact). Tiled view shows each service as a tile with a per-tile expand drawer for full host, URL, Docker, network, port, exposure, and widget detail. Icons use Tabler Icons v3.34.0 (loaded via CDN). Clicking the chart icon on a tile opens the drawer in widget-only mode (showing just the metric cards); the chevron opens the full drawer. The two modes switch in-place without closing and reopening.
  • Dashboard view controls (v0.6.0+). A Group by axis selector (group / stack / host) and a Show URL-less filter render above the service grid on all three views. State is URL-driven, so dashboards stay bookmarkable.
  • Internal + external URL health checks on a configurable interval.
  • Auto-downloads container icons from Homarr Labs Dashboard Icons.
  • Register endpoint for pushing container metadata from external tools.
  • SQLite backing store, file-based, no external DB server.
  • Daily YAML backups with retention.
  • Manual add / edit / delete through the web UI. Tiled tiles and Dashboard rows have a one-click trash icon with an inline confirm popover. Static (Locked) entries show a lock icon and can only be deleted from the edit page.
  • Optional Dozzle log link integration.
  • Local user accounts with session-based auth.
  • Per-entry sort priority within groups; grouping and group sort.
  • Pluggable widget system (Sonarr, Radarr, Bazarr, Overseerr, Prowlarr, Syncthing today; more under widgets/).
  • Exposure interpreter (v0.6.0+). Reads structured exposure observations from the notifier's YAML interpreters (Traefik, Dockflare, etc.) and synthesizes internalurl / externalurl without requiring operator-written dockernotifier.std.* labels. Operator UI edits and explicit labels always win over synthesized values.

Screenshots

Main Dashboard Tiled View Compact View
Mobile View Widgets Settings

Configuration

STD reads config from environment variables and an optional /config/settings.yml. Environment variables take priority. If settings.yml is missing, defaults apply and a settings.example.yml is dropped into /config for reference.

Default login: admin / changeme123. Change it on first run.

Settings reference

Setting Type ENV Default What it does
api_token string API_TOKEN Bearer token required by /api/v1/register.
std_dozzle_url string STD_DOZZLE_URL Optional link to a Dozzle instance; enables a Tools section in the UI.
backup_path string BACKUP_PATH /config/backups Where YAML backups are written.
backup_days_to_keep int BACKUP_DAYS_TO_KEEP 7 Backup retention.
url_healthcheck_interval int URL_HEALTHCHECK_INTERVAL 300 Seconds between health check passes.
widget_background_reload int WIDGET_BACKGROUND_RELOAD 900 Seconds between widget data refreshes.
widget_value_retention_days int WIDGET_VALUE_RETENTION_DAYS 30 Days of widget_value history to retain. A daily 00:15 background job prunes older rows.
register_field_ownership string REGISTER_FIELD_OWNERSHIP user_wins How register calls handle conflicts with UI edits on group_name and sort_priority. user_wins (default) preserves non-NULL UI values on update; notifier_wins always overwrites. Invalid values fall back to user_wins with a startup warning.
user_session_length int USER_SESSION_LENGTH 120 User session length in minutes.
flask_secret_key string FLASK_SECRET_KEY Required for production. Used to sign session cookies.

Example settings.yml

api_token: supersecrettoken
std_dozzle_url: http://dozzle.local
backup_path: /config/backups
backup_days_to_keep: 7
url_healthcheck_interval: 300
widget_background_reload: 900
widget_value_retention_days: 30
register_field_ownership: user_wins
user_session_length: 120

Docker Compose Setup

services:
  service-tracker-dashboard:
    image: crzykidd/service-tracker-dashboard:latest
    container_name: service-tracker-dashboard
    ports:
      - 8815:8815
    environment:
      - API_TOKEN=supersecrettoken
      - STD_DOZZLE_URL=http://dozzle.local
      - FLASK_DEBUG=0
      - FLASK_SECRET_KEY=changeme-in-prod
    volumes:
      - ./config:/config
    restart: unless-stopped

Database Migrations

Schema changes are tracked with Alembic. Migrations live in alembic/versions/ and alembic upgrade head runs automatically on container start (see entrypoint.sh).

To create a new migration after changing models, exec into a running container:

docker compose exec <service-name> \
  alembic revision --autogenerate -m "describe the change"

The new revision file lands under alembic/versions/. Review it before committing — autogenerate doesn't catch every kind of change (constraint renames, default-only changes, SQLite-specific quirks).

To check whether models and the live database have diverged without writing a migration:

docker compose exec <service-name> alembic check

API: Registering Services

/api/v1/register is the only register endpoint. The legacy /api/register shim that bridged v0.4.x producers through v0.5.0 was removed in v0.6.0; producers must run docker-api-notifier v0.3.0+ or send canonical-key payloads directly.

Endpoint

POST /api/v1/register
Authorization: Bearer <API_TOKEN>
Content-Type: application/json

Canonical payload

{
  "host": "docker01",
  "container_name": "nginx",
  "container_id": "abc123...",
  "image_name": "ghcr.io/user/nginx:latest",
  "image_icon": "nginx.svg",
  "docker_status": "running",
  "stack_name": "frontend",
  "started_at": "2026-05-10T12:34:56Z",
  "timestamp": "2026-05-10T12:35:00Z",
  "internalurl": "http://nginx:80",
  "externalurl": "https://my.domain.com",
  "internal_health_check_enabled": true,
  "external_health_check_enabled": true,
  "group_name": "web",
  "sort_priority": 1,
  "networks": [
    {"name": "proxy", "aliases": ["frontend"]},
    {"name": "ammoledger_default", "aliases": ["frontend"]}
  ],
  "exposed_ports": ["5173/tcp"],
  "published_ports": [
    {"container_port": 5173, "protocol": "tcp",
     "host_ip": "0.0.0.0", "host_port": 8080}
  ],
  "exposure_observations": [
    {"layer": "traefik", "hostname": "nginx.internal.example",
     "tls": true, "path_prefix": null, "auth": null, "details": null},
    {"layer": "dockflare", "hostname": "nginx.example.com",
     "tls": true, "path_prefix": null,
     "auth": "cloudflare_access:authenticate", "details": null}
  ]
}

Required fields

  • host — Docker host name. Composite key with container_name.
  • container_name — name of the container.

Everything else is optional. STD applies sensible defaults for any field that's missing.

/api/v1/register is strict — unknown keys are rejected with a 400 and the list of offending keys in the response body. Nested structures (networks, published_ports) are also validated via pydantic; malformed entries are rejected at the schema boundary.

Field ownership: user_wins vs notifier_wins

group_name and sort_priority can be edited in the web UI. When a notifier register arrives for a row whose UI value differs, the register_field_ownership setting decides who wins:

  • user_wins (default) — UI edits stick. The notifier may populate these fields on a new row or one where they're still NULL, but it won't overwrite a value the operator has set.
  • notifier_wins — every register call overwrites everything, including UI edits. Choose this if your container labels are the source of truth.

Regardless of mode, STD records what the notifier sent in notifier_reported_group_name and notifier_reported_sort_priority columns. These columns aren't surfaced in the UI yet — they support a planned overridden-labels export.

Network and ports capture (v0.6.0+)

The three optional networks / exposed_ports / published_ports fields are pure observation — STD overwrites them on every register call with no ownership semantics. They're populated by docker-api-notifier v0.4.0+; pre-v0.4.0 notifiers continue to work and just leave the columns NULL.

The read-only "Reported by notifier" block at the bottom of /edit/<id> surfaces this data. Empty / NULL columns render as "Not reported." Static entries (created manually via the UI) won't have this data either, and that's expected.

Exposure observations + URL synthesis (v0.6.0+)

The optional exposure_observations field carries structured output from the notifier's YAML interpreters (Traefik, Dockflare, …). Notifier v0.4.0+ runs these interpreters and emits one entry per interpreter that recognizes the container. STD writes them into the new service_exposure table and runs a synthesizer that may populate internalurl / externalurl automatically.

Operators tell STD what each layer means at /settingsExposure tab. Each discovered layer (only layers that have been observed appear) gets a direction dropdown:

  • internal — synthesized hostname populates internalurl.
  • external — synthesized hostname populates externalurl.
  • neither (default) — STD records the observation and shows a badge, but doesn't synthesize a URL from this layer.

Per-host overrides let you say "Traefik on the home host is internal, but Traefik on the edge VPS is external" without writing different YAML per host.

URL provenance

The two internalurl / externalurl columns now track who last wrote them via two new _source columns. Ordering, strongest last:

  1. NULL — no value, no opinion. Synthesizer is free to fill in.
  2. synthesized — value came from the synthesizer. Re-runs on every register and on settings save may change it.
  3. explicit_label — value came from a dockernotifier.std.internalurl / .externalurl label on the container. Synthesizer won't overwrite. A new explicit label updates it; a UI edit overrides.
  4. ui_edit — operator typed a value in the web UI. Nothing overrides until another UI edit. Clearing the field resets the source to NULL so synthesis can resume.

The edit page (/edit/<id>) shows a small badge next to each URL indicating the current source.

Wire-level semantics

  • exposure_observations absent or null — "no update." Existing exposure rows for the service are preserved. This is the state for pre-v0.4.0 notifiers and any producer that doesn't emit interpreter output.
  • exposure_observations: [] — "this container has no interpreter matches." All existing exposure rows for the service are cleared. The synthesizer may then clear any URL whose source is synthesized.
  • exposure_observations: [ ... ] — wholesale replacement. All prior rows for this service are dropped, the new rows are inserted, and the

Core symbols most depended-on inside this repo

Shape

Function 120
Route 23
Class 11
Method 8

Languages

Python85%
TypeScript15%

Modules by API surface

routes_dashboard.py33 symbols
static/js/dashboard.js24 symbols
routes_auth.py15 symbols
models.py15 symbols
view_helpers.py8 symbols
settings_store.py8 symbols
synthesizer.py7 symbols
app.py7 symbols
jobs.py6 symbols
schemas.py4 symbols
routes_api.py4 symbols
image_utils.py3 symbols

For agents

$ claude mcp add service-tracker-dashboard \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page