A self-hostable operations platform for data-center field teams. One app for what's happening today (duty, tasks, incidents), what we know (KB wiki + file search + per-site location tree), and who's around (staff, teams, on-call rotations).
Built so a single duty operator can answer "what's burning, who's on it, where's the runbook" without bouncing between four tools.
git clone https://github.com/Supawitk/NodeOps.git
cd NodeOps/backend
cp .env.example .env
docker compose up -d --buildWait ~30 seconds for the containers to come up, then open:
| URL | What | How to run |
|---|---|---|
| http://localhost:3000 | Operator console — main desktop app | included in docker compose |
| http://localhost:3100 | User-view — engineer's daily workspace | cd user-view && npm install && npm run dev |
| http://localhost:4321 | PWA — installable mobile shell | cd pwa && npm install && npm run dev |
| http://localhost:8000/docs | FastAPI Swagger UI | included |
| http://localhost:9001 | MinIO console | included |
🔑 Default login:
admin/1234. ChangeSEED_ADMIN_PASSWORDand setAUTH_TOKEN_SECRETin.envbefore exposing this beyond localhost. Generate a secret with:python -c "import secrets; print(secrets.token_hex(32))"
📱 iOS app: open mobile/megabase.xcodeproj in Xcode, set API base URL in the More tab to your laptop's LAN IP, or to the frpc URL below.
localhost only works from the machine running Docker. The easiest way to get a real URL is frpc — an open-source reverse-tunnel that runs on any $5/month VPS:
# frpc.toml on your laptop — point it at your VPS running frps
serverAddr = "your.vps.public.ip"
serverPort = 7000
auth.token = "shared-secret-between-frpc-and-frps"
[[proxies]]
name = "nodeops-api"
type = "tcp"
localIP = "127.0.0.1"
localPort = 8000
remotePort = 18000
[[proxies]]
name = "nodeops-web"
type = "tcp"
localIP = "127.0.0.1"
localPort = 3000
remotePort = 13000frpc -c frpc.tomlYour stack is now reachable at http://your.vps.public.ip:18000 (API) and :13000 (web). Point the PWA / iOS app's API base URL setting at the API one.
Three frontends + one native iOS app, one Python API, one Postgres, one S3-compatible store. All wired together by docker compose.
┌────────── Operator Console (:3000) ──────────┐
│ Vite + React 19 + TypeScript │ ← Desktop SPA for duty operators
│ React Flow + dagre (KB canvas) │
│ Markdown editor, PDF.js viewer │
└────┬─────────────────────────────────────────┘
│
┌────┴── User-view (:3100) ────────────────────┐
│ Lighter SPA — engineer's daily workspace │ ← Today / MyTasks / Inbox / Reports
└────┬─────────────────────────────────────────┘
│
┌────┴── PWA (:4321) ──────────────────────────┐
│ Installable mobile shell wrapping user-view │ ← Home-screen app for field engineers
│ Service worker, offline queue, glass tabbar │
└────┬─────────────────────────────────────────┘
│
┌────┴── iOS app (mobile/) ────────────────────┐
│ SwiftUI native app — same backend │ ← Optional: build with Xcode
└────┬─────────────────────────────────────────┘
│ /api (WebSocket + REST)
┌────▼──────────────────────────────────────────┐
│ FastAPI (Python 3.12) │
│ SQLAlchemy + raw SQL │
│ /v1/* — auto OpenAPI at /docs │
│ WebSocket /v1/ws — realtime broadcasts │
└──┬─────────────────────────────────────┬──────┘
│ │
┌──▼─────────────┐ ┌─────▼────────┐
│ Postgres 16 │ │ MinIO (S3) │
│ + pgvector │ │ attachments │
└────────────────┘ └──────────────┘
| Area | What lives there |
|---|---|
| Overview | KPIs (active issues, open tasks, overdue, staff online), today's duty, my tasks/reports, sites + status, mini-month calendar. |
| Calendar / Duty | Day / week / month / year views. Recurring schedules and one-off tasks share the same surface. |
| Operations | Sites (project cards, project-detail timelines), tasks (filterable list + Kanban ribbon), reporting (issue + note feed, severity, SLA timer, audit log). |
| Knowledge base | Tree-style wiki (folders / Markdown pages / file uploads with PDF + Office preview), full-text search, and a React Flow canvas view that mirrors the org tree (clients → sites → projects → teams → people) with live operational overlay. |
| Inbox / Chat | Realtime site-, team-, and room-scoped chat over WebSocket. Per-channel unread watermarks roll up to the sidebar Inbox badge. Tasks-to-me and issues share the same surface. |
| Realtime incidents | Field engineer can halt an active task → files a sev-tagged report linked to that task. Sev1/sev2 auto-blocks the task and pushes an incident.new toast to every operator console in <1s. Ack / resolve mirrors instantly across web + PWA + iOS. |
| Presence | Staff status (online / offline / on-leave) flips automatically on login + check-in / check-out. Same flag the operator console and the PWA both consume. |
| Management | Roster, teams, on-call shifts, clients / sites / projects CRUD, user accounts, optional Microsoft account linking. |
.
├── backend/ Backend stack — docker compose lives here.
│ ├── api/ FastAPI service (Python 3.12) — REST + WebSocket.
│ ├── web/ Operator console (Vite + React 19), :3000.
│ ├── infra/ Postgres init scripts (pgvector + base schema).
│ └── docker-compose.yml One command brings everything up.
├── user-view/ Engineer workspace SPA (:3100). Separate codebase
│ so day-of-work UI evolves without dragging the
│ operator console along.
├── pwa/ Installable mobile shell (:4321) wrapping user-view.
└── mobile/ SwiftUI native iOS app — open in Xcode.
The data layer is deliberately built so dropping in a retrieval-augmented chatbot is mostly UI work — the hard parts are already done:
| What's already in place | Where |
|---|---|
pgvector extension installed in Postgres on first boot — embeddings live next to relational data, retrieval is one SQL hop. |
backend/infra/postgres/init.sql, backend/api/pyproject.toml |
Docling extraction pipeline — every uploaded PDF / DOCX / XLSX / PPTX is parsed into plain text and stored in kb_nodes.body, then auto-indexed. No "convert this PDF to text" plumbing required. |
backend/api/app/services/kb_extract.py |
Structured KB tree — the wiki is a real tree (clients → sites → projects → teams → pages), not a flat document dump. Retrieval can scope by branch (e.g. "only this site"), filter by node type, or follow parent / child relations for context expansion. |
backend/api/app/routers/kb/ |
Full-text search already running — Postgres tsvector + GIN indexes over titles + extracted bodies. Useful as a cheap first-pass retriever, or as a fallback when vector recall is low. |
backend/api/app/routers/_schema.py (search the file for to_tsvector) |
| Per-page anchors — each KB page can be deep-linked to a specific heading, so retrieved chunks come with a click-through location, not just "we found this somewhere in the wiki." | KB editor pin feature |
Three small files. The Ollama env vars are already stubbed in .env.example:
OLLAMA_MODEL=qwen2.5:7b
EMBEDDING_MODEL=nomic-embed-textWhat you add:
backend/api/app/services/embed.py— wraphttpxPOST to Ollama's/api/embeddings. Add anembedding vector(768)column tokb_nodesvia a migration, embed on insert / update, store inline.backend/api/app/services/rag.py— given a query: embed it, runORDER BY embedding <=> $1 LIMIT 8againstkb_nodes, optionally rerank with full-text search, return the top chunks with their tree paths.backend/api/app/routers/chat_ai.py—POST /v1/chat/ai: takes a question, callsrag.retrieve(), stuffs the chunks into the system prompt, streams the LLM response back via SSE.
Frontend: a sidebar drawer on the operator console, the chat tab in the user-view inbox, and an "Ask AI" button on KB pages. All three can hit the same endpoint.
Because the KB is already a clean tree of human-curated content (not a scraped pile), retrieval quality should be unusually good out of the box — most RAG projects start by building this structure; NodeOps starts with it already there.
| Project | What it shaped here |
|---|---|
| llmwiki | The "wiki + LLM retrieval over the same store" model — keep human-edited knowledge and embeddings in one DB, retrieve at chat time. Drove the pgvector choice. |
| Docling | In-process PDF / DOCX / XLSX / PPTX → text + layout extraction. Lets the KB ingest office docs without a separate Tika-style sidecar service. |
| Gotenberg | HTTP wrapper around LibreOffice headless — converts office docs to PDF for the inline KB preview, so the operator sees the slide as the author drew it, not just the extracted text. |