Real-time Network Operations Center for monitoring cloud infrastructure. Services are checked every 30 seconds via HTTP/HTTPS/TCP/DNS probes. Failures open P1 incidents automatically, trigger email alerts, and push live updates to all connected clients over WebSocket.
Built as a portfolio project to demonstrate full-stack architecture: reactive frontend state machine, WebSocket with HTTP polling fallback, JWT auth with proactive refresh, and a PostgreSQL-backed event/incident pipeline.
- Multi-protocol health checks — HTTP, HTTPS, TCP, DNS probes with configurable timeout and retry logic
- Uptime SLA — 24 h uptime % calculated per service from the
checkstable, shown in Services and Dashboard - Latency bar — visual indicator per service; configurable degraded threshold (
LATENCY_THRESHOLD_MS) - Auto-incident on DOWN — worker detects UP→DOWN transitions and opens a P1 incident automatically if none is already open for that service
- Recovery alerts — email sent when a service returns to UP after DOWN/DEGRADED; subject line shows "RECOVERED"
- Split-panel list + detail view
- Status transitions: OPEN → INVESTIGATING → RESOLVED
- Timeline with system and manual entries
- Admin notes on active incidents
- Full event history (DOWN / DEGRADED / UP) with email badge when an alert was sent
- Paginated 25 events/page with filter-aware pagination
- WebSocket push from backend on every check cycle
- Automatic fallback to HTTP polling on disconnect with reconnection indicator
- JWT (8 h) with proactive refresh when < 30 min remain; reactive 401 retry with 2 h grace period
- Role-based UI: admin can add/remove services and manage incidents; viewer is read-only
- Session restored on page reload from localStorage
- Audit trail — every login/refresh attempt recorded in
auth_events(IP, user-agent, outcome) - Lockout — 5 failed logins in 10 minutes temporarily blocks the username (HTTP 429)
- IP-mismatch detection — refresh from a different IP than the last successful login is logged as
IP_MISMATCH(does not block; surfaces inGET /api/auth/eventsfor admin review)
- Encryption at rest —
services.host,services.portandchecks.error_msgare encrypted with AES-256-GCM before reaching Postgres. Topology data is unreadable from a DB dump or compromised replica withoutDATA_ENCRYPTION_KEY. Aggregations onlatency_ms/statusremain plain so uptime % and dashboards keep working. - Encryption in transit — terminate TLS in front of the Node API (see TLS deployment) so HTTPS + WSS is enforced for every probe-result delivery.
- Defense in depth — combined with disk-level encryption (LUKS / cloud KMS), an attacker needs the disk and the application key to read topology.
- Dark / light mode toggle — persisted in localStorage, transitions via CSS custom properties
- Responsive layout — hamburger drawer sidebar on mobile, full sidebar on desktop
- Confirmation dialog on destructive actions (remove service)
- Empty states — contextual messages when incident/alert lists are empty
- Toast notifications for live DOWN / DEGRADED events
| Layer | Tech |
|---|---|
| Frontend | React 18 · TypeScript · Vite · Tailwind CSS v3 |
| Backend | Node.js · Express · PostgreSQL 16 · WebSocket (ws) |
| Auth | JWT (8 h expiry) · bcryptjs |
| Alerts | Nodemailer / SMTP |
| Deploy | Docker Compose |
cd project/backend
cp .env.example .env # set JWT_SECRET and SMTP vars
docker compose up -d # postgres:16 + node app on :3000# Terminal 1 — database
cd project/backend
cp .env.example .env
docker compose up db -d # postgres on :5432
# Terminal 2 — backend
cd project/backend
npm install
npm run dev # nodemon → http://localhost:3000
# Terminal 3 — frontend
cd project/frontend
npm install
npm run dev # Vite → http://localhost:5173Default credentials: admin / admin123 · viewer / viewer123
project/
├── backend/
│ ├── src/
│ │ ├── app.js # Express entry point, middleware, routes
│ │ ├── db.js # pg Pool, seed users + services
│ │ ├── worker.js # Check scheduler (setInterval), auto-incident logic
│ │ ├── ws.js # WebSocket server, broadcasts check results
│ │ ├── logger.js # Winston logger
│ │ ├── schema.sql # PostgreSQL schema, indexes, triggers
│ │ └── routes/
│ │ ├── auth.js
│ │ ├── services.js
│ │ ├── checks.js
│ │ ├── incidents.js
│ │ └── monitoring.js
│ ├── Dockerfile
│ ├── docker-compose.yml
│ └── .env.example
│
├── frontend/
│ ├── src/
│ │ ├── engine/
│ │ │ └── engine.ts # State machine, pub/sub, API client, WebSocket
│ │ ├── types/
│ │ │ └── noc.types.ts # All shared TypeScript interfaces
│ │ ├── components/
│ │ │ ├── ui/ # StatusBadge, Btn, Modal, ConfirmDialog, EmptyState…
│ │ │ ├── Sidebar.tsx # Nav + provider status, mobile drawer
│ │ │ ├── TopBar.tsx # Title, WS indicator, theme toggle, hamburger
│ │ │ ├── LoginScreen.tsx
│ │ │ ├── ConnectionBanner.tsx
│ │ │ └── ToastContainer.tsx
│ │ ├── views/
│ │ │ ├── DashboardView.tsx
│ │ │ ├── ServicesView.tsx
│ │ │ ├── IncidentsView.tsx
│ │ │ └── AlertsView.tsx
│ │ ├── App.tsx
│ │ └── index.css # CSS custom properties for theming
│ ├── tailwind.config.js
│ └── vite.config.ts
│
└── old-ver/ # Original CDN/JSX prototype (reference only)
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
HTTP server port |
DATABASE_URL |
— | PostgreSQL connection string |
JWT_SECRET |
— | Change in production |
JWT_EXPIRES_IN |
8h |
Token lifetime |
CHECK_INTERVAL_SECONDS |
30 |
How often to run all checks |
CHECK_TIMEOUT_MS |
5000 |
Per-check HTTP/TCP timeout |
CHECK_RETRIES |
3 |
Consecutive failures before marking DOWN |
LATENCY_THRESHOLD_MS |
300 |
Response time above this → DEGRADED |
SMTP_HOST / SMTP_USER / SMTP_PASS |
— | Email alert config (Nodemailer) |
CORS_ORIGIN |
* |
Allowed origin for CORS (set to frontend URL in production) |
CHECKS_RETENTION_DAYS |
7 |
Days to keep raw check records |
EVENTS_RETENTION_DAYS |
30 |
Days to keep event log entries |
ALERTS_RETENTION_DAYS |
90 |
Days to keep sent alert records |
DATA_ENCRYPTION_KEY |
— | Required. 32-byte key (hex or base64) used by AES-256-GCM for encryption at rest. Generate with openssl rand -base64 32. |
AUTH_LOCKOUT_THRESHOLD |
5 |
Failed logins in window before account is temporarily locked |
AUTH_LOCKOUT_WINDOW_MIN |
10 |
Lockout window in minutes |
AUTH_EVENTS_RETENTION_DAYS |
90 |
Days to keep auth audit trail |
If you upgraded from a version before encryption-at-rest was added:
# 1. Pick a key and persist it to .env (NEVER commit it).
openssl rand -base64 32 # copy output → DATA_ENCRYPTION_KEY=...
# 2. Widen sensitive columns and create the auth_events table.
docker exec -i noc_db psql -U noc -d nocdb < backend/src/migrations/002_encrypt_sensitive.sql
docker exec -i noc_db psql -U noc -d nocdb < backend/src/migrations/003_auth_events.sql
# 3. Encrypt existing rows in place. Idempotent — safe to re-run.
cd backend && node src/migrations/002_encrypt_existing.jsThe decrypt path tolerates plaintext during the rollout, so step 3 can be deferred without breaking reads. Losing DATA_ENCRYPTION_KEY makes the encrypted columns unreadable — back it up alongside JWT_SECRET.
Probe results expose network topology. In production, terminate TLS in front of the Node API so probe payloads, login credentials, and WebSocket frames travel encrypted end-to-end. Caddy is the simplest option (auto-renewed Let's Encrypt + automatic WS upgrade):
# /etc/caddy/Caddyfile
noc.example.com {
encode zstd gzip
# WebSocket upgrade is handled automatically by reverse_proxy.
reverse_proxy localhost:3000
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
Referrer-Policy "no-referrer"
}
}Then point the frontend at https://noc.example.com/api and wss://noc.example.com/ws. The backend already calls app.set('trust proxy', 1) so req.ip resolves to the real client behind Caddy — required for the auth audit trail and IP-mismatch detection.
