Skip to content

Repository files navigation

saarthi

A self-hosted, plugin-extensible homelab stats dashboard. Single static Go binary, minimal Docker image, per-user customizable dashboards, and a public read-only share link for anything you want to show off without giving out access to the rest of your homelab.

Go License

Features

  • Built-in host stats — CPU, memory, network throughput, always on.
  • Plugin system — every other widget is a plugin: some ship built in, and users can add their own configured instances at runtime (no rebuild needed) from the dashboard's "Add widget" button.
  • Per-user dashboards — each authenticated user has their own saved widget layout and their own plugin instances (their own Traefik URL, their own weather location, ...).
  • Public share links — every user gets a /share/<token> URL they can turn on, exposing only the specific widgets they've explicitly marked public, read-only, no login required for viewers.
  • Centralized API key vault — enter a service's API key once on the Settings page, then pick it from a dropdown in any plugin instance that needs it, instead of re-typing it per instance.
  • Backup & restore — each user can download their own dashboard (instances, API keys, layout, share settings) as a JSON file and restore it later, live, no restart required.
  • Built-in login, or bring your own SSO — a first-run setup wizard creates the admin account; after that, anyone can sign up unless you disable it. If a forward-auth header (Authentik, Authelia, Traefik forward-auth, ...) is present on a request, it's trusted instead — so you can point this at your existing SSO and skip local accounts entirely.

Available plugins

Plugin What it shows Needs
CPU / Memory / Network Host utilization nothing (built in)
Traefik Router/service counts, request volume by status code Traefik API reachable, optionally its /metrics endpoint enabled
Immich Photo/video counts, library size, disk usage Immich API key
Nextcloud Users, files, active sessions, memory/CPU load Admin username + app password, serverinfo app enabled
Weather Current conditions for a chosen location nothing — uses Open-Meteo, no key
World Clock Live clocks for any timezones you list nothing
Stock Watchlist Quote + mini price history per symbol Finnhub API key (free tier)
Crypto Tracker Quote + 7-day sparkline per coin nothing — uses CoinGecko, no key

Quick start

curl -O https://raw.githubusercontent.com/rangoDJ/saarthi/main/docker-compose.yml
docker compose up -d

This pulls the published image from GHCR — nothing to build locally. Then:

  1. Open the dashboard — you'll land on a setup wizard to create the admin account (only shown while no accounts exist).
  2. Log in, click Settings, add any API keys you need under API keys, then add plugin instances from the main dashboard's + Add widget button.

Authentication

Two ways to authenticate, checked in this order:

  1. Forward-auth header — if your reverse proxy (Authentik, Authelia, Traefik forward-auth, ...) sets an identity header on the request, saarthi trusts it outright and skips its own login entirely. Default header name is X-Authentik-Username; point DASHBOARD_AUTH_HEADER at whatever your proxy calls it (e.g. Authelia's Remote-User).
  2. Built-in accounts — if no header is present, saarthi falls back to its own login (bcrypt-hashed passwords, session cookies). The first account ever created (via the /setup wizard) is the admin; anyone else can self-register at /signup unless you set DASHBOARD_ALLOW_SIGNUPS=false. There's no admin/user permission split yet beyond "who created the account first" — see Known limitations.

Configuration

All configuration is environment variables, prefixed DASHBOARD_.

Variable Default Purpose
DASHBOARD_HOST 0.0.0.0 Listen address
DASHBOARD_PORT 8090 Listen port
DASHBOARD_TOKEN (empty) Shared secret required (as X-Dashboard-Token) on mutating requests
DASHBOARD_ALLOWED_HOSTS (empty) Comma-separated allowlist of Host headers / CIDR ranges (DNS-rebinding protection)
DASHBOARD_AUTH_HEADER X-Authentik-Username Header your reverse proxy sets with the authenticated username
DASHBOARD_ALLOW_SIGNUPS true Set to false to disable /signup (the setup wizard for the first admin account still always works)
DASHBOARD_ACCOUNTS_FILE <settings dir>/accounts.json Where local login accounts (bcrypt hashes only) are stored
DASHBOARD_SETTINGS_DIR data Directory for settings storage — global.json plus one users/<name>.json per user, created/read lazily as each user is seen
DASHBOARD_LOG_LEVEL INFO Set to DEBUG for verbose per-request and per-collector logging

Security note: if the server is reachable on all interfaces (DASHBOARD_HOST=0.0.0.0, the default) with neither DASHBOARD_TOKEN nor DASHBOARD_ALLOWED_HOSTS set, all mutating routes (saving settings, creating plugin instances, etc.) are automatically disabled at startup — a deliberate fail-safe rather than silently exposing them. Set one of the two before exposing this beyond localhost.

Architecture

cmd/dashboard/        entrypoint — loads config, blank-imports every plugin package, starts the server
internal/plugin/      the Widget interface + registry every plugin implements/self-registers into
internal/manager/     resolves each widget's effective active state (enabled + detected/reachable) and drives the scheduler
internal/scheduler/   runs each active widget's Collect() on its own interval, holds the latest-value snapshot
internal/settings/    JSON-file-backed persistence: global toggles, per-user instances/credentials/layout
internal/localauth/   bcrypt-hashed local account store (setup wizard, signups)
internal/session/     in-memory session cookies for local accounts
internal/auth/        request-context carrier for the resolved username (see internal/api/authflow.go for how it's resolved)
internal/api/         HTTP routes + security middleware
internal/plugins/*/   one package per plugin (cpu, mem, net, traefik, weather, worldclock, immich, nextcloud, stocks, crypto)
web/                  frontend — vanilla JS widget registry + GridStack.js grid, no build step

Writing a plugin

Every plugin implements plugin.Widget (ID(), Name(), Category(), Detect()) plus whichever of these it needs:

  • plugin.Collector — has a live value polled on an interval (Interval(), Collect()).
  • plugin.Configurable — needs user-supplied settings (SettingsFields(), ApplySettings()); this is what makes a plugin appear as an instantiable type on the "Add widget" flow.
  • plugin.RouteProvider — exposes its own mutating HTTP routes.
  • plugin.Redetectable — its availability can change while running (e.g. a socket that comes and goes), so it should be re-checked periodically.

Look at internal/plugins/weather/weather.go for the simplest complete example (no auth, one external API call), or internal/plugins/traefik/ for one with a settings form and best-effort partial data. Register it with plugin.RegisterFactory(typeID, name, func() plugin.Widget { return &Widget{} }) in an init(), then add a blank import in cmd/dashboard/main.go.

On the frontend, add a matching web/static/widgets/<name>.js file calling registerWidgetType({ type, template, onMetrics, ... }), and include the <script> tag in web/templates/index.html and share.html.

Development

go build ./...
go vet ./...
go run ./cmd/dashboard

Requires Go 1.25+. No cgo, no external tools — the built-in plugins (CPU/mem/net) and every optional plugin talk to plain HTTP APIs, so the binary is fully static and the runtime image needs nothing beyond ca-certificates.

Known limitations

  • No historical charts / SQLite-backed metrics history yet — every widget shows current/live values only (some, like stocks and crypto, include a short embedded sparkline from their own API).
  • No disk-usage plugin yet.
  • Third-party plugins currently require writing Go and rebuilding the image — there's no external/sidecar plugin protocol (yet).
  • No admin/user roles — every authenticated user can toggle the shared host-wide widgets (CPU/mem/net) on or off for everyone, and any account can back up/restore only its own data, but there's no privileged "manage other users" surface yet.
  • Sessions for local accounts are in-memory only — a server restart logs everyone out (forward-auth-header users are unaffected, since that identity comes from the proxy on every request).

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages