docs+release: Phase 4 launch artifacts

README hero with real screenshots, install/config/widget docs, goreleaser
for dash + dash-agent, GHCR release workflow, changelog, community files,
agent systemd unit and Dockerfile, version stamping via -X main.version.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-21 19:39:21 +02:00
1 parent 97b8f5e87f
commit a89a81d46c
23 files changed
+785 -13

No files matched your search

+71
View File
@@ -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.
+124
View File
@@ -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 <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=<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=<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
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+89
View File
@@ -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 <TileError msg={error} />;
if (!data) return <TileLoading />;
return <div>{data.blockedPercent}%</div>;
}
```
```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/<name>.go` — fetcher + `init()` + `Meta()`
- [ ] `apps/frontend/src/widgets/<name>.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)