From 338e1c2adf021ab1f7592c09ab3096fd97e2031f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Mon, 17 Aug 2026 14:46:41 +0200 Subject: [PATCH] docs: replace boilerplate README with real documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit create-next-app's stock README (npm/yarn/bun instructions, Geist font mention, Vercel deploy link) never got replaced. Documents what PulseNode actually is: quick start, the config.yml/​.env split and hot- reload behavior, a reference table for all seven widget types and their fields, auto-discovery, the docker-compose deployment (including the DOCKER_GID gotcha found while testing M5), project structure, and the known gaps (no auth yet, system widget reflects container cgroup unless the host proc/sys are mounted in). --- README.md | 160 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 140 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index e215bc4..e4c4a3e 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,156 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# PulseNode -## Getting Started +A self-hosted infrastructure dashboard: system stats, Docker containers, +databases, and HTTP/Traefik health, configured entirely through a YAML file +and pushed to the browser live over WebSockets. No database of its own - +`config.yml` is the source of truth, metrics are ephemeral. -First, run the development server: +## Features + +- **YAML-configured** widgets and layout, hot-reloaded - edit `config.yml` + and connected browsers update without a page refresh +- **Secrets via `.env`**, interpolated into `config.yml` at load time + (`${VAR}` / `${VAR:-default}`), never sent to the browser +- **Live updates over WebSocket** - each widget subscribes to its own topic; + the server only serializes results for widgets someone is actually viewing +- **Widget types**: Docker containers, Postgres/Redis instances, system + resources, HTTP health checks, Traefik router status, bookmarks, search +- **Optional label-based auto-discovery** - containers carrying + `traefik.enable=true` can be turned into dashboard widgets automatically; + manual `config.yml` entries always take precedence +- **CSS-variable theming**, light/dark toggle, `theme.customCssPath` for a + fully custom stylesheet +- **Ships as a single Docker image**, designed to run non-root with a + read-only root filesystem + +## Quick start (local development) ```bash -npm run dev -# or -yarn dev -# or +pnpm install +cp config/.env.example config/.env # fill in real values +cp .env.example .env # only needed for docker compose pnpm dev -# or -bun dev ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +Open `http://localhost:3000`. `pnpm dev` runs the custom server +(`server.ts`) via `tsx watch`, which boots Next.js, the WebSocket server, and +the collector scheduler together, and restarts on server-side code changes. +Editing `config.yml` itself does **not** restart the process - it's picked +up live via the config watcher. -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +Other scripts: `pnpm build` (production build), `pnpm start` (run the +production build via the same custom server), `pnpm lint`, `pnpm exec tsc +--noEmit`. -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +## Configuration -## Learn More +### `config/config.yml` -To learn more about Next.js, take a look at the following resources: +Everything is organized into `groups`, each a list of `widgets`: -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +```yaml +settings: + title: PulseNode -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +theme: + mode: dark # dark | light | auto + variables: + --pn-accent: "#38bdf8" + # customCssPath: custom.css # relative to config/, served at /api/theme/custom-css -## Deploy on Vercel +discovery: + docker: + enabled: false # set true to auto-discover traefik-labeled containers + network: falcon_network # only include containers on this docker network -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +groups: + - name: Core Infra + widgets: + - type: docker + name: Traefik + containerName: traefik + href: https://traefik.example.com + interval: 10s +``` -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +Config errors are **non-destructive**: if `config.yml` fails validation +after a live edit, PulseNode logs the error and keeps serving the last +valid config instead of crashing. + +### `config/.env` + +Secrets and per-deployment values referenced from `config.yml` via +`${VAR}` or `${VAR:-default}`. Loaded once at startup and on every config +reload; never exposed to the client. See `config/.env.example`. + +### Widget types + +| `type` | Required fields | Notes | +|------------|-------------------------------------|-------| +| `bookmark` | `name`, `href` | Static link card. Optional `description`. | +| `search` | - | Client-side search box. Optional `engines` (`duckduckgo`/`google`/`bing`, default `[duckduckgo]`), `defaultEngine`. | +| `docker` | `name`, `containerName` | Status, uptime, CPU/mem, restart count via `dockerode`. Optional `href`, `showStats` (default `true`), `interval` (default `5s`). | +| `database` | `name`, `containerName`, `engine` | Thin skin over the same Docker collector - `engine` is `postgres` or `redis`, used for icon/label only. `interval` default `10s`. | +| `system` | - | Host CPU/mem/disk/network via `systeminformation`. Optional `name` (default `System`), `interval` (default `5s`). | +| `http` | `name`, `url` | HTTP health check with latency and consecutive-failure tracking. Optional `method` (default `GET`), `timeout` (default `5s`), `interval` (default `30s`), `expect.status` (default `200`). | +| `traefik` | `apiUrl` | Router/entrypoint/middleware status from Traefik's own API (`--api.dashboard=true` must be enabled). Optional `name` (default `Traefik`), `interval` (default `15s`). | + +`interval`/`timeout` values are duration strings: `500ms`, `5s`, `1m`, `1h`. + +### Auto-discovery + +With `discovery.docker.enabled: true`, PulseNode scans running containers +every 60s (and immediately on any `config.yml` change) for +`traefik.enable=true` labels, parses the router's `Host(...)` rule for a +link, and adds anything not already listed manually to a synthetic +"Discovered" group. `discovery.docker.excludeLabels` and `.network` narrow +what qualifies; `.labelPrefix` (default `traefik`) controls which label +namespace is read. + +## Deployment + +```bash +cp .env.example .env +cp config/.env.example config/.env +# edit both, then: +docker compose up -d --build +``` + +- `.env` (repo root) holds `TRAEFIK_HOST`, `NETWORK_NAME`, and `DOCKER_GID` - + compose-level values, interpolated into `docker-compose.yml`'s Traefik + labels and used to join the container to the docker group. Find your + host's docker group id with `getent group docker | cut -d: -f3`; without + it the non-root container gets `EACCES` on `/var/run/docker.sock`. +- `config/.env` holds values referenced from `config.yml` (see above). +- The compose file joins the container to an **external** `falcon_network` + so collectors can reach services directly by container name + (`http://umami:3000/...`) instead of only through Traefik. +- `/var/run/docker.sock` is mounted read-only; `/app/config` is mounted + read-only from `./config`. +- The container runs as a non-root user with a read-only root filesystem, + dropped capabilities, and `tini` as PID 1. +- `/api/health` backs the container `HEALTHCHECK`. + +## Project structure + +``` +config/ config.yml + .env (gitignored) live here +server.ts custom server: http + WebSocket (/ws) + collector scheduler +app/ Next.js App Router pages and API routes +components/widgets/ one folder per widget type (Widget.tsx + shared Skeleton/StatusDot) +components/layout/ Dashboard shell, theme toggle, brand mark +lib/config/ schema (zod), loader (parse/interpolate/watch), effective (+ discovery merge) +lib/collectors/ docker, system, http, traefik collectors + the scheduler +lib/discovery/ traefik-label auto-discovery +lib/ws/ WebSocket server (topics) and the browser-side subscription hooks +``` + +## Known limitations + +- **No authentication yet.** PulseNode assumes it's reachable only on a + private network (e.g. behind Traefik with no public route, or on a VPN). + A minimal session-based auth gate is a planned follow-up, not yet built. +- `system` widget metrics reflect whatever cgroup/namespace the container + runs in - for true host-level stats rather than the container's own view, + mount `/proc` and `/sys` from the host and run with `pid: host` (not + configured by default).