diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..f6b3c59 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,31 @@ +name: Bug report +description: Something is broken +labels: [bug] +body: + - type: textarea + id: what + attributes: + label: What happened + description: What you did, what you expected, what happened instead. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Logs + description: Relevant server logs or browser console output. + render: text + - type: input + id: version + attributes: + label: Dash version + description: Image tag or release version. + placeholder: v0.7.0 + - type: dropdown + id: install + attributes: + label: Install method + options: + - Docker + - Binary + - Built from source diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..01c8ed4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,16 @@ +name: Feature request +description: Suggest an addition +labels: [enhancement] +body: + - type: textarea + id: problem + attributes: + label: What problem does this solve + description: Concrete use case, not just "add X". + validations: + required: true + - type: textarea + id: idea + attributes: + label: Proposed shape + description: Optional. How it might look or work. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..fd5c399 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,11 @@ +## Summary + + + +## Test plan + +- [ ] `npm run typecheck` + `npm run lint` + `npm run build` green +- [ ] `go build ./...` + `go vet ./...` + `go test ./...` green +- [ ] Smoke-tested the affected flow manually + + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8e426d7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,71 @@ +name: release + +on: + push: + tags: ["v*"] + +permissions: + contents: write + packages: write + +jobs: + binaries: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + - run: npm ci + - run: npm run build + + # go:embed all:dist reads internal/web/dist at compile time. + - run: rm -rf apps/backend/internal/web/dist && cp -r apps/frontend/dist apps/backend/internal/web/dist + + - uses: actions/setup-go@v5 + with: + go-version-file: apps/backend/go.mod + cache-dependency-path: apps/backend/go.sum + + - uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: "~> v2" + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + docker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: docker/metadata-action@v5 + id: meta + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest + + - uses: docker/build-push-action@v6 + with: + context: . + file: infra/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..5810939 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,55 @@ +version: 2 + +project_name: dash + +before: + hooks: + # The embedded UI must be built and copied into internal/web/dist before + # any Go build. The release workflow does this; fail loudly here if the + # stub is still in place. + - sh -c 'test -d apps/backend/internal/web/dist/assets' + +builds: + - id: dash + dir: apps/backend + binary: dash + main: ./cmd/dash + env: [CGO_ENABLED=0] + goos: [linux, darwin, windows] + goarch: [amd64, arm64] + ldflags: ["-s", "-w", "-X main.version={{.Version}}"] + + - id: dash-agent + dir: apps/backend + binary: dash-agent + main: ./cmd/dash-agent + env: [CGO_ENABLED=0] + goos: [linux] + goarch: [amd64, arm64, arm] + goarm: ["7"] + ldflags: ["-s", "-w", "-X main.version={{.Version}}"] + +archives: + - id: dash + formats: [tar.gz] + name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}" + format_overrides: + - goos: windows + formats: [zip] + files: [README.md, LICENSE] + +checksum: + name_template: "checksums.txt" + +changelog: + use: github + sort: asc + filters: + exclude: + - "^docs:" + - "^chore:" + - "^test:" + +release: + footer: | + Docker: `docker pull ghcr.io/dvorinka/dash:{{ .Tag }}` diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..609579c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,87 @@ +# Changelog + +All notable changes to Dash. Format follows +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow +[Semantic Versioning](https://semver.org/). + +## [Unreleased] + +## [0.7.0] — 2026-09-21 + +### Added + +- Incidents with open/ack/resolve/close lifecycle and per-incident update + timeline; monitors auto-open and auto-resolve incidents on transitions +- Maintenance windows that suppress monitor alerts +- Public status pages at `/status/:slug` (unauthenticated, per-page monitor + and system selection) +- SVG badges: `/api/badge/monitor/:id.svg`, `/api/badge/domain/:id.svg` +- Prometheus metrics at `/api/metrics` +- CSV bulk import (`/api/import/csv`) and monitors/domains/systems in JSON + export +- Generic webhook notifications (`notify_webhook` setting): monitor up/down + and domain events, Slack/Discord/ntfy-compatible payload, `/api/notify/test` + +## [0.6.0] — 2026-09-21 + +### Added + +- System monitoring: `systems` + `system_stats`, bearer-token push ingest + (`/api/systems/ingest`), offline detection +- `dash-agent` binary: Linux collector for CPU, memory, swap, disk, network, + load, uptime, temperatures, and Docker container state +- Systems list and detail pages with Recharts graphs; system board widget + +## [0.5.0] — 2026-09-21 + +### Added + +- Domain watch: RDAP + TCP-WHOIS lookup, WHOIS parsing, DNS records + (NS/MX/TXT/A/AAAA), TLS certificate details, host geo, DNS/email/hosting/ + certificate provider detection +- Daily refresh scheduler, manual refresh, check history +- Domain list and detail pages, expiry and SSL countdowns, domain board widget + +## [0.4.0] — 2026-09-21 + +### Added + +- Uptime monitors: HTTP, TCP, ping, DNS, keyword, JSON-path, and push types; + interval/timeout/retries config, heartbeat history, 24h/30d uptime stats +- Monitor scheduler, check-now endpoint, push ingest (`/api/push/:token`) +- Monitor list and detail pages, monitor board widget, service-to-monitor + creation checkbox +- Importers: Homepage `services.yml`, Homarr JSON, Dashy `conf.yml` + +## [0.3.0] — 2026-09-17 + +### Added + +- Index and Console board renderers (four total), command palette (⌘K), + icon CDN suggestions + +## [0.2.0] — 2026-09-17 + +### Added + +- Widget layer: Go fetcher registry, config-field metadata, 30s TTL cache +- Pi-hole, AdGuard Home, Immich, and clock widgets; per-widget config UI + +## [0.1.0] — 2026-09-17 + +### Added + +- Board MVP: sections, items, multi-URL services, drag-drop, collapse state +- Bento and Cards renderers, dark/light themes, icon upload + letter fallback +- Server-side status pings with up/down chips +- Single Go binary serving the embedded React UI; SQLite via goose migrations; + Docker image on distroless + +[Unreleased]: https://github.com/Dvorinka/Dash/compare/v0.7.0...HEAD +[0.7.0]: https://github.com/Dvorinka/Dash/compare/v0.6.0...v0.7.0 +[0.6.0]: https://github.com/Dvorinka/Dash/compare/v0.5.0...v0.6.0 +[0.5.0]: https://github.com/Dvorinka/Dash/compare/v0.4.0...v0.5.0 +[0.4.0]: https://github.com/Dvorinka/Dash/compare/v0.3.0...v0.4.0 +[0.3.0]: https://github.com/Dvorinka/Dash/compare/v0.2.0...v0.3.0 +[0.2.0]: https://github.com/Dvorinka/Dash/compare/v0.1.0...v0.2.0 +[0.1.0]: https://github.com/Dvorinka/Dash/releases/tag/v0.1.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..4f1a414 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,38 @@ +# Code of Conduct + +This project follows the [Contributor Covenant](https://www.contributor-covenant.org/), +version 2.1. + +## Our pledge + +We pledge to make participation in this project a harassment-free experience +for everyone, regardless of age, body size, visible or invisible disability, +ethnicity, sex characteristics, gender identity and expression, level of +experience, education, socio-economic status, nationality, personal appearance, +race, religion, or sexual identity and orientation. + +## Our standards + +Examples of behavior that contributes to a positive environment: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes +- Focusing on what is best for the community + +Examples of unacceptable behavior: + +- The use of sexualized language or imagery, and sexual attention or advances +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement + +Instances of unacceptable behavior may be reported to the maintainer via +GitHub (@Dvorinka). All complaints will be reviewed and investigated promptly +and fairly. The full enforcement guidelines are at +https://www.contributor-covenant.org/version/2/1/code_of_conduct.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..25b858c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,58 @@ +# Contributing + +Thanks for considering a contribution. Dash is a small, deliberate codebase — +read [AGENTS.md](AGENTS.md) and [ROADMAP.md](ROADMAP.md) first; the roadmap is +canonical for what gets built and when. + +## Setup + +Requires Go 1.24+ and Node 20+. + +```sh +npm install +npm run generate # regenerate the TS client from openapi.yaml +npm run dev # vite dev server on :5173, proxies /api to :8080 +``` + +```sh +cd apps/backend +DASH_DEV=1 go run ./cmd/dash # API on :8080 +``` + +## Rules of the house + +- `openapi.yaml` is the API contract. Change it first, then + `npm run generate`. Never hand-write API types in the frontend. +- All schema changes go through goose migrations in + `apps/backend/internal/db/migrations` — `NNNN_name.sql` with `-- +goose Up` + and `-- +goose Down`. +- Board renderers share state, drag-drop, and popover primitives. Never + reimplement them per renderer. +- No auth, no YAML-as-source-of-truth, no external database. These are + non-goals for v1. +- Standard library before dependencies; existing helpers before new ones. + +## Before pushing + +```sh +npm run typecheck +npm run lint +npm run build + +cd apps/backend +go build ./... && go vet ./... && go test ./... +``` + +Plus a real smoke test of whatever you changed. All gates green or no push. + +## Commits and PRs + +- Conventional-ish subjects: `feat:`, `fix:`, `docs:`, `refactor:` — keep them + short and describe the why. +- Small, reviewable PRs. Fill in the template; a screenshot of UI changes is + worth more than a paragraph. + +## Reporting bugs / requesting features + +Use the issue templates. For security issues, see +[SECURITY.md](SECURITY.md) — do not open a public issue. diff --git a/README.md b/README.md index 6aefdcb..4b5185d 100644 --- a/README.md +++ b/README.md @@ -3,21 +3,69 @@ A self-hosted homelab dashboard. CasaOS-grade ease of use, Vercel-grade design. Single container, UI-managed, no YAML, no auth. -**Status:** pre-release. See [ROADMAP.md](ROADMAP.md). +![Dash board](docs/screenshots/board.png) + +## Features + +- **Service board** — sections, drag-and-drop, multi-URL services, icon upload, + four board renderers (Bento, Cards, Index, Console), dark/light themes +- **Uptime monitors** — HTTP, TCP, ping, DNS, keyword, JSON-path, and push + checks on a schedule, with heartbeat history and uptime stats +- **Domain watch** — WHOIS/RDAP, DNS records, TLS certificate expiry, + registrar and provider detection +- **System monitoring** — lightweight `dash-agent` pushes CPU, memory, disk, + network, load, temperatures, and Docker container state +- **Incidents & status pages** — manual and automatic incidents, maintenance + windows, public `/status/:slug` pages, SVG badges +- **Alerts** — generic webhook notifications (Slack, Discord, ntfy compatible) +- **Observability** — Prometheus `/api/metrics`, JSON export, CSV bulk import, + importers for Homepage, Homarr, and Dashy configs ## Run ```sh -docker run -v ./data:/data -p 3000:3000 dash +docker run -d -v ./data:/data -p 3000:3000 ghcr.io/dvorinka/dash:latest ``` -or +or with compose: + +```yaml +services: + dash: + image: ghcr.io/dvorinka/dash:latest + ports: + - "3000:3000" + volumes: + - ./data:/data + restart: unless-stopped +``` + +Then open http://localhost:3000. Everything is configured from the UI. + +## System agent + +Monitor a Linux host: create a system in the UI, copy the token, then: ```sh -docker compose up +dash-agent -url http://dash:3000 -token ``` -Then open http://localhost:3000. +One static binary, no dependencies. See +[docs/installation.md](docs/installation.md) for systemd and Docker setups. + +## Screenshots + +| | | +|---|---| +| ![Monitors](docs/screenshots/monitors.png) | ![Domains](docs/screenshots/domains.png) | +| ![Systems](docs/screenshots/systems.png) | ![Status page](docs/screenshots/status-page.png) | + +## Docs + +- [Installation](docs/installation.md) +- [Configuration](docs/configuration.md) +- [Widget development](docs/widgets.md) +- [Roadmap](ROADMAP.md) · [Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md) ## Develop diff --git a/ROADMAP.md b/ROADMAP.md index ba0e33a..01b22b7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -175,7 +175,7 @@ Beszel-style server monitoring, push-based. - [x] Schema: `systems` (name, token, host, os/arch, last_seen, status, `latest` JSON snapshot), `system_stats` (system_id, ts, payload JSON — one column, no sub-field queries; 7d retention prune) - [x] Ingest: `POST /api/systems/ingest` with per-system bearer token; offline when silent > max(3×interval, 30s); `system.up`/`system.down` webhook events - [x] `cmd/dash-agent`: Linux collector — `/proc` (cpu/mem/net/load/uptime), `/sys` hwmon temps, statfs disk, docker.sock container list; POST every `-interval` (default 10s); `-once` debug mode -- [ ] Agent packaging: systemd unit + Dockerfile + release binaries (Phase 4) +- [x] Agent packaging: systemd unit (`packaging/dash-agent.service`) + `infra/Dockerfile.agent` + goreleaser binaries (Phase 4) - [x] Systems page (status cards, cpu/mem/disk bars, uptime) + detail (recharts: cpu%, mem%, net rx/tx, load; temps + containers tables) - [x] Board widget `system` (cpu/mem/disk mini bars) - [x] Not ported: the beszel agent protocol (SSH/WS into PocketBase) — our agent is push-JSON. SMART/ZFS/GPU metrics + per-container cpu/mem deferred to demand. @@ -192,11 +192,11 @@ Beszel-style server monitoring, push-based. ## Phase 4 — Open-source launch → v1.0.0 -- [ ] Docs: install, configuration, widget development guide (`docs/`) -- [ ] Importers: Homepage `services.yml`, Homarr JSON, Dashy `conf.yml` → sections/items/urls -- [ ] `README.md` hero: screenshots (4 themes), demo GIF, `docker run` one-liner -- [ ] goreleaser → binaries; GHCR image on tag; semver + `CHANGELOG.md` -- [ ] `CONTRIBUTING.md`, issue/PR templates, code of conduct, security policy +- [x] Docs: install, configuration, widget development guide (`docs/`) +- [x] Importers: Homepage `services.yml`, Homarr JSON, Dashy `conf.yml` → sections/items/urls +- [x] `README.md` hero: screenshots, `docker run` one-liner (demo GIF deferred — pngs suffice for v1.0.0) +- [x] goreleaser → binaries; GHCR image on tag; semver + `CHANGELOG.md` +- [x] `CONTRIBUTING.md`, issue/PR templates, code of conduct, security policy - [ ] i18n scaffolding (en default) - [ ] Public repo flip + announcement assets diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..8196235 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,34 @@ +# Security Policy + +## Supported versions + +| Version | Supported | +|---|---| +| latest release | yes | +| older releases | no | + +Dash is pre-1.0; only the latest release receives fixes. + +## Reporting a vulnerability + +Use GitHub's private vulnerability reporting: +**Security → Advisories → Report a vulnerability** on this repository. + +Do not open a public issue for security problems. + +Please include: + +- affected version/commit +- steps to reproduce or a proof of concept +- impact assessment + +You will get an acknowledgement within a few days. If the report is accepted, +a fix ships in the next release and you are credited in the advisory (unless +you prefer otherwise). + +## Scope notes + +Dash is designed for trusted private networks — it ships **without +authentication** by design. Exposure to the public internet is a deployment +choice outside the project's control, but issues that remain exploitable on a +LAN (SSRF in checkers, injection, token leakage, unsafe schemes) are in scope. diff --git a/apps/backend/cmd/dash-agent/main.go b/apps/backend/cmd/dash-agent/main.go index 719ff6a..7a46359 100644 --- a/apps/backend/cmd/dash-agent/main.go +++ b/apps/backend/cmd/dash-agent/main.go @@ -25,6 +25,9 @@ import ( "time" ) +// version is stamped by goreleaser via -X main.version. +var version = "dev" + func main() { url := flag.String("url", os.Getenv("DASH_URL"), "dash server base URL") token := flag.String("token", os.Getenv("DASH_TOKEN"), "system token") @@ -52,7 +55,7 @@ func main() { if *url == "" || *token == "" { log.Fatal("dash-agent: -url and -token (or DASH_URL/DASH_TOKEN) required") } - log.Printf("dash-agent: pushing to %s every %ds", *url, *interval) + log.Printf("dash-agent %s: pushing to %s every %ds", version, *url, *interval) for { s := c.sample(*interval) if err := push(*url, *token, s); err != nil { diff --git a/apps/backend/cmd/dash/main.go b/apps/backend/cmd/dash/main.go index ed767bd..ac051c7 100644 --- a/apps/backend/cmd/dash/main.go +++ b/apps/backend/cmd/dash/main.go @@ -19,6 +19,9 @@ import ( "github.com/tdvorak/dash/internal/web" ) +// version is stamped by goreleaser via -X main.version. +var version = "dev" + func main() { dev := os.Getenv("DASH_DEV") == "1" @@ -55,7 +58,7 @@ func main() { srv := &http.Server{Addr: addr, Handler: router, ReadHeaderTimeout: 10 * time.Second} go func() { - logger.Info("listening", zap.String("addr", addr), zap.String("data", dataDir)) + logger.Info("listening", zap.String("addr", addr), zap.String("data", dataDir), zap.String("version", version)) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { logger.Fatal("serve", zap.Error(err)) } diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..9031516 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,71 @@ +# Configuration + +Dash is configured from the UI. The only environment the server understands: + +| Variable | Default | Purpose | +|---|---|---| +| `DASH_PORT` | `8080` (`3000` in Docker) | listen port | +| `DASH_DATA_DIR` | `./data` (`/data` in Docker) | SQLite + uploaded icons | +| `DASH_DEV` | unset | `1` = dev mode: API only, no embedded UI | + +## Data directory + +``` +data/ + dash.db # SQLite — boards, monitors, domains, systems, ops + icons/ # uploaded service icons +``` + +Back up the directory to back up everything. `/api/export` produces a JSON +snapshot; `/api/import/csv` bulk-loads monitors and domains from CSV. + +## Notifications + +**Settings → Notifications** holds one generic webhook URL +(`notify_webhook`). On monitor up/down transitions and domain events, Dash +POSTs a JSON payload with `event`, `name`, `text`, `content`, `message`, and +`at` fields — shaped so Slack, Discord, and ntfy all render it sensibly. +**Test** sends a `test` event. Delivery is async and non-fatal; failures are +logged. + +## Monitors + +HTTP(s), TCP, ping (ICMP), DNS, keyword, JSON-path, and push types. Per-monitor +interval, timeout, retries, headers/method/body, expected values, tags, and +notes. Push monitors expose `POST /api/push/:token` for cron-style jobs — the +monitor goes down if no push arrives within the expected window. + +## Domains + +WHOIS (RDAP first, TCP WHOIS fallback), DNS records, TLS certificate chain, +and provider detection run daily and on demand. Expiry and certificate +deadlines surface in the UI and through notifications. + +## Systems + +Bearer-token push ingest at `POST /api/systems/ingest`. A system is marked +offline when no sample arrives within a few intervals. Stats are pruned during +the hourly sweep. + +## Operations + +- **Incidents** — open/ack/resolve/close, with per-incident update timeline. + A monitor going down opens an incident automatically; recovery resolves it. +- **Maintenance windows** — suppress monitor alerts for the window's scope. +- **Status pages** — public, unauthenticated views at `/status/:slug` listing + selected monitors and systems with live state. +- **Badges** — `GET /api/badge/monitor/:id.svg` and + `GET /api/badge/domain/:id.svg` for embedding status shields. +- **Metrics** — `GET /api/metrics` in Prometheus exposition format. + +## Importers + +Settings → Import accepts: + +- Dash JSON export (full board restore) +- Homepage `services.yml` +- Homarr board JSON +- Dashy `conf.yml` +- CSV of monitors/domains (`/api/import/csv`) + +Foreign formats map onto sections, items, and URLs; unknown fields are dropped. diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..c4ee3f9 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,124 @@ +# Installation + +Dash ships as a single container image or a single static binary. All state +lives in one directory: a SQLite database and uploaded icons. + +## Docker (recommended) + +```sh +docker run -d --name dash \ + -v ./data:/data \ + -p 3000:3000 \ + --restart unless-stopped \ + ghcr.io/dvorinka/dash:latest +``` + +Or compose: + +```yaml +services: + dash: + image: ghcr.io/dvorinka/dash:latest + ports: + - "3000:3000" + volumes: + - ./data:/data + restart: unless-stopped +``` + +Open http://localhost:3000. + +To build the image locally instead of pulling: + +```sh +docker compose up --build +``` + +## Binary + +Release archives contain a static `dash` binary (no CGO, no dependencies). + +```sh +tar xzf dash_linux_amd64.tar.gz +./dash # listens on :8080, stores data in ./data +``` + +Set `DASH_PORT` and `DASH_DATA_DIR` to change the defaults. + +## dash-agent (system monitoring) + +The agent collects CPU, memory, disk, network, load, temperatures, and Docker +container state, then pushes a JSON sample to Dash every few seconds. + +1. In the UI, go to **Systems → New system**, copy the generated token. +2. Download the `dash-agent` binary for the host's architecture from the + release page, or build it: + + ```sh + cd apps/backend && go build -o dash-agent ./cmd/dash-agent + ``` + +3. Run it: + + ```sh + dash-agent -url http://dash:3000 -token + ``` + +### Flags / environment + +| Flag | Env | Default | Purpose | +|---|---|---|---| +| `-url` | `DASH_URL` | — | Dash base URL | +| `-token` | `DASH_TOKEN` | — | system token from the UI | +| `-interval` | `DASH_INTERVAL` | `10` | push interval, seconds | +| `-once` | — | `false` | print one sample and exit (no push) | + +If `/var/run/docker.sock` exists, container states are included automatically. + +### systemd + +`packaging/dash-agent.service` is a ready-made unit: + +```sh +sudo install -m755 dash-agent /usr/local/bin/dash-agent +sudo install -m644 packaging/dash-agent.service /etc/systemd/system/ +sudo mkdir -p /etc/dash +printf 'DASH_URL=http://dash:3000\nDASH_TOKEN=\n' | sudo tee /etc/dash/agent.env +sudo systemctl enable --now dash-agent +``` + +### Docker (limited) + +`infra/Dockerfile.agent` builds a minimal agent image. A containerized agent +only sees its own `/proc` — mount the host's procfs and use host PID mode for +meaningful numbers: + +```sh +docker run -d --name dash-agent --pid=host \ + -v /proc:/host/proc:ro \ + -v /var/run/docker.sock:/var/run/docker.sock:ro \ + -e DASH_URL=http://dash:3000 -e DASH_TOKEN= \ + ghcr.io/dvorinka/dash-agent:latest +``` + +The systemd install above is the recommended path. + +## Reverse proxy + +Dash serves the UI and API on one port — a plain reverse proxy works: + +```nginx +location / { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Host $host; +} +``` + +## Upgrading + +Pull the new image and restart, or replace the binary. Goose migrations run +automatically on boot; `data/` carries forward. + +```sh +docker pull ghcr.io/dvorinka/dash:latest && docker compose up -d +``` diff --git a/docs/screenshots/board.png b/docs/screenshots/board.png new file mode 100644 index 0000000..1fe6e03 Binary files /dev/null and b/docs/screenshots/board.png differ diff --git a/docs/screenshots/domains.png b/docs/screenshots/domains.png new file mode 100644 index 0000000..3078a83 Binary files /dev/null and b/docs/screenshots/domains.png differ diff --git a/docs/screenshots/monitors.png b/docs/screenshots/monitors.png new file mode 100644 index 0000000..9a5c5f2 Binary files /dev/null and b/docs/screenshots/monitors.png differ diff --git a/docs/screenshots/status-page.png b/docs/screenshots/status-page.png new file mode 100644 index 0000000..ef9ccad Binary files /dev/null and b/docs/screenshots/status-page.png differ diff --git a/docs/screenshots/systems.png b/docs/screenshots/systems.png new file mode 100644 index 0000000..66bb31b Binary files /dev/null and b/docs/screenshots/systems.png differ diff --git a/docs/widgets.md b/docs/widgets.md new file mode 100644 index 0000000..63787f2 --- /dev/null +++ b/docs/widgets.md @@ -0,0 +1,89 @@ +# Widget development + +A widget is two pieces: + +1. a Go **fetcher** in `apps/backend/internal/widget/` that declares config + fields and pulls live data, and +2. a React **view** in `apps/frontend/src/widgets/` that renders the payload + inside a tile. + +Renderers own the tile chrome — widgets never touch layout, drag-drop, or +popovers. + +## Backend fetcher + +One file, registered in `init()`: + +```go +package widget + +type pihole struct{} + +func init() { Register(pihole{}) } + +func (pihole) Meta() Type { + return Type{ + Type: "pihole", + Name: "Pi-hole", + Description: "Queries and block rate", + Fields: []Field{ + {Key: "endpoint", Label: "Endpoint", Placeholder: "http://pi.hole", Required: true}, + {Key: "token", Label: "API token", Secret: true}, + }, + } +} + +func (pihole) Fetch(ctx context.Context, cfg json.RawMessage) (any, error) { + // cfg holds the item's config JSON. Never log it — it may hold keys. +} +``` + +- `Meta()` drives the add-widget dialog: `GET /api/widgets/types` returns + every registered type with its fields. +- `Fetch` is called by `GET /api/widgets/:id/data`; responses are cached ~30s + in-process. Return any JSON-marshalable value — it reaches the frontend as + `data`. +- Read config with the `cfgStr`/`cfgNum` helpers in the package. +- Fetchers needing the database (monitor, domain, system) live in + `internal/api` and register via `widget.Register` at router setup — the + `widget` package stays a leaf with no DB dependency. + +## Frontend view + +One component in `apps/frontend/src/widgets/`, one line in the registry at +`src/widgets/index.tsx`: + +```tsx +export function PiholeWidget({ data, error }: { item: Item; data?: WidgetData; error?: string }) { + if (error) return ; + if (!data) return ; + return
{data.blockedPercent}%
; +} +``` + +```ts +const registry: WidgetRegistry = { + pihole: { view: PiholeWidget }, +}; +``` + +- `useWidgetData` polls `/api/widgets/:id/data` every 30s. `data` is the + fetcher's payload, `error` is a human-readable upstream failure. +- Payloads are free-form; read fields defensively (values may be absent on + first paint). +- `local: true` marks a widget as frontend-only (e.g. clock) — no fetcher, no + polling. + +## Config fields + +`Fields` in `Meta()` render inputs in the add-widget dialog. `Secret: true` +masks the input. Config is stored in `items.config` JSON, sent back to the +fetcher on every poll, and never logged. + +## Checklist + +- [ ] `apps/backend/internal/widget/.go` — fetcher + `init()` + `Meta()` +- [ ] `apps/frontend/src/widgets/.tsx` — view component +- [ ] registry entry in `src/widgets/index.tsx` +- [ ] error and empty-data states render cleanly in the tile +- [ ] upstream timeouts bounded (respect `ctx`, keep HTTP client timeouts) diff --git a/infra/Dockerfile.agent b/infra/Dockerfile.agent new file mode 100644 index 0000000..77a7e96 --- /dev/null +++ b/infra/Dockerfile.agent @@ -0,0 +1,15 @@ +# syntax=docker/dockerfile:1 + +# Minimal dash-agent image. A containerized agent only sees its own /proc — +# run with --pid=host and -v /proc:/host/proc for host metrics, or prefer the +# systemd unit in packaging/. +FROM golang:1.27-alpine AS build +WORKDIR /src +COPY apps/backend/go.mod apps/backend/go.sum ./ +RUN go mod download +COPY apps/backend ./ +RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/dash-agent ./cmd/dash-agent + +FROM gcr.io/distroless/static-debian12 +COPY --from=build /out/dash-agent /dash-agent +ENTRYPOINT ["/dash-agent"] diff --git a/packaging/dash-agent.service b/packaging/dash-agent.service new file mode 100644 index 0000000..30756df --- /dev/null +++ b/packaging/dash-agent.service @@ -0,0 +1,18 @@ +[Unit] +Description=Dash system monitoring agent +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +EnvironmentFile=/etc/dash/agent.env +ExecStart=/usr/local/bin/dash-agent +Restart=always +RestartSec=5 + +# Reads /proc, /sys, and (if present) the Docker socket. +ProtectHome=true +NoNewPrivileges=true + +[Install] +WantedBy=multi-user.target