A personal command center — what needs your attention now, in one place.
Opsora is a self-hosted dashboard that answers one question: what should I care about right now? It pulls life (todos, calendar) and infrastructure (Linux servers, Pi-hole, Docker, GitHub) into a single ranked attention feed instead of a wall of charts.
Status: v0.5 — every module is fully interactive and runs on real data only: unconfigured sources show an honest "not configured" state instead of mock numbers. Metrics stream over SSE, tasks sync to SQLite on the server, and the server pushes alerts to Telegram/Discord. A failed live source shows a visible error state instead of breaking the page.
| Area | Included |
|---|---|
| Home | Attention feed, today's todos, today's schedule, infrastructure strip, activity |
| Todos | CRUD, statuses, priorities, projects, tags, smart views, quick add, SQLite sync |
| Calendar | Seven-day agenda with today highlighting |
| GitHub | Repos, PRs, issues, CI status, activity — live with GITHUB_TOKEN |
| Servers | CPU/RAM/disk/network, services, processes, history — live via the agent |
| Pi-hole | 24h queries/blocks, top clients and domains — live with PIHOLE_URL |
| Docker | Container table, health, CPU/memory, uptime — live automatically from the socket |
| Alerts | Every signal, filtered by severity + browser and Telegram/Discord delivery |
| Analytics | Rule-based insights generated across all sources |
| Live | SSE stream pushes updates every 5s; polling is the fallback |
| Command bar | ⌘K palette and single-key navigation (N T C G D S P A) |
Copy .env.example to .env.local and configure what you have. Anything left
unset renders a "not configured" empty state with setup hints — the dashboard
never invents data.
If /var/run/docker.sock exists, Docker data is live. Opt out with
OPSORA_DOCKER=0.
The agent is a single dependency-free Node script:
OPSORA_URL=http://localhost:9942 \
OPSORA_ROLE="Primary homelab server" \
OPSORA_SERVICES=docker,ssh,caddy \
node agent/opsora-agent.mjs--oncesends a single report; otherwise it loops (OPSORA_INTERVAL, default 30s).- Metrics: CPU, memory, disks, network throughput, load, temperature, top processes and systemd service states.
- Set
OPSORA_AGENT_TOKENon the server andOPSORA_TOKENon the agent to require a shared secret.
Minimal systemd unit:
[Unit]
Description=Opsora agent
After=network-online.target
[Service]
Environment=OPSORA_URL=http://localhost:9942
Environment=OPSORA_ROLE=Primary homelab server
ExecStart=/usr/bin/node /opt/opsora/agent/opsora-agent.mjs
Restart=always
[Install]
WantedBy=multi-user.targetGITHUB_TOKEN=github_pat_...Read-only access is enough. Repos, latest commits, Actions runs, review-requested and authored PRs, plus open issues.
PIHOLE_URL=http://pi.hole
PIHOLE_PASSWORD=...GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
GOOGLE_REFRESH_TOKEN=...
GOOGLE_CALENDAR_ID=primary # optionalCreate an OAuth client of type "Desktop app" in Google Cloud, then let the helper script walk you through consent and print the refresh token:
GOOGLE_CLIENT_ID=... GOOGLE_CLIENT_SECRET=... node scripts/google-oauth.mjsTELEGRAM_BOT_TOKEN=... # from @BotFather
TELEGRAM_CHAT_ID=...
DISCORD_WEBHOOK_URL=...When configured, the server checks the attention feed every minute and pushes
new critical/warning signals (set OPSORA_ALERT_LEVEL=all to include info,
OPSORA_ALERTS=0 to disable). The first run only records the current state, so
you never get a startup flood. Use the "Send test" button in Settings to verify
a channel.
- Next.js (App Router) + React + TypeScript
- Tailwind v4 with design tokens as CSS variables
- shadcn-style copy-in primitives (Radix only where behavior requires it)
- TanStack Query for polling with stale/error states
- Zustand for UI state and todo persistence
- Recharts for the two large charts, hand-rolled SVG sparklines everywhere else
- Tauri v2 (Rust) for the native desktop shell (
src-tauri/)
pnpm install
cp .env.example .env.local # optional: configure live sources
pnpm dev # http://localhost:9942Other scripts:
pnpm build # production build (standalone output)
pnpm start # run the production server
pnpm test # vitest — attention, adapters, calendar, alerts, SQLite (43 tests)
pnpm typecheck # tsc --noEmit
pnpm lint # eslintThe same dashboard as a native window. The packaged app bundles the Next.js standalone server and a Node.js 22 runtime, so end users do not need Node.js installed — it boots the server on launch, shows a themed splash while it warms up, then opens the dashboard.
Prerequisites for building: Rust + WebKit dev libraries. Running from source
only requires Node.js for the dev server; pnpm desktop:stage downloads the
bundled runtime automatically (cached in .node-runtime/).
pnpm desktop:dev # Tauri window + Next dev server (http://localhost:9942)
pnpm desktop:runtime # pre-download the Node binary that gets bundled
pnpm desktop:stage # Next production build → stage server + runtime + public/
pnpm desktop:build # produce native installers (AppImage/deb/rpm, NSIS/MSI)How it works:
scripts/desktop-stage.mjs(runs as Tauri'sbeforeBuildCommand) production-builds Next.js and stages the standalone server, static assets and the Node runtime intosrc-tauri/bundled-server/.src-tauri/src/main.rspicks a free port (OPSORA_PORToverrides, default9942, random fallback when the port is taken by something that is not Opsora), spawns the bundled server with the bundled Node, waits for it to answer, then swaps the splash page for the dashboard.- External links open in the system browser; the app window stays on the local server. A second launch focuses the existing window instead of starting a rival server.
- Config lives in the OS app-data directory:
opsora.env(tokens,PIHOLE_URL, …) is written with a commented template on first launch and injected into the server process. Server data (SQLite, agent metrics) lives next to it inserver-data/, so updates never wipe tasks.
Pull the prebuilt image (CI publishes it on every release tag):
docker run -d \
--name opsora \
-p 9942:9942 \
--env-file .env \
--group-add "$(stat -c '%g' /var/run/docker.sock)" \
-v opsora-data:/app/.data \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
ghcr.io/suryaprakash251201/opsora:latest--env-file .envpasses yourGITHUB_TOKEN/PIHOLE_URL/ … into the container. Without it every source renders a "not configured" empty state.- The socket mount plus
--group-addis what makes the Docker page live. The image runs as a non-root user, so it needs the host's docker group GID to read/var/run/docker.sock.
Or build and run from source:
cp .env.example .env # fill in what you have
docker compose up -d --buildThe image builds a Next.js standalone server and runs as a non-root user. Ingested
agent metrics persist in the opsora-data volume. docker-compose.yml already
mounts the host socket and loads .env; set DOCKER_GID if your host docker
GID is not 983 (stat -c '%g' /var/run/docker.sock).
| Workflow | Trigger | What it does |
|---|---|---|
CI (.github/workflows/ci.yml) |
push to main, PRs |
lint → typecheck → test → production build → Tauri shell (fmt/check/clippy) |
Desktop (.github/workflows/desktop.yml) |
manual, or called by Release | build self-contained Tauri installers on ubuntu-22.04 (AppImage/deb/rpm) and windows-latest (NSIS/MSI), upload them as artifacts |
Release (.github/workflows/release.yml) |
tag v* |
verify → Docker image + desktop installers → GitHub release with all assets attached |
To cut a release:
git tag v0.6.0 && git push origin v0.6.0 ┌──────────────── agent (Linux) ────────────────┐
│ /proc, df, ps, systemctl → POST /api/agent │
└───────────────────────┬───────────────────────┘
▼
Browser ── SSE /api/stream (5s) ──┐
│ ├──► /api/snapshot + /api/sources/[source]
└── TanStack Query (fallback) ─┘ │
Adapter registry (server-side)
│
Browser ◄── /api/todos ◄── SQLite (.data/opsora.db)
Server startup ── instrumentation ──► alert dispatcher (60s)
└──► Telegram · Discord
│
┌────────────┬────────────┬──────────┴─────────┬────────────┐
servers github pihole docker calendar
agent store REST + PAT v6 API+sid socket API OAuth refresh
└────────────┴────────────┴────────────────────┴────────────┘
│
attention engine (pure)
│
Home · Alerts · Analytics · topbar
Two ideas hold the app together:
1. Adapters. Every source implements the same interface
(src/lib/adapters/types.ts) behind a registry that resolves the live
adapter when credentials/data exist, or an honest empty state otherwise.
There is no mock data path. Tokens stay on the server; the UI only sees the
normalized snapshot shape.
2. The attention engine. A pure, unit-tested module
(src/lib/attention/engine.ts) converts every snapshot into one ranked list of
AttentionItems. Severity, urgency and recency feed a single score, and Home,
Alerts and the topbar all read from that one list — no duplicated alert logic.
agent/opsora-agent.mjs # dependency-free Linux metrics agent
scripts/desktop-stage.mjs # stages standalone server + Node runtime for Tauri
scripts/fetch-node-runtime.mjs # downloads the pinned Node binary to bundle
src-tauri/ # Tauri v2 desktop shell (Rust + splash + config)
src-tauri/splash/ # loading / error page shown while the server boots
src/
app/
(dashboard)/ # shell + pages
api/snapshot/ # all sources in one payload
api/sources/[source]/ # one source at a time
api/agent/ # metric ingest for the agent
components/
shell/ # sidebar, topbar, command palette, circuit bg
icons/ # animated "live" SVG icon system
ui/ # copy-in primitives
viz/ # stat tiles, sparklines, meters, rings, charts
features/ # home, todos, github, servers, alerts, analytics…
lib/
adapters/ # registry, live adapters, agent store
attention/ # rules, scoring, types, tests
activity.ts # activity feed derived from real snapshots
types/ # shared snapshot shapes (servers, github, …)
stores/ # Zustand UI state (theme, sidebar, sync…)
- v0.1 — shell, todos, attention engine, all pages
- v0.2 — live Docker + agent ingest + GitHub + Pi-hole adapters, notifications
- v0.3 — SSE live stream, SQLite task sync, source health panel
- v0.4 — Google Calendar OAuth, Telegram/Discord delivery, empty states
- v0.5 — theme system fix, circuit background, live icons, real-data-only, Tauri desktop
- v0.6 — self-contained desktop installers (bundled Node), Windows/Linux builds in CI, splash + single-instance shell, command-palette task search
- next — AI daily summaries over the attention feed
Dark, dense, premium command-center UI: #09090B background, #111113 panels,
#27272A borders, monospace metrics with tabular numerals, status colors that
never rely on hue alone.
Built for one user: Surya. Name Opsora — operations + aura.