5 Commits
Author SHA1 Message Date
valknar e14fbc9205 feat: streamline traefik widget with icon/status dot, fix grid gap
CI / Static checks (push) Successful in 36s
CI / Build and push image (push) Successful in 1m9s
Traefik's widget was the only card without a BrandIcon or StatusDot in
its header, and had no href either - added icon/href fields to the
schema and matched the exact docker/service widget header markup.
StatusDot reuses the same "running once data loaded" convention the
other widgets already use for a non-container up/down signal.

System's widget was span=5 while every other widget in this app is
span=4 (docker/service/traefik) or 3 (http) - on a 12-column grid that
left a permanent 3-column gap next to it every row. Matched it to
span=4 so system+traefik+one more widget tile a full row exactly.
Also added grid-auto-flow: dense as a general safety net against
future span-mismatch gaps.
2026-08-17 21:23:01 +02:00
valknar 4308169608 feat: brand icons for widgets, fix stale accent color
CI / Static checks (push) Successful in 34s
CI / Build and push image (push) Skipped
Two separate issues, both real bugs:

- Dark mode was showing light blue instead of green: config.yml still
  had a leftover --pn-accent: #38bdf8 override from before the design
  redesign, which won via the runtime theme injector regardless of
  what app/globals.css defaulted to. Removed the override and pushed
  the dark-mode default from a soft mint (#5ee08c) to an actual neon
  green (#39ff88) - closer to what "neon" means and closer to the real
  phosphor-green a cardiac monitor trace uses, which fits the pulse
  metaphor better than the pastel version did.

- No per-service brand icons anywhere. Added an optional `icon` field
  to docker/bookmark widgets (a key into lib/brand-icons.ts) rather
  than guessing from containerName, since a heuristic would silently
  misfire for anyone's own container naming. database widgets derive
  their icon automatically from `engine` (postgres/redis) since that's
  already required and unambiguous. Icon path data comes from the CC0
  simple-icons package via scripts/extract-brand-icons.py, extracted
  and committed as plain TS data (lib/brand-icons.ts) rather than
  imported live - keeps it self-hosted (no unpkg.com/CDN calls at
  runtime, matching the fonts) and pinned regardless of dependency
  updates. Rendered monochrome (currentColor, not each brand's own
  hex) to stay consistent with the single-accent design rather than a
  dozen competing colors - only the shape carries the identity.

Wired real icons into config.yml for every service that has one:
traefik, coolify, gitea (+ gitea_runner), immich (+ immich_ml), n8n,
passbolt, umami, headscale, plus postgresql/redis on every database
widget. code and mailpit have no brand match in the curated set, so
they render without one rather than a wrong/generic substitute.

Verified in the actual rendered page: compiled CSS confirms
--pn-accent is #39ff88 (dark) / #339a5a (light) with no trace of the
old #38bdf8, and all 18 expected icon instances (10 docker + 8
database widgets) are present.
2026-08-17 17:55:27 +02:00
valknar e1e571754a feat: apply imported design (bento grid, real elevation tokens, live indicator)
CI / Static checks (push) Successful in 33s
CI / Build and push image (push) Skipped
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.
2026-08-17 17:21:18 +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 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