Collector failures, service-API errors, WS connection lifecycle, and
config reload/discovery events were previously invisible outside the
browser (or, for a few config/discovery cases, logged with an
inconsistent ad-hoc console.error). Add a small scoped logger
(lib/logger.ts, level via LOG_LEVEL) and wire it through server.ts,
the scheduler, service collector, config loader/effective store, and
the WS server so operators can see failures via `docker logs`.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
One new `service` widget type (nested discriminated union on `service`)
rather than six, so a single config entry shows both docker container
health and a service-specific stat (repo count, project list, photo
count, workflow count, active users, users/nodes) - avoiding the
overhead of configuring a docker widget and a separate service widget
per container. All six collectors hit the service's container name +
internal port directly on falcon_network, the same container-to-
container pattern just proven out for Traefik's own API, avoiding
vpn-only/hairpin-NAT entirely.
Also adds the public/private config split that was scoped in the
original project plan but never built: lib/config/public.ts strips
apiToken/apiKey/password fields before the config reaches the browser
via SSR or the WS config topic - required before any widget could
carry a real secret. Verified via a throwaway secret field that it's
absent from both the SSR HTML and the WS config:update frame.
Endpoint shapes verified live against the running gitea/coolify/immich/
n8n/umami/headscale containers before committing (unauthenticated
requests correctly 401/200 on every target route; gitea's
X-Total-Count header confirmed present).
Imports the visual direction from a claude.ai/design session ("PulseNode
Dashboard.dc.html") and reconciles it with the app's existing choices
rather than a wholesale swap: kept the IBM Plex Sans/Mono + Space
Grotesk typography and the pulse-green accent concept from the earlier
pass, adopted the imported design's structural ideas - a real
tonal/elevation token system (derived divider/muted colors via
color-mix instead of hardcoded per-theme duplicates, actual box-shadow
elevation), a 12-column bento grid with per-widget-type column spans
instead of uniform equal-width cards, fading-edge section dividers,
and a shared WidgetCard/MetricBar/StatusTag vocabulary so every widget
type stops repeating its own card markup.
Icons come from @phosphor-icons/react's /ssr entrypoint (bundled at
build time) rather than the imported design's unpkg.com CDN script -
that script is fine for the standalone design-tool preview, but a
runtime third-party call would break the self-hosted-only principle
already established (fonts self-hosted via next/font, no external
requests). New app/icon.svg reuses the same nav badge mark as the
favicon, replacing the never-touched create-next-app default.
Two new pieces of real functionality prompted by the design's mockup
toast/live-indicator, not just decoration: a "live" WebSocket
connection-status indicator (lib/ws/client.ts now tracks and exposes
real connection state), and a toast that fires on actual config:update
and config:error events - the latter finally surfaces config validation
failures in the browser, previously visible only in server logs despite
being designed for exactly this back in M1.
Adds the remaining monitor types: a systeminformation-backed system
widget (single-flight cached so multiple widgets don't force
concurrent samples), an http widget with per-widget consecutive-
failure tracking, and a traefik widget that reads router/entrypoint/
middleware status from Traefik's own API. Also adds opt-in label-based
docker auto-discovery (discovery.docker in config.yml): containers
carrying traefik.enable=true are turned into docker widgets in a
synthetic "Discovered" group, re-scanned on a timer and whenever
config.yml changes, with manual widgets always taking precedence over
a discovered one for the same container.
Fixes a real bug surfaced while testing discovery: Next's App Router
bundles app/** through its own compiler pass, separate from server.ts
(run directly via tsx), so configStore/effectiveConfigStore were
silently instantiated twice - one instance watched and updated by
server.ts, another frozen instance read by SSR. Live config edits were
never reflected on page load without a full process restart. Both
stores now key their singleton off globalThis, which both module
graphs share within the same process.
Verified against a real Traefik v3 container (API-driven router list,
ping-based http check) and a labeled nginx container (auto-discovery),
including that a config.yml edit now shows up in a fresh page load
without restarting the server, in both dev and the production build.
Adds a custom server.ts (http server + Next request handler + a
noServer:true WebSocket server on /ws) so the app can push live data
without polling. A CollectorScheduler runs one interval-based job per
docker/database widget instance, backed by dockerode against
/var/run/docker.sock, and reconciles jobs when config.yml changes.
Config hot-reload is now fully wired: chokidar watches config.yml/.env,
re-validates on change, and broadcasts config:update (or a
non-destructive config:error, keeping the last-good config) to every
connected browser. The client subscribes to per-widget topics over a
single shared WebSocket with exponential-backoff reconnect and
last-result caching for instant resubscribe.
Verified end-to-end against real throwaway containers (nginx, redis):
live CPU/mem/uptime/health streamed over the socket, interval changes
picked up without a server restart, and a broken config.yml correctly
falls back to the last-valid config instead of crashing the app.