diff --git a/README.md b/README.md new file mode 100644 index 0000000..965077d --- /dev/null +++ b/README.md @@ -0,0 +1,157 @@ +# Dash — uptime observatory + +A self-hosted uptime monitoring dashboard in a single binary. A Go check +engine with SQLite persistence streams live status over SSE into a +hand-built React frontend: three bespoke SVG circular visualizations with +spring physics, full RTL (Farsi) support, and a shareable public status mode. + +![Dash](docs/dashboard.png) + +## What it does + +- **Five check types** — HTTP (status + latency), TCP port, TLS certificate + expiry (days remaining), ICMP ping, and Docker (container running + + healthcheck status via the daemon socket). Each target runs on its own + interval. +- **Flap detection** — N consecutive failures before a target is marked + down, M consecutive successes before it recovers. No alert noise from a + single dropped packet. +- **Incident grouping** — one incident spans the entire down period, from + the first failed check of the streak until recovery. +- **Live stream** — `/api/stream` pushes every probe and every confirmed + state transition over Server-Sent Events. The UI never polls. +- **Every result persisted** — timestamp, latency, status, error string in + SQLite (pure Go driver, no CGO), pruned after `history_days`. + +## The frontend + +Three circular instruments, all hand-built SVG + Framer Motion (no charting +libraries): + +1. **Status rings** — one per target. The outer arc fills to the uptime + percentage over the selected window (24H/7D) and springs to every new + value; the inner brass gauge encodes latency on a log scale (1ms–2.5s); + each completed probe lands as a visible pulse; confirmed state changes + ripple outward. Click a ring and it travels into a detail view (shared + layout transition) with a latency sparkline, a per-target day dial, and + recent incidents. +2. **Orbital overview** — every target orbits the system core at + Kepler-flavored speeds (inner orbits run faster). Healthy bodies drift + calmly with motion trails, degraded ones breathe, a down body breaks its + orbit outward and flares red over the scar of its old path. The core + carries fleet-wide uptime and a brass scanner hand. +3. **Day dial** — a 24-hour clock face. Each of the last 24 hours is one + segment at its true wall-clock position, colored by status. Hours + without data stay hairline-thin. A brass hand marks now. One glance, one + full day. + +Plus: a boot sequence where the instruments draw themselves in, staggered; +`prefers-reduced-motion` collapses all motion to fully-formed static +rendering; **فارسی / RTL** is a first-class layout mode (mirrored layout, +Vazirmatn type, Persian digits and the Persian solar calendar via `fa-IR`); +**dark and light themes** (persisted, both tuned variants of the same brass +/ ink design language); and a **Status** mode — the same instruments +stripped to a clean, shareable public status page. + +Continuous motion (the orbital field, the scanner hand) runs outside React +entirely — one animation-frame loop mutates SVG attributes directly, so the +page stays at full frame rate regardless of target count. No SVG filters, +no blend modes on hot layers. + +## Build + +Requires Go ≥ 1.22 and Node ≥ 20. The frontend must be built first because +the binary embeds it via `go:embed`. + +```sh +# 1. build the frontend into web/dist +cd web && npm install && npm run build && cd .. + +# 2. compile the single binary (web/dist is embedded) +go build -o dash . +``` + +The result is one self-contained file, `dash`. Copy it and a `targets.yaml` +to any Linux/macOS/Windows box — nothing else to install. + +## Run + +```sh +./dash # uses ./targets.yaml, ./dash.db, :8080 +./dash -config /etc/dash/targets.yaml -db /var/lib/dash.db -listen :9090 +``` + +Open http://localhost:8080. + +> **ICMP checks** need raw-socket privileges. Run as root, or grant the +> binary `CAP_NET_RAW` (`setcap cap_net_raw+ep ./dash`), or allow +> unprivileged ping (`sysctl net.ipv4.ping_group_range="0 2147483647"`). +> Other check types work without any privileges. + +## Configuration — targets.yaml + +```yaml +settings: + listen: ":8080" # bind address + db: "dash.db" # sqlite file + history_days: 30 # raw check retention + +targets: + - id: example-web # unique slug (required) + name: "Example Website" + type: http # http | tcp | tls | icmp + url: https://example.com # http → url, tcp/tls → address, icmp → host + interval: 15s # time between checks (default 30s) + timeout: 5s # per-check timeout (default 5s) + fail_threshold: 3 # consecutive fails → down (default 3) + recover_threshold: 2 # consecutive oks → up (default 2) + expect_status: 200 # http only: require exact status + degraded_latency_ms: 1500 # mark degraded above this latency + + - id: github-tls + name: "GitHub TLS" + type: tls + address: github.com:443 + interval: 5m + warn_days: 21 # tls only: degraded when cert expires sooner + + - id: my-container + name: "My Container" + type: docker + container: my-container # docker container name + interval: 30s + # socket: /var/run/docker.sock (default) +``` + +> **Docker checks** read the daemon socket. Run the binary as a user in the +> `docker` group (or `SupplementaryGroups=docker` in the systemd unit). A +> container is *up* when running, *degraded* while its healthcheck reports +> `starting`, and failing when stopped or `unhealthy`. + +## API + +| Endpoint | Description | +| --- | --- | +| `GET /api/targets` | Current state of all targets: flap-filtered status, last check, 24h/7d uptime, latency percentiles, open incident | +| `GET /api/targets/{id}/history?range=24h` | Time series: downsampled points plus 24 aggregate buckets (`range`: `30m`, `24h`, `7d`, …) | +| `GET /api/incidents?range=7d` | Grouped incidents, ongoing first | +| `GET /api/stream` | SSE: `snapshot` on connect, then `check` per probe and `state` per confirmed transition | + +## Development + +```sh +go run . & # backend on :8080 +cd web && npm run dev # vite on :5173, proxies /api → :8080 +``` + +## Layout + +``` +main.go, webfs.go entrypoint + embedded frontend +internal/config targets.yaml parsing, defaults, validation +internal/checker the four probe implementations +internal/engine scheduler, flap detection, incidents, SSE hub +internal/store sqlite persistence (modernc.org/sqlite, no CGO) +internal/api REST + SSE handlers +web/ React + TypeScript + Vite + Tailwind v4 + Motion +```