Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
158 lines
6.4 KiB
Markdown
158 lines
6.4 KiB
Markdown
# 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.
|
||
|
||

|
||
|
||
## 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
|
||
```
|