Commit Graph
6 Commits
Author SHA1 Message Date
valknar 1e7bfd8250 feat: dogfood config.yml against the real stacks repo
Replaces the placeholder bookmark links with real docker/database
widgets for every container in the 10 Projekte/stacks compose files
(traefik, coolify+redis+db, gitea+runner+db, code, immich+ml+redis+db,
n8n+db, passbolt+db, umami+db, headscale, mailpit) - the actual
"widgets for all services in Projekte/stacks" deliverable, not just an
example. Also turns on label-based discovery against falcon_network as
a safety net for anything added later without a config.yml update;
manual widgets here still take precedence for anything already listed.

Verified the config validates and renders with no schema errors, and
that a widget pointed at a container that doesn't exist in this
environment fails gracefully (a clean error result over the socket)
rather than crashing - expected here since none of the real stacks run
in this sandbox; wiring against the live falcon_network happens at
actual deploy time.
2026-08-17 14:42:25 +02:00
valknar 0140960df6 feat: Docker deployment (M5)
Multi-stage Dockerfile (deps -> build -> prod-deps -> runtime) that
ships a full production node_modules rather than Next's standalone
output, since standalone tracing is incompatible with a custom server
(noted back in M1). Runs as a non-root user with a read-only rootfs,
dropped capabilities, and tini as PID 1. tsx moves from dev to a real
runtime dependency since the production start script runs server.ts
directly rather than a precompiled bundle. next.config.ts marks
dockerode/systeminformation as serverExternalPackages so Next's
bundler leaves their OS-conditional requires alone.

docker-compose.yml mirrors the sibling stacks' own conventions
(TRAEFIK_HOST/NETWORK_NAME in .env, falcon_network as an external
network, the same traefik.* label shape) so it fits their existing
tooling, plus a new /api/health route and healthcheck.mjs for the
container HEALTHCHECK.

Verified end-to-end with a real `docker build` + `docker compose up`:
non-root/read-only/cap-dropped container boots cleanly, is reachable
by container name from another container on the shared network (as
Traefik would reach it), and the container's own HEALTHCHECK reports
healthy. That run surfaced a real gap - the non-root user got EACCES
on /var/run/docker.sock, since it's owned by root:docker on the host -
fixed via group_add on a DOCKER_GID env var (documented in .env.example
with the command to find it), then re-verified that both docker.sock
access and label-based auto-discovery work correctly under the fix.
2026-08-17 14:39:44 +02:00
valknar 1e550319aa feat: visual identity, theme toggle, custom CSS, loading states (M4)
Grounds the visual design in the product's own subject matter (vital-
signs monitoring) rather than generic dark-mode defaults: a signature
"pulse" green tied to healthy status (the accent color and the "up"
status color are the same hue - a steady green pulse reads as
healthy, same convention as a cardiac monitor), a small ECG-trace
brand mark, and a restrained pulsing-glow animation on healthy status
dots (prefers-reduced-motion respected) as the one deliberate motion
touch. Typography moves off the bare system-ui stack: IBM Plex Sans
for UI text, IBM Plex Mono with tabular numerals for metric values
(CPU/mem/uptime/latency), Space Grotesk used once for the wordmark -
chosen partly for IBM Plex's own systems-monitoring heritage.

Functional additions:
- Light/dark toggle, independent of config.yml's theme.mode, persisted
  to localStorage with a blocking inline script to avoid a flash of
  the wrong theme on load; auto mode now genuinely follows
  prefers-color-scheme instead of hardcoding dark.
- theme.customCssPath support via a new route handler that serves a
  user-mounted CSS file at runtime (can't be a build-time import,
  hence the targeted no-css-tags lint suppression), with a path-
  traversal guard since it's still reading from disk on every request.
- Loading-skeleton state for widgets awaiting their first WebSocket
  result, distinct from both the error state and genuine no-data.

Verified visually in Chrome: theme toggle switches instantly in both
directions, a live docker widget's status dot and monospace metrics
render correctly in both palettes, and config hot-reload still adds a
new widget without a page refresh.
2026-08-17 14:27:07 +02:00
valknar 847c4be26b feat: system/http/traefik collectors and docker auto-discovery (M3)
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.
2026-08-17 14:17:24 +02:00
valknar 9890f964c5 feat: live docker/database widgets over WebSocket (M2)
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.
2026-08-17 14:01:53 +02:00
valknar 80c2362ac2 feat: scaffold PulseNode dashboard (M1)
Next.js 16 + TypeScript + Tailwind v4 app with a YAML-driven config
system: schema validation (zod), .env interpolation, and a widget
registry rendering bookmark/search/group widgets. CSS custom
properties (--pn-*) drive theming and are overridable from
config.yml's theme.variables. Config load errors surface as a
readable error page instead of crashing the app.

Hot reload, docker/system/http collectors, and the WebSocket push
layer land in later milestones per the approved plan.
2026-08-17 13:49:55 +02:00