feat: The Other Dude v9.0.1 — full-featured email system
ci: add GitHub Pages deployment workflow for docs site Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
117
docs/API.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# API Reference
|
||||
|
||||
## Overview
|
||||
|
||||
TOD exposes a REST API built with FastAPI. Interactive documentation is available at:
|
||||
|
||||
- Swagger UI: `http://<host>:<port>/docs` (dev environment only)
|
||||
- ReDoc: `http://<host>:<port>/redoc` (dev environment only)
|
||||
|
||||
Both Swagger and ReDoc are disabled in staging/production environments.
|
||||
|
||||
## Authentication
|
||||
|
||||
### SRP-6a Login
|
||||
|
||||
- `POST /api/auth/login` -- SRP-6a authentication (returns JWT access + refresh tokens)
|
||||
- `POST /api/auth/refresh` -- Refresh an expired access token
|
||||
- `POST /api/auth/logout` -- Invalidate the current session
|
||||
|
||||
All authenticated endpoints require one of:
|
||||
|
||||
- `Authorization: Bearer <token>` header
|
||||
- httpOnly cookie (set automatically by the login flow)
|
||||
|
||||
Access tokens expire after 15 minutes. Refresh tokens are valid for 7 days.
|
||||
|
||||
### API Key Authentication
|
||||
|
||||
- Create API keys in Admin > API Keys
|
||||
- Use header: `X-API-Key: mktp_<key>`
|
||||
- Keys have operator-level RBAC permissions
|
||||
- Prefix: `mktp_`, stored as SHA-256 hash
|
||||
|
||||
## Endpoint Groups
|
||||
|
||||
All API routes are mounted under the `/api` prefix.
|
||||
|
||||
| Group | Prefix | Description |
|
||||
|-------|--------|-------------|
|
||||
| Auth | `/api/auth/*` | Login, register, SRP exchange, password reset, token refresh |
|
||||
| Tenants | `/api/tenants/*` | Tenant/organization CRUD |
|
||||
| Users | `/api/users/*` | User management, RBAC role assignment |
|
||||
| Devices | `/api/devices/*` | Device CRUD, scanning, status |
|
||||
| Device Groups | `/api/device-groups/*` | Logical device grouping |
|
||||
| Device Tags | `/api/device-tags/*` | Tag-based device labeling |
|
||||
| Metrics | `/api/metrics/*` | TimescaleDB device metrics (CPU, memory, traffic) |
|
||||
| Config Backups | `/api/config-backups/*` | Automated RouterOS config backup history |
|
||||
| Config Editor | `/api/config-editor/*` | Live RouterOS config browsing and editing |
|
||||
| Firmware | `/api/firmware/*` | RouterOS firmware version management and upgrades |
|
||||
| Alerts | `/api/alerts/*` | Alert rule CRUD, alert history |
|
||||
| Events | `/api/events/*` | Device event log |
|
||||
| Device Logs | `/api/device-logs/*` | RouterOS syslog entries |
|
||||
| Templates | `/api/templates/*` | Config templates for batch operations |
|
||||
| Clients | `/api/clients/*` | Connected client (DHCP lease) data |
|
||||
| Topology | `/api/topology/*` | Network topology map data |
|
||||
| SSE | `/api/sse/*` | Server-Sent Events for real-time updates |
|
||||
| Audit Logs | `/api/audit-logs/*` | Immutable audit trail |
|
||||
| Reports | `/api/reports/*` | PDF report generation (Jinja2 + WeasyPrint) |
|
||||
| API Keys | `/api/api-keys/*` | API key CRUD |
|
||||
| Maintenance Windows | `/api/maintenance-windows/*` | Scheduled maintenance window management |
|
||||
| VPN | `/api/vpn/*` | WireGuard VPN tunnel management |
|
||||
| Certificates | `/api/certificates/*` | Internal CA and device certificate management |
|
||||
| Transparency | `/api/transparency/*` | KMS access event dashboard |
|
||||
|
||||
## Health Checks
|
||||
|
||||
| Endpoint | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `GET /health` | Liveness | Always returns 200 if the API process is alive. Response includes `version`. |
|
||||
| `GET /health/ready` | Readiness | Returns 200 only when PostgreSQL, Redis, and NATS are all healthy. Returns 503 otherwise. |
|
||||
| `GET /api/health` | Liveness | Backward-compatible alias under `/api` prefix. |
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
- Auth endpoints: 5 requests/minute per IP
|
||||
- General endpoints: no global rate limit (per-route limits may apply)
|
||||
|
||||
Rate limit violations return HTTP 429 with a JSON error body.
|
||||
|
||||
## Error Format
|
||||
|
||||
All error responses use a standard JSON format:
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "Human-readable error message"
|
||||
}
|
||||
```
|
||||
|
||||
HTTP status codes follow REST conventions:
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 400 | Bad request / validation error |
|
||||
| 401 | Unauthorized (missing or expired token) |
|
||||
| 403 | Forbidden (insufficient RBAC permissions) |
|
||||
| 404 | Resource not found |
|
||||
| 409 | Conflict (duplicate resource) |
|
||||
| 422 | Unprocessable entity (Pydantic validation) |
|
||||
| 429 | Rate limit exceeded |
|
||||
| 500 | Internal server error |
|
||||
| 503 | Service unavailable (readiness check failed) |
|
||||
|
||||
## RBAC Roles
|
||||
|
||||
Endpoints enforce role-based access control. The four roles in descending privilege order:
|
||||
|
||||
| Role | Scope | Description |
|
||||
|------|-------|-------------|
|
||||
| `super_admin` | Global (no tenant) | Full platform access, tenant management |
|
||||
| `admin` | Tenant | Full access within their tenant |
|
||||
| `operator` | Tenant | Device operations, config changes |
|
||||
| `viewer` | Tenant | Read-only access |
|
||||
|
||||
## Multi-Tenancy
|
||||
|
||||
Tenant isolation is enforced at the database level via PostgreSQL Row-Level Security (RLS). The `app_user` database role automatically filters all queries by the authenticated user's `tenant_id`. Super admins operate outside tenant scope.
|
||||
329
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,329 @@
|
||||
# Architecture
|
||||
|
||||
## System Overview
|
||||
|
||||
TOD (The Other Dude) is a containerized MSP fleet management platform for MikroTik RouterOS devices. It uses a three-service architecture: a React frontend, a Python FastAPI backend, and a Go poller. All services communicate through PostgreSQL, Redis, and NATS JetStream. Multi-tenancy is enforced at the database level via PostgreSQL Row-Level Security (RLS).
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────────┐ ┌──────────────┐
|
||||
│ Frontend │────▶│ Backend API │◀───▶│ Go Poller │
|
||||
│ React/nginx │ │ FastAPI │ │ go-routeros │
|
||||
└─────────────┘ └────────┬────────┘ └──────┬───────┘
|
||||
│ │
|
||||
┌──────────────┼──────────────────────┤
|
||||
│ │ │
|
||||
┌────────▼──┐ ┌──────▼──────┐ ┌──────────▼──┐
|
||||
│ Redis │ │ PostgreSQL │ │ NATS │
|
||||
│ locks, │ │ 17 + Timescale│ │ JetStream │
|
||||
│ cache │ │ DB + RLS │ │ pub/sub │
|
||||
└───────────┘ └─────────────┘ └─────────────┘
|
||||
│
|
||||
┌──────▼──────┐
|
||||
│ OpenBao │
|
||||
│ Transit KMS │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
## Services
|
||||
|
||||
### Frontend (React / nginx)
|
||||
|
||||
- **Stack**: React 19, TypeScript, TanStack Router (file-based routing), TanStack Query (data fetching), Tailwind CSS 3.4, Vite
|
||||
- **Production**: Static build served by nginx on port 80 (exposed as port 3000)
|
||||
- **Development**: Vite dev server with hot module replacement
|
||||
- **Design system**: Geist Sans + Geist Mono fonts, HSL color tokens via CSS custom properties, class-based dark/light mode
|
||||
- **Real-time**: Server-Sent Events (SSE) for live device status updates, alerts, and operation progress
|
||||
- **Client-side encryption**: SRP-6a authentication flow with 2SKD key derivation; Emergency Kit PDF generation
|
||||
- **UX features**: Command palette (Cmd+K), Framer Motion page transitions, collapsible sidebar, skeleton loaders
|
||||
- **Memory limit**: 64MB
|
||||
|
||||
### Backend API (FastAPI)
|
||||
|
||||
- **Stack**: Python 3.12+, FastAPI 0.115+, SQLAlchemy 2.0 async, asyncpg, Gunicorn
|
||||
- **Two database engines**:
|
||||
- `admin_engine` (superuser) -- used only for auth/bootstrap and NATS subscribers that need cross-tenant access
|
||||
- `app_engine` (non-superuser `app_user` role) -- used for all device/data routes, enforces RLS
|
||||
- **Authentication**: JWT tokens (15min access, 7d refresh), SRP-6a zero-knowledge proof, RBAC (super_admin, admin, operator, viewer)
|
||||
- **NATS subscribers**: Three independent subscribers for device status, metrics, and firmware events. Non-fatal startup -- API serves requests even if NATS is unavailable
|
||||
- **Background services**: APScheduler for nightly config backups and daily firmware version checks
|
||||
- **OpenBao integration**: Provisions per-tenant Transit encryption keys on startup, dual-read fallback if OpenBao is unavailable
|
||||
- **Startup sequence**: Configure logging -> Run Alembic migrations -> Bootstrap first admin -> Start NATS subscribers -> Ensure SSE streams -> Start schedulers -> Provision OpenBao keys
|
||||
- **API documentation**: OpenAPI docs at `/docs` and `/redoc` (dev environment only)
|
||||
- **Health endpoints**: `/health` (liveness), `/health/ready` (readiness -- checks PostgreSQL, Redis, NATS)
|
||||
- **Middleware stack** (LIFO order): RequestID -> SecurityHeaders -> RateLimiting -> CORS -> Route handler
|
||||
- **Memory limit**: 512MB
|
||||
|
||||
#### API Routers
|
||||
|
||||
The backend exposes 21 route groups under the `/api` prefix:
|
||||
|
||||
| Router | Purpose |
|
||||
|--------|---------|
|
||||
| `auth` | Login (SRP-6a + legacy), token refresh, registration |
|
||||
| `tenants` | Tenant CRUD (super_admin only) |
|
||||
| `users` | User management, RBAC |
|
||||
| `devices` | Device CRUD, status, commands |
|
||||
| `device_groups` | Logical device grouping |
|
||||
| `device_tags` | Tagging and filtering |
|
||||
| `metrics` | Time-series metrics (TimescaleDB) |
|
||||
| `config_backups` | Configuration backup history |
|
||||
| `config_editor` | Live RouterOS config editing |
|
||||
| `firmware` | Firmware version tracking and upgrades |
|
||||
| `alerts` | Alert rules and active alerts |
|
||||
| `events` | Device event log |
|
||||
| `device_logs` | RouterOS system logs |
|
||||
| `templates` | Configuration templates |
|
||||
| `clients` | Connected client devices |
|
||||
| `topology` | Network topology (ReactFlow data) |
|
||||
| `sse` | Server-Sent Events streams |
|
||||
| `audit_logs` | Immutable audit trail |
|
||||
| `reports` | PDF report generation (Jinja2 + weasyprint) |
|
||||
| `api_keys` | API key management (mktp_ prefix) |
|
||||
| `maintenance_windows` | Scheduled maintenance with alert suppression |
|
||||
| `vpn` | WireGuard VPN management |
|
||||
| `certificates` | Internal CA and device TLS certificates |
|
||||
| `transparency` | KMS access event dashboard |
|
||||
|
||||
### Go Poller
|
||||
|
||||
- **Stack**: Go 1.23, go-routeros/v3, pgx/v5, nats.go
|
||||
- **Polling model**: Synchronous per-device polling on a configurable interval (default 60s)
|
||||
- **Device communication**: RouterOS binary API over TLS (port 8729), InsecureSkipVerify for self-signed certs
|
||||
- **TLS fallback**: Three-tier strategy -- CA-verified -> InsecureSkipVerify -> plain API
|
||||
- **Distributed locking**: Redis locks prevent concurrent polling of the same device (safe for multi-instance deployment)
|
||||
- **Circuit breaker**: Backs off from unreachable devices to avoid wasting poll cycles
|
||||
- **Credential decryption**: OpenBao Transit with LRU cache (1024 entries, 5min TTL) to minimize KMS calls
|
||||
- **Output**: Publishes poll results to NATS JetStream; the API's NATS subscribers process and persist them
|
||||
- **Database access**: Uses `poller_user` role which bypasses RLS (needs cross-tenant device access)
|
||||
- **VPN routing**: Adds static route to WireGuard gateway for reaching remote devices
|
||||
- **Memory limit**: 256MB
|
||||
|
||||
## Infrastructure Services
|
||||
|
||||
### PostgreSQL 17 + TimescaleDB
|
||||
|
||||
- **Image**: `timescale/timescaledb:2.17.2-pg17`
|
||||
- **Row-Level Security (RLS)**: Enforces tenant isolation at the database level. All data tables have a `tenant_id` column; RLS policies filter by `current_setting('app.tenant_id')`
|
||||
- **Database roles**:
|
||||
- `postgres` (superuser) -- admin engine, auth/bootstrap, migrations
|
||||
- `app_user` (non-superuser) -- RLS-enforced, used by API for data routes
|
||||
- `poller_user` -- bypasses RLS, used by Go poller for cross-tenant device access
|
||||
- **TimescaleDB hypertables**: Time-series storage for device metrics (CPU, memory, interface traffic, etc.)
|
||||
- **Migrations**: Alembic, run automatically on API startup
|
||||
- **Initialization**: `scripts/init-postgres.sql` creates roles and enables extensions
|
||||
- **Data volume**: `./docker-data/postgres`
|
||||
- **Memory limit**: 512MB
|
||||
|
||||
### Redis
|
||||
|
||||
- **Image**: `redis:7-alpine`
|
||||
- **Uses**:
|
||||
- Distributed locking for the Go poller (prevents concurrent polling of the same device)
|
||||
- Rate limiting on auth endpoints (5 requests/min)
|
||||
- Credential cache for OpenBao Transit responses
|
||||
- **Data volume**: `./docker-data/redis`
|
||||
- **Memory limit**: 128MB
|
||||
|
||||
### NATS JetStream
|
||||
|
||||
- **Image**: `nats:2-alpine`
|
||||
- **Role**: Message bus between the Go poller and the Python API
|
||||
- **Streams**: DEVICE_EVENTS (poll results, status changes), ALERT_EVENTS (SSE delivery), OPERATION_EVENTS (SSE delivery)
|
||||
- **Durable consumers**: Ensure no message loss during API restarts
|
||||
- **Monitoring port**: 8222
|
||||
- **Data volume**: `./docker-data/nats`
|
||||
- **Memory limit**: 128MB
|
||||
|
||||
### OpenBao (HashiCorp Vault fork)
|
||||
|
||||
- **Image**: `openbao/openbao:2.1`
|
||||
- **Mode**: Dev server (auto-unsealed, in-memory storage)
|
||||
- **Transit secrets engine**: Provides envelope encryption for device credentials at rest
|
||||
- **Per-tenant keys**: Each tenant gets a dedicated Transit encryption key
|
||||
- **Init script**: `infrastructure/openbao/init.sh` enables Transit engine and creates initial keys
|
||||
- **Dev token**: `dev-openbao-token` (must be replaced in production)
|
||||
- **Memory limit**: 256MB
|
||||
|
||||
### WireGuard
|
||||
|
||||
- **Image**: `lscr.io/linuxserver/wireguard`
|
||||
- **Role**: VPN gateway for reaching RouterOS devices on remote networks
|
||||
- **Port**: 51820/UDP
|
||||
- **Routing**: API and Poller containers add static routes through the WireGuard container to reach device subnets (e.g., `10.10.0.0/16`)
|
||||
- **Data volume**: `./docker-data/wireguard`
|
||||
- **Memory limit**: 128MB
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Device Polling Cycle
|
||||
|
||||
```
|
||||
Go Poller Redis OpenBao RouterOS NATS API PostgreSQL
|
||||
│ │ │ │ │ │ │
|
||||
├──query device list──────▶│ │ │ │ │ │
|
||||
│◀─────────────────────────┤ │ │ │ │ │
|
||||
├──acquire lock────────────▶│ │ │ │ │ │
|
||||
│◀──lock granted───────────┤ │ │ │ │ │
|
||||
├──decrypt credentials (cache miss)────────▶│ │ │ │ │
|
||||
│◀──plaintext credentials──────────────────┤ │ │ │ │
|
||||
├──binary API (8729 TLS)───────────────────────────────────▶│ │ │ │
|
||||
│◀──system info, interfaces, metrics───────────────────────┤ │ │ │
|
||||
├──publish poll result──────────────────────────────────────────────────▶│ │ │
|
||||
│ │ │ │ │ ──subscribe──▶│ │
|
||||
│ │ │ │ │ ├──upsert data──▶│
|
||||
├──release lock────────────▶│ │ │ │ │ │
|
||||
```
|
||||
|
||||
1. Poller queries PostgreSQL for the list of active devices
|
||||
2. Acquires a Redis distributed lock per device (prevents duplicate polling)
|
||||
3. Decrypts device credentials via OpenBao Transit (LRU cache avoids repeated KMS calls)
|
||||
4. Connects to the RouterOS binary API on port 8729 over TLS
|
||||
5. Collects system info, interface stats, routing tables, and metrics
|
||||
6. Publishes results to NATS JetStream
|
||||
7. API NATS subscriber processes results and upserts into PostgreSQL
|
||||
8. Releases Redis lock
|
||||
|
||||
### Config Push (Two-Phase with Panic Revert)
|
||||
|
||||
```
|
||||
Frontend API RouterOS
|
||||
│ │ │
|
||||
├──push config─▶│ │
|
||||
│ ├──apply config─▶│
|
||||
│ ├──set revert timer─▶│
|
||||
│ │◀──ack────────┤
|
||||
│◀──pending────┤ │
|
||||
│ │ │ (timer counting down)
|
||||
├──confirm─────▶│ │
|
||||
│ ├──cancel timer─▶│
|
||||
│ │◀──ack────────┤
|
||||
│◀──confirmed──┤ │
|
||||
```
|
||||
|
||||
1. Frontend sends config commands to the API
|
||||
2. API connects to the device and applies the configuration
|
||||
3. Sets a revert timer on the device (RouterOS safe mode / scheduler)
|
||||
4. Returns pending status to the frontend
|
||||
5. User confirms the change works (e.g., connectivity still up)
|
||||
6. If confirmed: API cancels the revert timer, config is permanent
|
||||
7. If timeout or rejected: device automatically reverts to the previous configuration
|
||||
|
||||
This pattern prevents lockouts from misconfigured firewall rules or IP changes.
|
||||
|
||||
### Authentication (SRP-6a Zero-Knowledge Proof)
|
||||
|
||||
```
|
||||
Browser API PostgreSQL
|
||||
│ │ │
|
||||
│──register────────────────▶│ │
|
||||
│ (email, salt, verifier) │──store verifier──────▶│
|
||||
│ │ │
|
||||
│──login step 1────────────▶│ │
|
||||
│ (email, client_public) │──lookup verifier─────▶│
|
||||
│◀──(salt, server_public)──┤◀─────────────────────┤
|
||||
│ │ │
|
||||
│──login step 2────────────▶│ │
|
||||
│ (client_proof) │──verify proof────────│
|
||||
│◀──(server_proof, JWT)────┤ │
|
||||
```
|
||||
|
||||
1. **Registration**: Client derives a verifier from `password + secret_key` using PBKDF2 (650K iterations) + HKDF + XOR (2SKD). Only the salt and verifier are sent to the server -- never the password
|
||||
2. **Login step 1**: Client sends email and ephemeral public value; server responds with stored salt and its own ephemeral public value
|
||||
3. **Login step 2**: Client computes a proof from the shared session key; server validates the proof without ever seeing the password
|
||||
4. **Token issuance**: On successful proof, server issues JWT (15min access + 7d refresh)
|
||||
5. **Emergency Kit**: A downloadable PDF containing the user's secret key for account recovery
|
||||
|
||||
## Multi-Tenancy Model
|
||||
|
||||
- Every data table includes a `tenant_id` column
|
||||
- PostgreSQL RLS policies filter rows by `current_setting('app.tenant_id')`
|
||||
- The API sets tenant context (`SET app.tenant_id = ...`) on each database session
|
||||
- `super_admin` role has NULL `tenant_id` and can access all tenants
|
||||
- `poller_user` bypasses RLS intentionally (needs cross-tenant device access for polling)
|
||||
- Tenant isolation is enforced at the database level, not the application level -- even a compromised API cannot leak cross-tenant data through `app_user` connections
|
||||
|
||||
## Security Layers
|
||||
|
||||
| Layer | Mechanism | Purpose |
|
||||
|-------|-----------|---------|
|
||||
| **Authentication** | SRP-6a | Zero-knowledge proof -- password never transmitted or stored |
|
||||
| **Key Derivation** | 2SKD (PBKDF2 650K + HKDF + XOR) | Two-secret key derivation from password + secret key |
|
||||
| **Encryption at Rest** | OpenBao Transit | Envelope encryption for device credentials |
|
||||
| **Tenant Isolation** | PostgreSQL RLS | Database-level row filtering by tenant_id |
|
||||
| **Access Control** | JWT + RBAC | Role-based permissions (super_admin, admin, operator, viewer) |
|
||||
| **Rate Limiting** | Redis-backed | Auth endpoints limited to 5 requests/min |
|
||||
| **TLS Certificates** | Internal CA | Certificate management and deployment to RouterOS devices |
|
||||
| **Security Headers** | Middleware | CSP, SRI hashes on JS bundles, X-Frame-Options, etc. |
|
||||
| **Secret Validation** | Startup check | Rejects known-insecure defaults in non-dev environments |
|
||||
|
||||
## Network Topology
|
||||
|
||||
All services communicate over a single Docker bridge network (`tod`). External ports:
|
||||
|
||||
| Service | Internal Port | External Port | Protocol |
|
||||
|---------|--------------|---------------|----------|
|
||||
| Frontend | 80 | 3000 | HTTP |
|
||||
| API | 8000 | 8001 | HTTP |
|
||||
| PostgreSQL | 5432 | 5432 | TCP |
|
||||
| Redis | 6379 | 6379 | TCP |
|
||||
| NATS | 4222 | 4222 | TCP |
|
||||
| NATS Monitor | 8222 | 8222 | HTTP |
|
||||
| OpenBao | 8200 | 8200 | HTTP |
|
||||
| WireGuard | 51820 | 51820 | UDP |
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
backend/ FastAPI Python backend
|
||||
app/
|
||||
main.py Application entry point, lifespan, router registration
|
||||
config.py Pydantic Settings configuration
|
||||
database.py SQLAlchemy engines (admin + app_user)
|
||||
models/ SQLAlchemy ORM models
|
||||
routers/ FastAPI route handlers (21 modules)
|
||||
services/ Business logic, NATS subscribers, schedulers
|
||||
middleware/ Rate limiting, request ID, security headers
|
||||
frontend/ React TypeScript frontend
|
||||
src/
|
||||
routes/ TanStack Router file-based routes
|
||||
components/ Reusable UI components
|
||||
lib/ API client, crypto, utilities
|
||||
poller/ Go microservice for device polling
|
||||
main.go Entry point
|
||||
Dockerfile Multi-stage build
|
||||
infrastructure/ Deployment configuration
|
||||
docker/ Dockerfiles for api, frontend
|
||||
helm/ Kubernetes Helm charts
|
||||
openbao/ OpenBao init scripts
|
||||
scripts/ Database init scripts
|
||||
docker-compose.yml Infrastructure services (postgres, redis, nats, openbao, wireguard)
|
||||
docker-compose.override.yml Application services for dev (api, poller, frontend)
|
||||
```
|
||||
|
||||
## Running the Stack
|
||||
|
||||
```bash
|
||||
# Infrastructure only (postgres, redis, nats, openbao, wireguard)
|
||||
docker compose up -d
|
||||
|
||||
# Full stack including application services (api, poller, frontend)
|
||||
docker compose up -d # override.yml is auto-loaded in dev
|
||||
|
||||
# Build images sequentially to avoid OOM on low-RAM machines
|
||||
docker compose build api
|
||||
docker compose build poller
|
||||
docker compose build frontend
|
||||
```
|
||||
|
||||
## Container Memory Limits
|
||||
|
||||
| Service | Limit |
|
||||
|---------|-------|
|
||||
| PostgreSQL | 512MB |
|
||||
| API | 512MB |
|
||||
| Go Poller | 256MB |
|
||||
| OpenBao | 256MB |
|
||||
| Redis | 128MB |
|
||||
| NATS | 128MB |
|
||||
| WireGuard | 128MB |
|
||||
| Frontend (nginx) | 64MB |
|
||||
127
docs/CONFIGURATION.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# Configuration Reference
|
||||
|
||||
TOD uses Pydantic Settings for configuration. All values can be set via environment variables or a `.env` file in the backend working directory.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Application
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `APP_NAME` | `TOD - The Other Dude` | Application display name |
|
||||
| `APP_VERSION` | `0.1.0` | Semantic version string |
|
||||
| `ENVIRONMENT` | `dev` | Runtime environment: `dev`, `staging`, or `production` |
|
||||
| `DEBUG` | `false` | Enable debug mode |
|
||||
| `CORS_ORIGINS` | `http://localhost:3000,http://localhost:5173,http://localhost:8080` | Comma-separated list of allowed CORS origins |
|
||||
| `APP_BASE_URL` | `http://localhost:5173` | Frontend base URL (used in password reset emails) |
|
||||
|
||||
### Authentication & JWT
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `JWT_SECRET_KEY` | *(insecure dev default)* | HMAC signing key for JWTs. **Must be changed in production.** Generate with: `python -c "import secrets; print(secrets.token_urlsafe(64))"` |
|
||||
| `JWT_ALGORITHM` | `HS256` | JWT signing algorithm |
|
||||
| `JWT_ACCESS_TOKEN_EXPIRE_MINUTES` | `15` | Access token lifetime in minutes |
|
||||
| `JWT_REFRESH_TOKEN_EXPIRE_DAYS` | `7` | Refresh token lifetime in days |
|
||||
| `PASSWORD_RESET_TOKEN_EXPIRE_MINUTES` | `30` | Password reset link validity in minutes |
|
||||
|
||||
### Database
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `DATABASE_URL` | `postgresql+asyncpg://postgres:postgres@localhost:5432/mikrotik` | Admin (superuser) async database URL. Used for migrations and bootstrap operations. |
|
||||
| `SYNC_DATABASE_URL` | `postgresql+psycopg2://postgres:postgres@localhost:5432/mikrotik` | Synchronous database URL used by Alembic migrations only. |
|
||||
| `APP_USER_DATABASE_URL` | `postgresql+asyncpg://app_user:app_password@localhost:5432/mikrotik` | Non-superuser async database URL. Enforces PostgreSQL RLS for tenant isolation. |
|
||||
| `DB_POOL_SIZE` | `20` | App user connection pool size |
|
||||
| `DB_MAX_OVERFLOW` | `40` | App user pool max overflow connections |
|
||||
| `DB_ADMIN_POOL_SIZE` | `10` | Admin connection pool size |
|
||||
| `DB_ADMIN_MAX_OVERFLOW` | `20` | Admin pool max overflow connections |
|
||||
|
||||
### Security
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `CREDENTIAL_ENCRYPTION_KEY` | *(insecure dev default)* | AES-256-GCM encryption key for device credentials at rest. Must be exactly 32 bytes, base64-encoded. **Must be changed in production.** Generate with: `python -c "import secrets, base64; print(base64.b64encode(secrets.token_bytes(32)).decode())"` |
|
||||
|
||||
### OpenBao / Vault (KMS)
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `OPENBAO_ADDR` | `http://localhost:8200` | OpenBao Transit server address for per-tenant envelope encryption |
|
||||
| `OPENBAO_TOKEN` | *(insecure dev default)* | OpenBao authentication token. **Must be changed in production.** |
|
||||
|
||||
### NATS
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `NATS_URL` | `nats://localhost:4222` | NATS JetStream server URL for pub/sub between Go poller and Python API |
|
||||
|
||||
### Redis
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `REDIS_URL` | `redis://localhost:6379/0` | Redis URL for caching, distributed locks, and rate limiting |
|
||||
|
||||
### SMTP (Notifications)
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `SMTP_HOST` | `localhost` | SMTP server hostname |
|
||||
| `SMTP_PORT` | `587` | SMTP server port |
|
||||
| `SMTP_USER` | *(none)* | SMTP authentication username |
|
||||
| `SMTP_PASSWORD` | *(none)* | SMTP authentication password |
|
||||
| `SMTP_USE_TLS` | `false` | Enable STARTTLS for SMTP connections |
|
||||
| `SMTP_FROM_ADDRESS` | `noreply@mikrotik-portal.local` | Sender address for outbound emails |
|
||||
|
||||
### Firmware
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `FIRMWARE_CACHE_DIR` | `/data/firmware-cache` | Path to firmware download cache (PVC mount in production) |
|
||||
| `FIRMWARE_CHECK_INTERVAL_HOURS` | `24` | Hours between automatic RouterOS version checks |
|
||||
|
||||
### Storage Paths
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `GIT_STORE_PATH` | `./git-store` | Path to bare git repos for config backup history (one repo per tenant). In production: `/data/git-store` on a ReadWriteMany PVC. |
|
||||
| `WIREGUARD_CONFIG_PATH` | `/data/wireguard` | Shared volume path for WireGuard configuration files |
|
||||
|
||||
### Bootstrap
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `FIRST_ADMIN_EMAIL` | *(none)* | Email for the initial super_admin user. Only used if no users exist in the database. |
|
||||
| `FIRST_ADMIN_PASSWORD` | *(none)* | Password for the initial super_admin user. The user is created with `must_upgrade_auth=True`, triggering SRP registration on first login. |
|
||||
|
||||
## Production Safety
|
||||
|
||||
TOD refuses to start in `staging` or `production` environments if any of these variables still have their insecure dev defaults:
|
||||
|
||||
- `JWT_SECRET_KEY`
|
||||
- `CREDENTIAL_ENCRYPTION_KEY`
|
||||
- `OPENBAO_TOKEN`
|
||||
|
||||
The process exits with code 1 and a clear error message indicating which variable needs to be rotated.
|
||||
|
||||
## Docker Compose Profiles
|
||||
|
||||
| Profile | Command | Services |
|
||||
|---------|---------|----------|
|
||||
| *(default)* | `docker compose up -d` | Infrastructure only: PostgreSQL, Redis, NATS, OpenBao |
|
||||
| `full` | `docker compose --profile full up -d` | All services: infrastructure + API, Poller, Frontend |
|
||||
|
||||
## Container Memory Limits
|
||||
|
||||
All containers have enforced memory limits to prevent OOM on the host:
|
||||
|
||||
| Service | Memory Limit |
|
||||
|---------|-------------|
|
||||
| PostgreSQL | 512 MB |
|
||||
| Redis | 128 MB |
|
||||
| NATS | 128 MB |
|
||||
| API | 512 MB |
|
||||
| Poller | 256 MB |
|
||||
| Frontend | 64 MB |
|
||||
|
||||
Build Docker images sequentially (not in parallel) to avoid OOM during builds.
|
||||
257
docs/DEPLOYMENT.md
Normal file
@@ -0,0 +1,257 @@
|
||||
# TOD - The Other Dude — Deployment Guide
|
||||
|
||||
## Overview
|
||||
|
||||
TOD (The Other Dude) is a containerized fleet management platform for RouterOS devices. This guide covers Docker Compose deployment for production environments.
|
||||
|
||||
### Architecture
|
||||
|
||||
- **Backend API** (Python/FastAPI) -- REST API with JWT authentication and PostgreSQL RLS
|
||||
- **Go Poller** -- Polls RouterOS devices via binary API, publishes events to NATS
|
||||
- **Frontend** (React/nginx) -- Single-page application served by nginx
|
||||
- **PostgreSQL + TimescaleDB** -- Primary database with time-series extensions
|
||||
- **Redis** -- Distributed locking and rate limiting
|
||||
- **NATS JetStream** -- Message bus for device events
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker Engine 24+ with Docker Compose v2
|
||||
- At least 4GB RAM (2GB absolute minimum -- builds are memory-intensive)
|
||||
- External SSD or fast storage recommended for Docker volumes
|
||||
- Network access to RouterOS devices on ports 8728 (API) and 8729 (API-SSL)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Clone and Configure
|
||||
|
||||
```bash
|
||||
git clone <repository-url> tod
|
||||
cd tod
|
||||
|
||||
# Copy environment template
|
||||
cp .env.example .env.prod
|
||||
```
|
||||
|
||||
### 2. Generate Secrets
|
||||
|
||||
```bash
|
||||
# Generate JWT secret
|
||||
python3 -c "import secrets; print(secrets.token_urlsafe(64))"
|
||||
|
||||
# Generate credential encryption key (32 bytes, base64-encoded)
|
||||
python3 -c "import secrets, base64; print(base64.b64encode(secrets.token_bytes(32)).decode())"
|
||||
```
|
||||
|
||||
Edit `.env.prod` with the generated values:
|
||||
|
||||
```env
|
||||
ENVIRONMENT=production
|
||||
JWT_SECRET_KEY=<generated-jwt-secret>
|
||||
CREDENTIAL_ENCRYPTION_KEY=<generated-encryption-key>
|
||||
POSTGRES_PASSWORD=<strong-password>
|
||||
|
||||
# First admin user (created on first startup)
|
||||
FIRST_ADMIN_EMAIL=admin@example.com
|
||||
FIRST_ADMIN_PASSWORD=<strong-password>
|
||||
```
|
||||
|
||||
### 3. Build Images
|
||||
|
||||
Build images **one at a time** to avoid out-of-memory crashes on constrained hosts:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.prod.yml build api
|
||||
docker compose -f docker-compose.yml -f docker-compose.prod.yml build poller
|
||||
docker compose -f docker-compose.yml -f docker-compose.prod.yml build frontend
|
||||
```
|
||||
|
||||
### 4. Start the Stack
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env.prod up -d
|
||||
```
|
||||
|
||||
### 5. Verify
|
||||
|
||||
```bash
|
||||
# Check all services are running
|
||||
docker compose ps
|
||||
|
||||
# Check API health (liveness)
|
||||
curl http://localhost:8000/health
|
||||
|
||||
# Check readiness (PostgreSQL, Redis, NATS connected)
|
||||
curl http://localhost:8000/health/ready
|
||||
|
||||
# Access the portal
|
||||
open http://localhost
|
||||
```
|
||||
|
||||
Log in with the `FIRST_ADMIN_EMAIL` and `FIRST_ADMIN_PASSWORD` credentials set in step 2.
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
### Required Variables
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `ENVIRONMENT` | Deployment environment | `production` |
|
||||
| `JWT_SECRET_KEY` | JWT signing secret (min 32 chars) | `<generated>` |
|
||||
| `CREDENTIAL_ENCRYPTION_KEY` | AES-256 key for device credentials (base64) | `<generated>` |
|
||||
| `POSTGRES_PASSWORD` | PostgreSQL superuser password | `<strong-password>` |
|
||||
| `FIRST_ADMIN_EMAIL` | Initial admin account email | `admin@example.com` |
|
||||
| `FIRST_ADMIN_PASSWORD` | Initial admin account password | `<strong-password>` |
|
||||
|
||||
### Optional Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `GUNICORN_WORKERS` | `2` | API worker process count |
|
||||
| `DB_POOL_SIZE` | `20` | App database connection pool size |
|
||||
| `DB_MAX_OVERFLOW` | `40` | Max overflow connections above pool |
|
||||
| `DB_ADMIN_POOL_SIZE` | `10` | Admin database connection pool size |
|
||||
| `DB_ADMIN_MAX_OVERFLOW` | `20` | Admin max overflow connections |
|
||||
| `POLL_INTERVAL_SECONDS` | `60` | Device polling interval |
|
||||
| `CONNECTION_TIMEOUT_SECONDS` | `10` | RouterOS connection timeout |
|
||||
| `COMMAND_TIMEOUT_SECONDS` | `30` | RouterOS per-command timeout |
|
||||
| `CIRCUIT_BREAKER_MAX_FAILURES` | `5` | Consecutive failures before backoff |
|
||||
| `CIRCUIT_BREAKER_BASE_BACKOFF_SECONDS` | `30` | Initial backoff duration |
|
||||
| `CIRCUIT_BREAKER_MAX_BACKOFF_SECONDS` | `900` | Maximum backoff (15 min) |
|
||||
| `LOG_LEVEL` | `info` | Logging verbosity (`debug`/`info`/`warn`/`error`) |
|
||||
| `CORS_ORIGINS` | `http://localhost:3000` | Comma-separated CORS origins |
|
||||
|
||||
### Security Notes
|
||||
|
||||
- **Never use default secrets in production.** The application refuses to start if it detects known insecure defaults (like the dev JWT secret) in non-dev environments.
|
||||
- **Credential encryption key** is used to encrypt RouterOS device passwords at rest. Losing this key means re-entering all device credentials.
|
||||
- **CORS_ORIGINS** should be set to your actual domain in production.
|
||||
- **RLS enforcement**: The app_user database role enforces row-level security. Tenants cannot access each other's data even with a compromised JWT.
|
||||
|
||||
## Storage Configuration
|
||||
|
||||
Docker volumes mount to the host filesystem. Default locations are configured in `docker-compose.yml`:
|
||||
|
||||
- **PostgreSQL data**: `./docker-data/postgres`
|
||||
- **Redis data**: `./docker-data/redis`
|
||||
- **NATS data**: `./docker-data/nats`
|
||||
- **Git store (config backups)**: `./docker-data/git-store`
|
||||
|
||||
To change storage locations, edit the volume mounts in `docker-compose.yml`.
|
||||
|
||||
## Resource Limits
|
||||
|
||||
Container memory limits are enforced in `docker-compose.prod.yml` to prevent OOM crashes:
|
||||
|
||||
| Service | Memory Limit |
|
||||
|---------|-------------|
|
||||
| PostgreSQL | 512MB |
|
||||
| Redis | 128MB |
|
||||
| NATS | 128MB |
|
||||
| API | 512MB |
|
||||
| Poller | 256MB |
|
||||
| Frontend | 64MB |
|
||||
|
||||
Adjust under `deploy.resources.limits.memory` in `docker-compose.prod.yml`.
|
||||
|
||||
## API Documentation
|
||||
|
||||
The backend serves interactive API documentation at:
|
||||
|
||||
- **Swagger UI**: `http://localhost:8000/docs`
|
||||
- **ReDoc**: `http://localhost:8000/redoc`
|
||||
|
||||
All endpoints include descriptions, request/response schemas, and authentication requirements.
|
||||
|
||||
## Monitoring (Optional)
|
||||
|
||||
Enable Prometheus and Grafana monitoring with the observability compose overlay:
|
||||
|
||||
```bash
|
||||
docker compose \
|
||||
-f docker-compose.yml \
|
||||
-f docker-compose.prod.yml \
|
||||
-f docker-compose.observability.yml \
|
||||
--env-file .env.prod up -d
|
||||
```
|
||||
|
||||
- **Prometheus**: `http://localhost:9090`
|
||||
- **Grafana**: `http://localhost:3001` (default: admin/admin)
|
||||
|
||||
### Exported Metrics
|
||||
|
||||
The API and poller export Prometheus metrics:
|
||||
|
||||
| Metric | Source | Description |
|
||||
|--------|--------|-------------|
|
||||
| `http_requests_total` | API | HTTP request count by method, path, status |
|
||||
| `http_request_duration_seconds` | API | Request latency histogram |
|
||||
| `mikrotik_poll_total` | Poller | Poll cycles by status (success/error/skipped) |
|
||||
| `mikrotik_poll_duration_seconds` | Poller | Poll cycle duration histogram |
|
||||
| `mikrotik_devices_active` | Poller | Number of devices being polled |
|
||||
| `mikrotik_circuit_breaker_skips_total` | Poller | Polls skipped due to backoff |
|
||||
| `mikrotik_nats_publish_total` | Poller | NATS publishes by subject and status |
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Backup Strategy
|
||||
|
||||
- **Database**: Use `pg_dump` or configure PostgreSQL streaming replication
|
||||
- **Config backups**: Git repositories in the git-store volume (automatic nightly backups)
|
||||
- **Encryption key**: Store `CREDENTIAL_ENCRYPTION_KEY` securely -- required to decrypt device credentials
|
||||
|
||||
### Updating
|
||||
|
||||
```bash
|
||||
git pull
|
||||
docker compose -f docker-compose.yml -f docker-compose.prod.yml build api
|
||||
docker compose -f docker-compose.yml -f docker-compose.prod.yml build poller
|
||||
docker compose -f docker-compose.yml -f docker-compose.prod.yml build frontend
|
||||
docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env.prod up -d
|
||||
```
|
||||
|
||||
Database migrations run automatically on API startup via Alembic.
|
||||
|
||||
### Logs
|
||||
|
||||
```bash
|
||||
# All services
|
||||
docker compose logs -f
|
||||
|
||||
# Specific service
|
||||
docker compose logs -f api
|
||||
|
||||
# Filter structured JSON logs with jq
|
||||
docker compose logs api --no-log-prefix 2>&1 | jq 'select(.event != null)'
|
||||
|
||||
# View audit logs (config editor operations)
|
||||
docker compose logs api --no-log-prefix 2>&1 | jq 'select(.event | startswith("routeros_"))'
|
||||
```
|
||||
|
||||
### Graceful Shutdown
|
||||
|
||||
All services handle SIGTERM for graceful shutdown:
|
||||
|
||||
- **API (gunicorn)**: Finishes in-flight requests within `GUNICORN_GRACEFUL_TIMEOUT` (default 30s), then disposes database connection pools
|
||||
- **Poller (Go)**: Cancels all device polling goroutines via context propagation, waits for in-flight polls to complete
|
||||
- **Frontend (nginx)**: Stops accepting new connections and finishes serving active requests
|
||||
|
||||
```bash
|
||||
# Graceful stop (sends SIGTERM, waits 30s)
|
||||
docker compose stop
|
||||
|
||||
# Restart a single service
|
||||
docker compose restart api
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| API won't start with secret error | Generate production secrets (see step 2 above) |
|
||||
| Build crashes with OOM | Build images one at a time (see step 3 above) |
|
||||
| Device shows offline | Check network access to device API port (8728/8729) |
|
||||
| Health check fails | Check `docker compose logs api` for startup errors |
|
||||
| Rate limited (429) | Wait 60 seconds or check Redis connectivity |
|
||||
| Migration fails | Check `docker compose logs api` for Alembic errors |
|
||||
| NATS subscriber won't start | Non-fatal -- API runs without NATS; check NATS container health |
|
||||
| Poller circuit breaker active | Device unreachable; check `CIRCUIT_BREAKER_*` env vars to tune backoff |
|
||||
203
docs/README.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# The Other Dude
|
||||
|
||||
**Fleet management for MikroTik RouterOS devices.** Built for MSPs who manage hundreds of routers across multiple tenants. Think "UniFi Controller, but for MikroTik."
|
||||
|
||||
The Other Dude is a self-hosted, multi-tenant platform that gives you centralized visibility, configuration management, real-time monitoring, and zero-knowledge security across your entire MikroTik fleet -- from a single pane of glass.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### Fleet
|
||||
|
||||
- **Dashboard** -- At-a-glance fleet health with device counts, uptime sparklines, and status breakdowns per organization.
|
||||
- **Device Management** -- Detailed device pages with system info, interfaces, routes, firewall rules, DHCP leases, and real-time resource metrics.
|
||||
- **Fleet Table** -- Virtual-scrolled table (TanStack Virtual) that handles hundreds of devices without breaking a sweat.
|
||||
- **Device Map** -- Geographic view of device locations.
|
||||
- **Subnet Scanner** -- Discover new RouterOS devices on your network and onboard them in clicks.
|
||||
|
||||
### Configuration
|
||||
|
||||
- **Config Editor** -- Browse and edit RouterOS configuration sections with a structured command interface. Two-phase config push with automatic panic-revert ensures you never brick a remote device.
|
||||
- **Batch Config** -- Apply configuration changes across multiple devices simultaneously with template support.
|
||||
- **Bulk Commands** -- Execute arbitrary RouterOS commands across device groups.
|
||||
- **Templates** -- Reusable configuration templates with variable substitution.
|
||||
- **Simple Config** -- A Linksys/Ubiquiti-style simplified interface covering Internet, LAN/DHCP, WiFi, Port Forwarding, Firewall, DNS, and System settings. No RouterOS CLI knowledge required.
|
||||
- **Config Backup & Diff** -- Git-backed configuration storage with full version history and side-by-side diffs. Restore any previous configuration with one click.
|
||||
|
||||
### Monitoring
|
||||
|
||||
- **Network Topology** -- Interactive topology map (ReactFlow + Dagre layout) showing device interconnections and shared subnets.
|
||||
- **Real-Time Metrics** -- Live CPU, memory, disk, and interface traffic via Server-Sent Events (SSE) backed by NATS JetStream.
|
||||
- **Alert Rules** -- Configurable threshold-based alerts for any metric (CPU > 90%, interface down, uptime reset, etc.).
|
||||
- **Notification Channels** -- Route alerts to email, webhooks, or Slack.
|
||||
- **Audit Trail** -- Immutable log of every action taken in the portal, with user attribution and exportable records.
|
||||
- **Transparency Dashboard** -- KMS access event monitoring for tenant admins (who accessed what encryption keys, when).
|
||||
- **Reports** -- Generate PDF reports (fleet summary, device detail, security audit, performance) with Jinja2 + WeasyPrint.
|
||||
|
||||
### Security
|
||||
|
||||
- **Zero-Knowledge Architecture** -- 1Password-style hybrid design. SRP-6a authentication means the server never sees your password. Two-Secret Key Derivation (2SKD) with PBKDF2 (650K iterations) + HKDF + XOR.
|
||||
- **Secret Key** -- 128-bit `A3-XXXXXX` format key stored in IndexedDB with Emergency Kit PDF export.
|
||||
- **OpenBao KMS** -- Per-tenant envelope encryption via Transit secret engine. Go poller uses LRU cache (1024 keys / 5-min TTL) for performance.
|
||||
- **Internal Certificate Authority** -- Issue and deploy TLS certificates to RouterOS devices via SFTP. Three-tier TLS fallback: CA-verified, InsecureSkipVerify, plain API.
|
||||
- **WireGuard VPN** -- Manage WireGuard tunnels for secure device access across NAT boundaries.
|
||||
- **Credential Encryption** -- AES-256-GCM (Fernet) encryption of all stored device credentials at rest.
|
||||
- **RBAC** -- Four roles: `super_admin`, `admin`, `operator`, `viewer`. PostgreSQL Row-Level Security enforces tenant isolation at the database layer.
|
||||
|
||||
### Administration
|
||||
|
||||
- **Multi-Tenancy** -- Full organization isolation with PostgreSQL RLS. Super admins manage all tenants; tenant admins see only their own devices and users.
|
||||
- **User Management** -- Per-tenant user administration with role assignment.
|
||||
- **API Keys** -- Generate `mktp_`-prefixed API keys with SHA-256 hash storage and operator-level RBAC for automation and integrations.
|
||||
- **Firmware Management** -- Track RouterOS versions across your fleet, plan upgrades, and push firmware updates.
|
||||
- **Maintenance Windows** -- Schedule maintenance periods with automatic alert suppression.
|
||||
- **Setup Wizard** -- Guided 3-step onboarding for first-time deployment.
|
||||
|
||||
### UX
|
||||
|
||||
- **Command Palette** -- `Cmd+K` / `Ctrl+K` quick navigation (cmdk).
|
||||
- **Keyboard Shortcuts** -- Vim-style sequence shortcuts (`g d` for dashboard, `g t` for topology, `[` to toggle sidebar).
|
||||
- **Dark / Light Mode** -- Class-based theming with flicker-free initialization.
|
||||
- **Page Transitions** -- Smooth route transitions with Framer Motion.
|
||||
- **Skeleton Loaders** -- Shimmer-gradient loading states throughout the UI.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
+-----------+
|
||||
| Frontend |
|
||||
| React/nginx|
|
||||
+-----+-----+
|
||||
|
|
||||
/api/ proxy
|
||||
|
|
||||
+-----v-----+
|
||||
| API |
|
||||
| FastAPI |
|
||||
+--+--+--+--+
|
||||
| | |
|
||||
+-------------+ | +--------------+
|
||||
| | |
|
||||
+-----v------+ +-----v-----+ +-------v-------+
|
||||
| PostgreSQL | | Redis | | NATS |
|
||||
| TimescaleDB | | (locks, | | JetStream |
|
||||
| (RLS) | | caching) | | (pub/sub) |
|
||||
+-----^------+ +-----^-----+ +-------^-------+
|
||||
| | |
|
||||
+-----+-------+-------+---------+-------+
|
||||
| Poller (Go) |
|
||||
| Polls RouterOS devices via binary API |
|
||||
| port 8729 TLS |
|
||||
+----------------------------------------+
|
||||
|
|
||||
+--------v---------+
|
||||
| RouterOS Fleet |
|
||||
| (your devices) |
|
||||
+-------------------+
|
||||
```
|
||||
|
||||
- **Frontend** serves the React SPA via nginx and proxies `/api/` to the backend.
|
||||
- **API** handles all business logic, authentication, and database access with RLS-enforced tenant isolation.
|
||||
- **Poller** is a Go microservice that polls RouterOS devices on a configurable interval using the RouterOS binary API, publishing results to NATS and persisting to PostgreSQL.
|
||||
- **PostgreSQL + TimescaleDB** stores all relational data with hypertables for time-series metrics.
|
||||
- **Redis** provides distributed locks (one poller per device) and rate limiting.
|
||||
- **NATS JetStream** delivers real-time events from the poller to the API (and onward to the frontend via SSE).
|
||||
- **OpenBao** provides Transit secret engine for per-tenant envelope encryption (zero-knowledge key management).
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|-----------|
|
||||
| Frontend | React 19, TanStack Router + Query, Tailwind CSS 3.4, Vite, Framer Motion |
|
||||
| Backend | Python 3.12, FastAPI 0.115, SQLAlchemy 2.0 async, asyncpg, Pydantic v2 |
|
||||
| Poller | Go 1.24, go-routeros/v3, pgx/v5, nats.go |
|
||||
| Database | PostgreSQL 17 + TimescaleDB 2.17, Row-Level Security |
|
||||
| Cache | Redis 7 |
|
||||
| Message Bus | NATS with JetStream |
|
||||
| KMS | OpenBao 2.1 (Transit secret engine) |
|
||||
| VPN | WireGuard |
|
||||
| Auth | SRP-6a (zero-knowledge), JWT (15m access / 7d refresh) |
|
||||
| Reports | Jinja2 + WeasyPrint (PDF generation) |
|
||||
| Containerization | Docker Compose (dev, staging, production profiles) |
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
See the full [Quick Start Guide](../QUICKSTART.md) for detailed instructions.
|
||||
|
||||
```bash
|
||||
# Clone and configure
|
||||
cp .env.example .env
|
||||
|
||||
# Start infrastructure
|
||||
docker compose up -d
|
||||
|
||||
# Build app images (one at a time to avoid OOM)
|
||||
docker compose build api
|
||||
docker compose build poller
|
||||
docker compose build frontend
|
||||
|
||||
# Start the full stack
|
||||
docker compose up -d
|
||||
|
||||
# Verify
|
||||
curl http://localhost:8001/health
|
||||
open http://localhost:3000
|
||||
```
|
||||
|
||||
Three environment profiles are available:
|
||||
|
||||
| Environment | Frontend | API | Notes |
|
||||
|-------------|----------|-----|-------|
|
||||
| Dev | `localhost:3000` | `localhost:8001` | Hot-reload, volume-mounted source |
|
||||
| Staging | `localhost:3080` | `localhost:8081` | Built images, staging secrets |
|
||||
| Production | `localhost` (port 80) | Internal (proxied) | Gunicorn workers, log rotation |
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
| Document | Description |
|
||||
|----------|-------------|
|
||||
| [Quick Start](../QUICKSTART.md) | Get running in minutes |
|
||||
| [Deployment Guide](DEPLOYMENT.md) | Production deployment, TLS, backups |
|
||||
| [Architecture](ARCHITECTURE.md) | System design, data flows, multi-tenancy |
|
||||
| [Security Model](SECURITY.md) | Zero-knowledge auth, encryption, RLS, RBAC |
|
||||
| [User Guide](USER-GUIDE.md) | End-user guide for all features |
|
||||
| [API Reference](API.md) | REST API endpoints and authentication |
|
||||
| [Configuration](CONFIGURATION.md) | Environment variables and tuning |
|
||||
|
||||
---
|
||||
|
||||
## Screenshots
|
||||
|
||||
See the [documentation site](https://theotherdude.net) for screenshots.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
backend/ Python FastAPI backend
|
||||
frontend/ React TypeScript frontend
|
||||
poller/ Go microservice for device polling
|
||||
infrastructure/ Helm charts, Dockerfiles, OpenBao init
|
||||
docs/ Documentation
|
||||
docker-compose.yml Base compose (infrastructure services)
|
||||
docker-compose.override.yml Dev overrides (hot-reload)
|
||||
docker-compose.staging.yml Staging profile
|
||||
docker-compose.prod.yml Production profile
|
||||
docker-compose.observability.yml Prometheus + Grafana
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
Open-source. Self-hosted. Your data stays on your infrastructure.
|
||||
149
docs/SECURITY.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# Security Model
|
||||
|
||||
## Overview
|
||||
|
||||
TOD (The Other Dude) implements a 1Password-inspired zero-knowledge security architecture. The server never stores or sees user passwords. All data is stored on infrastructure you own and control — no external telemetry, analytics, or third-party data transmission.
|
||||
|
||||
## Authentication: SRP-6a Zero-Knowledge Proof
|
||||
|
||||
TOD uses the Secure Remote Password (SRP-6a) protocol for authentication, ensuring the server never receives, transmits, or stores user passwords.
|
||||
|
||||
- **SRP-6a protocol:** Password is verified via a zero-knowledge proof — only a cryptographic verifier derived from the password is stored on the server, never the password itself.
|
||||
- **Two-Secret Key Derivation (2SKD):** Combines the user password with a 128-bit Secret Key using a multi-step derivation process, ensuring that compromise of either factor alone is insufficient.
|
||||
- **Key derivation pipeline:** PBKDF2 with 650,000 iterations + HKDF expansion + XOR combination of both factors.
|
||||
- **Secret Key format:** `A3-XXXXXX` (128-bit), stored exclusively in the browser's IndexedDB. The server never sees or stores the Secret Key.
|
||||
- **Emergency Kit:** Downloadable PDF containing the Secret Key for account recovery. Generated client-side.
|
||||
- **Session management:** JWT tokens with 15-minute access token lifetime and 7-day refresh token lifetime, delivered via httpOnly cookies.
|
||||
- **SRP session state:** Ephemeral SRP handshake data stored in Redis with automatic expiration.
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
```
|
||||
Client Server
|
||||
| |
|
||||
| POST /auth/srp/init {email} |
|
||||
|------------------------------------>|
|
||||
| {salt, server_ephemeral_B} |
|
||||
|<------------------------------------|
|
||||
| |
|
||||
| [Client derives session key from |
|
||||
| password + Secret Key + salt + B] |
|
||||
| |
|
||||
| POST /auth/srp/verify {A, M1} |
|
||||
|------------------------------------>|
|
||||
| [Server verifies M1 proof] |
|
||||
| {M2, access_token, refresh_token} |
|
||||
|<------------------------------------|
|
||||
```
|
||||
|
||||
## Credential Encryption
|
||||
|
||||
Device credentials (RouterOS usernames and passwords) are encrypted at rest using envelope encryption:
|
||||
|
||||
- **Encryption algorithm:** AES-256-GCM (via Fernet symmetric encryption).
|
||||
- **Key management:** OpenBao Transit secrets engine provides the master encryption keys.
|
||||
- **Per-tenant isolation:** Each tenant has its own encryption key in OpenBao Transit.
|
||||
- **Envelope encryption:** Data is encrypted with a data encryption key (DEK), which is itself encrypted by the tenant's Transit key.
|
||||
- **Go poller decryption:** The poller service decrypts credentials at runtime via the Transit API, with an LRU cache (1,024 entries, 5-minute TTL) to reduce KMS round-trips.
|
||||
- **CA private keys:** Encrypted with AES-256-GCM before database storage. PEM key material is never logged.
|
||||
|
||||
## Tenant Isolation
|
||||
|
||||
Multi-tenancy is enforced at the database level, making cross-tenant data access structurally impossible:
|
||||
|
||||
- **PostgreSQL Row-Level Security (RLS):** All data tables have RLS policies that filter rows by `tenant_id`.
|
||||
- **`app_user` database role:** All application queries run through a non-superuser role that enforces RLS. Even a SQL injection attack cannot cross tenant boundaries.
|
||||
- **Session context:** `tenant_id` is set via PostgreSQL session variables (`SET app.current_tenant`) on every request, derived from the authenticated user's JWT.
|
||||
- **`super_admin` role:** Users with NULL `tenant_id` can access all tenants for platform administration. Represented as `'super_admin'` in the RLS context.
|
||||
- **`poller_user` role:** Bypasses RLS by design — the polling service needs cross-tenant device access to poll all devices. This is an intentional security trade-off documented in the architecture.
|
||||
|
||||
## Role-Based Access Control (RBAC)
|
||||
|
||||
| Role | Scope | Capabilities |
|
||||
|------|-------|-------------|
|
||||
| `super_admin` | Global | Full system access, tenant management, user management across all tenants |
|
||||
| `admin` | Tenant | Manage devices, users, settings, certificates within their tenant |
|
||||
| `operator` | Tenant | Device operations, configuration changes, monitoring |
|
||||
| `viewer` | Tenant | Read-only access to devices, metrics, and dashboards |
|
||||
|
||||
- RBAC is enforced at both the API middleware layer and database level.
|
||||
- API keys inherit the `operator` permission level and are scoped to a single tenant.
|
||||
- API key tokens use the `mktp_` prefix and are stored as SHA-256 hashes (the plaintext token is shown once at creation and never stored).
|
||||
|
||||
## Internal Certificate Authority
|
||||
|
||||
TOD includes a per-tenant Internal Certificate Authority for managing TLS certificates on RouterOS devices:
|
||||
|
||||
- **Per-tenant CA:** Each tenant can generate its own self-signed Certificate Authority.
|
||||
- **Device certificate lifecycle:** Certificates follow a state machine: `issued` -> `deploying` -> `deployed` -> `expiring`/`revoked`/`superseded`.
|
||||
- **Deployment:** Certificates are deployed to devices via SFTP.
|
||||
- **Three-tier TLS fallback:** The Go poller attempts connections in order:
|
||||
1. CA-verified TLS (using the tenant's CA certificate)
|
||||
2. InsecureSkipVerify TLS (for self-signed RouterOS certs)
|
||||
3. Plain API connection (fallback)
|
||||
- **Key protection:** CA private keys are encrypted with AES-256-GCM before database storage. PEM key material is never logged or exposed via API responses.
|
||||
- **Certificate rotation and revocation:** Supported via the certificate lifecycle state machine.
|
||||
|
||||
## Network Security
|
||||
|
||||
- **RouterOS communication:** All device communication uses the RouterOS binary API over TLS (port 8729). InsecureSkipVerify is enabled by default because RouterOS devices typically use self-signed certificates.
|
||||
- **CORS enforcement:** Strict CORS policy in production, configured via `CORS_ORIGINS` environment variable.
|
||||
- **Rate limiting:** Authentication endpoints are rate-limited to 5 requests per minute per IP to prevent brute-force attacks.
|
||||
- **Cookie security:** httpOnly cookies prevent JavaScript access to session tokens. The `Secure` flag is auto-detected based on whether CORS origins use HTTPS.
|
||||
|
||||
## Data Protection
|
||||
|
||||
- **Config backups:** Encrypted at rest via OpenBao Transit envelope encryption before database storage.
|
||||
- **Audit logs:** Encrypted at rest via Transit encryption — audit log content is protected even from database administrators.
|
||||
- **Subresource Integrity (SRI):** SHA-384 hashes on JavaScript bundles prevent tampering with frontend code.
|
||||
- **Content Security Policy (CSP):** Strict CSP headers prevent XSS, code injection, and unauthorized resource loading.
|
||||
- **No external dependencies:** Fully self-hosted with no external analytics, telemetry, CDNs, or third-party services. The only outbound connections are:
|
||||
- RouterOS firmware update checks (no device data sent)
|
||||
- SMTP for email notifications (if configured)
|
||||
- Webhooks for alerts (if configured)
|
||||
|
||||
## Security Headers
|
||||
|
||||
The following security headers are enforced on all responses:
|
||||
|
||||
| Header | Value | Purpose |
|
||||
|--------|-------|---------|
|
||||
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` | Force HTTPS connections |
|
||||
| `X-Content-Type-Options` | `nosniff` | Prevent MIME-type sniffing |
|
||||
| `X-Frame-Options` | `DENY` | Prevent clickjacking via iframes |
|
||||
| `Content-Security-Policy` | Strict policy | Prevent XSS and code injection |
|
||||
| `Referrer-Policy` | `strict-origin-when-cross-origin` | Limit referrer information leakage |
|
||||
|
||||
## Audit Trail
|
||||
|
||||
- **Immutable audit log:** All significant actions are recorded in the `audit_logs` table — logins, configuration changes, device operations, admin actions.
|
||||
- **Fire-and-forget logging:** The `log_action()` function records audit events asynchronously without blocking the main request.
|
||||
- **Per-tenant access:** Tenants can only view their own audit logs (enforced by RLS).
|
||||
- **Encryption at rest:** Audit log content is encrypted via OpenBao Transit.
|
||||
- **CSV export:** Audit logs can be exported in CSV format for compliance and reporting.
|
||||
- **Account deletion:** When a user deletes their account, audit log entries are anonymized (PII removed) but the action records are retained for security compliance.
|
||||
|
||||
## Data Retention
|
||||
|
||||
| Data Type | Retention | Notes |
|
||||
|-----------|-----------|-------|
|
||||
| User accounts | Until deleted | Users can self-delete from Settings |
|
||||
| Device metrics | 90 days | Purged by TimescaleDB retention policy |
|
||||
| Configuration backups | Indefinite | Stored in git repositories on your server |
|
||||
| Audit logs | Indefinite | Anonymized on account deletion |
|
||||
| API keys | Until revoked | Cascade-deleted with user account |
|
||||
| Encrypted key material | Until user deleted | Cascade-deleted with user account |
|
||||
| Session data (Redis) | 15 min / 7 days | Auto-expiring access/refresh tokens |
|
||||
| Password reset tokens | 30 minutes | Auto-expire |
|
||||
| SRP session state | Short-lived | Auto-expire in Redis |
|
||||
|
||||
## GDPR Compliance
|
||||
|
||||
TOD provides built-in tools for GDPR compliance:
|
||||
|
||||
- **Right of Access (Art. 15):** Users can view their account information on the Settings page.
|
||||
- **Right to Data Portability (Art. 20):** Users can export all personal data in JSON format from Settings.
|
||||
- **Right to Erasure (Art. 17):** Users can permanently delete their account and all associated data. Audit logs are anonymized (PII removed) with a deletion receipt generated for compliance verification.
|
||||
- **Right to Rectification (Art. 16):** Account information can be updated by the tenant administrator.
|
||||
|
||||
As a self-hosted application, the deployment operator is the data controller and is responsible for compliance with applicable data protection laws.
|
||||
246
docs/USER-GUIDE.md
Normal file
@@ -0,0 +1,246 @@
|
||||
# TOD - The Other Dude: User Guide
|
||||
|
||||
MSP fleet management platform for MikroTik RouterOS devices.
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
### First Login
|
||||
|
||||
1. Navigate to the portal URL provided by your administrator.
|
||||
2. Log in with the admin credentials created during initial deployment.
|
||||
3. Complete **SRP security enrollment** -- the portal uses zero-knowledge authentication (SRP-6a), so a unique Secret Key is generated for your account.
|
||||
4. **Save your Emergency Kit PDF immediately.** This PDF contains your Secret Key, which you will need to log in from any new browser or device. Without it, you cannot recover access.
|
||||
5. Complete the **Setup Wizard** to create your first organization and add your first device.
|
||||
|
||||
### Setup Wizard
|
||||
|
||||
The Setup Wizard launches automatically for first-time super_admin users. It walks through three steps:
|
||||
|
||||
- **Step 1 -- Create Organization**: Enter a name for your tenant (organization). This is the top-level container for all your devices, users, and configuration.
|
||||
- **Step 2 -- Add Device**: Enter the IP address, API port (default 8729 for TLS), and RouterOS credentials for your first device. The portal will attempt to connect and verify the device.
|
||||
- **Step 3 -- Verify & Complete**: The portal polls the device to confirm connectivity. Once verified, you are taken to the dashboard.
|
||||
|
||||
You can always add more organizations and devices later from the sidebar.
|
||||
|
||||
---
|
||||
|
||||
## Navigation
|
||||
|
||||
TOD uses a collapsible sidebar with four sections. Press `[` to toggle the sidebar between expanded (240px) and collapsed (48px) views. On mobile, the sidebar opens as an overlay.
|
||||
|
||||
### Fleet
|
||||
|
||||
| Item | Description |
|
||||
|------|-------------|
|
||||
| **Dashboard** | Overview of your fleet with device status cards, active alerts, and metrics sparklines. The landing page after login. |
|
||||
| **Devices** | Fleet table with search, sort, and filter. Click any device row to open its detail page. |
|
||||
| **Map** | Geographic map view of device locations. |
|
||||
|
||||
### Manage
|
||||
|
||||
| Item | Description |
|
||||
|------|-------------|
|
||||
| **Config Editor** | Browse and edit RouterOS configuration paths in real-time. Select a device from the header dropdown. |
|
||||
| **Batch Config** | Apply configuration changes across multiple devices simultaneously using templates. |
|
||||
| **Bulk Commands** | Execute RouterOS CLI commands across selected devices in bulk. |
|
||||
| **Templates** | Create and manage reusable configuration templates. |
|
||||
| **Firmware** | Check for RouterOS updates and schedule firmware upgrades across your fleet. |
|
||||
| **Maintenance** | Schedule maintenance windows to suppress alerts during planned work. |
|
||||
| **VPN** | WireGuard VPN tunnel management -- create, deploy, and monitor tunnels between devices. |
|
||||
| **Certificates** | Internal Certificate Authority management -- generate, deploy, and rotate TLS certificates for your devices. |
|
||||
### Monitor
|
||||
|
||||
| Item | Description |
|
||||
|------|-------------|
|
||||
| **Topology** | Interactive network map showing device connections and shared subnets, rendered with ReactFlow and Dagre layout. |
|
||||
| **Alerts** | Live alert feed with filtering by severity (info, warning, critical) and acknowledgment actions. |
|
||||
| **Alert Rules** | Define threshold-based alert rules on device metrics with configurable severity and notification channels. |
|
||||
| **Audit Trail** | Immutable, append-only log of all operations -- configuration changes, logins, user management, and admin actions. |
|
||||
| **Transparency** | KMS access event dashboard showing encryption key usage across your organization (admin only). |
|
||||
| **Reports** | Generate and export PDF reports: fleet summary, device health, compliance, and SLA. |
|
||||
|
||||
### Admin
|
||||
|
||||
| Item | Description |
|
||||
|------|-------------|
|
||||
| **Users** | User management with role-based access control (RBAC). Assign roles: super_admin, admin, operator, viewer. |
|
||||
| **Organizations** | Create and manage tenants for multi-tenant MSP operation. Each tenant has isolated data via PostgreSQL row-level security. |
|
||||
| **API Keys** | Generate and manage programmatic access tokens (prefixed `mktp_`) with operator-level permissions. |
|
||||
| **Settings** | System configuration, theme toggle (dark/light), and profile settings. |
|
||||
| **About** | Platform version, feature summary, and project information. |
|
||||
|
||||
---
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| `Cmd+K` / `Ctrl+K` | Open command palette for quick navigation and actions |
|
||||
| `[` | Toggle sidebar collapsed/expanded |
|
||||
| `?` | Show keyboard shortcut help dialog |
|
||||
| `g d` | Go to Dashboard |
|
||||
| `g f` | Go to Firmware |
|
||||
| `g t` | Go to Topology |
|
||||
| `g a` | Go to Alerts |
|
||||
|
||||
The command palette (`Cmd+K`) provides fuzzy search across all pages, devices, and common actions. It is accessible in both dark and light themes.
|
||||
|
||||
---
|
||||
|
||||
## Device Management
|
||||
|
||||
### Adding Devices
|
||||
|
||||
There are several ways to add devices to your fleet:
|
||||
|
||||
1. **Setup Wizard** -- automatically offered on first login.
|
||||
2. **Fleet Table** -- click the "Add Device" button from the Devices page.
|
||||
3. **Subnet Scanner** -- enter a CIDR range (e.g., `192.168.1.0/24`) to auto-discover MikroTik devices on the network.
|
||||
|
||||
When adding a device, provide:
|
||||
|
||||
- **IP Address** -- the management IP of the RouterOS device.
|
||||
- **API Port** -- default is 8729 (TLS). The portal connects via the RouterOS binary API protocol.
|
||||
- **Credentials** -- username and password for the device. Credentials are encrypted at rest with AES-256-GCM.
|
||||
|
||||
### Device Detail Page
|
||||
|
||||
Click any device in the fleet table to open its detail page. Tabs include:
|
||||
|
||||
| Tab | Description |
|
||||
|-----|-------------|
|
||||
| **Overview** | System info, uptime, hardware model, RouterOS version, resource usage, and interface status summary. |
|
||||
| **Interfaces** | Real-time traffic graphs for each network interface. |
|
||||
| **Config** | Browse the full device configuration tree by RouterOS path. |
|
||||
| **Firewall** | View and manage firewall filter rules, NAT rules, and address lists. |
|
||||
| **DHCP** | Active DHCP leases, server configuration, and address pools. |
|
||||
| **Backups** | Configuration backup timeline with side-by-side diff viewer to compare changes over time. |
|
||||
| **Clients** | Connected clients and wireless registrations. |
|
||||
|
||||
### Config Editor
|
||||
|
||||
The Config Editor provides direct access to RouterOS configuration paths (e.g., `/ip/address`, `/ip/firewall/filter`, `/interface/bridge`).
|
||||
|
||||
- Select a device from the header dropdown.
|
||||
- Navigate the configuration tree to browse, add, edit, or delete entries.
|
||||
- Two apply modes are available:
|
||||
- **Standard Apply** -- changes are applied immediately.
|
||||
- **Safe Apply** -- two-phase commit with automatic panic-revert. Changes are applied, and you have a confirmation window to accept them. If the confirmation times out (device becomes unreachable), changes automatically revert to prevent lockouts.
|
||||
|
||||
Safe Apply is strongly recommended for firewall rules and routing changes on remote devices.
|
||||
|
||||
### Simple Config
|
||||
|
||||
Simple Config provides a consumer-router-style interface modeled after Linksys and Ubiquiti UIs. It is designed for operators who prefer guided configuration over raw RouterOS paths.
|
||||
|
||||
Seven category tabs:
|
||||
|
||||
1. **Internet** -- WAN connection type, PPPoE, DHCP client settings.
|
||||
2. **LAN / DHCP** -- LAN addressing, DHCP server and pool configuration.
|
||||
3. **WiFi** -- Wireless SSID, security, and channel settings.
|
||||
4. **Port Forwarding** -- NAT destination rules for inbound services.
|
||||
5. **Firewall** -- Simplified firewall rule management.
|
||||
6. **DNS** -- DNS server and static DNS entries.
|
||||
7. **System** -- Device identity, timezone, NTP, admin password.
|
||||
|
||||
Toggle between **Simple** (guided) and **Standard** (full config editor) modes at any time. Per-device settings are stored in browser localStorage.
|
||||
|
||||
---
|
||||
|
||||
## Monitoring & Alerts
|
||||
|
||||
### Alert Rules
|
||||
|
||||
Create threshold-based rules that fire when device metrics cross defined boundaries:
|
||||
|
||||
- Select the metric to monitor (CPU, memory, disk, interface traffic, uptime, etc.).
|
||||
- Set the threshold value and comparison operator.
|
||||
- Choose severity: **info**, **warning**, or **critical**.
|
||||
- Assign one or more notification channels.
|
||||
|
||||
### Notification Channels
|
||||
|
||||
Alerts can be delivered through multiple channels:
|
||||
|
||||
| Channel | Description |
|
||||
|---------|-------------|
|
||||
| **Email** | SMTP-based email notifications. Configure server, port, and recipients. |
|
||||
| **Webhook** | HTTP POST to any URL with a JSON payload containing alert details. |
|
||||
| **Slack** | Slack incoming webhook with Block Kit formatting for rich alert messages. |
|
||||
|
||||
### Maintenance Windows
|
||||
|
||||
Schedule maintenance periods to suppress alerts during planned work:
|
||||
|
||||
- Define start and end times.
|
||||
- Apply to specific devices or fleet-wide.
|
||||
- Alerts generated during the window are recorded but do not trigger notifications.
|
||||
- Maintenance windows can be recurring or one-time.
|
||||
|
||||
---
|
||||
|
||||
## Reports
|
||||
|
||||
Generate PDF reports from the Reports page. Four report types are available:
|
||||
|
||||
| Report | Content |
|
||||
|--------|---------|
|
||||
| **Fleet Summary** | Overall fleet health, device counts by status, top alerts, and aggregate statistics. |
|
||||
| **Device Health** | Per-device detailed report with hardware info, resource trends, and recent events. |
|
||||
| **Compliance** | Security posture audit -- firmware versions, default credentials, firewall policy checks. |
|
||||
| **SLA** | Uptime and availability metrics over a selected period with percentage calculations. |
|
||||
|
||||
Reports are generated as downloadable PDFs using server-side rendering.
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
### Zero-Knowledge Architecture
|
||||
|
||||
TOD uses a 1Password-style hybrid zero-knowledge model:
|
||||
|
||||
- **SRP-6a authentication** -- your password never leaves the browser. The server verifies a cryptographic proof without knowing the password.
|
||||
- **Secret Key** -- a 128-bit key in `A3-XXXXXX` format, generated during enrollment. Combined with your password for two-secret key derivation (2SKD).
|
||||
- **Emergency Kit** -- a downloadable PDF containing your Secret Key. Store it securely offline; you need it to log in from new browsers.
|
||||
- **Envelope encryption** -- configuration backups and audit logs are encrypted at rest using per-tenant keys managed by the KMS (OpenBao Transit).
|
||||
|
||||
### Roles and Permissions
|
||||
|
||||
| Role | Capabilities |
|
||||
|------|-------------|
|
||||
| **super_admin** | Full platform access across all tenants. Can create organizations, manage all users, and access system settings. |
|
||||
| **admin** | Full access within their tenant. Can manage users, devices, and configuration for their organization. |
|
||||
| **operator** | Can view devices, apply configurations, and acknowledge alerts. Cannot manage users or organization settings. |
|
||||
| **viewer** | Read-only access to devices, dashboards, and reports within their tenant. |
|
||||
|
||||
### Credential Storage
|
||||
|
||||
Device credentials (RouterOS username/password) are encrypted at rest with AES-256-GCM (Fernet) and only decrypted in memory by the poller when connecting to devices.
|
||||
|
||||
---
|
||||
|
||||
## Theme
|
||||
|
||||
TOD supports dark and light modes:
|
||||
|
||||
- **Dark mode** (default) uses the Midnight Slate palette.
|
||||
- **Light mode** provides a clean, high-contrast alternative.
|
||||
- Toggle in **Settings** or let the portal follow your system preference.
|
||||
- The command palette and all UI components adapt to the active theme.
|
||||
|
||||
---
|
||||
|
||||
## Tips
|
||||
|
||||
- Use the **command palette** (`Cmd+K`) for the fastest way to navigate. It searches pages, devices, and actions.
|
||||
- The **Audit Trail** is immutable -- every configuration change, login, and admin action is recorded and cannot be deleted.
|
||||
- **Safe Apply** is your safety net for remote devices. If a firewall change locks you out, the automatic revert restores access.
|
||||
- **API Keys** (prefixed `mktp_`) provide programmatic access at operator-level permissions for automation and scripting.
|
||||
- The **Topology** view uses automatic Dagre layout. Toggle shared subnet edges to reduce visual clutter on complex networks.
|
||||
|
||||
---
|
||||
|
||||
*TOD -- The Other Dude is not affiliated with or endorsed by MikroTik (SIA Mikrotikls).*
|
||||
1
docs/website/CNAME
Normal file
@@ -0,0 +1 @@
|
||||
theotherdude.net
|
||||
BIN
docs/website/assets/alerts.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
docs/website/assets/config-editor.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
docs/website/assets/dashboard-lebowski-lanes.png
Normal file
|
After Width: | Height: | Size: 143 KiB |
BIN
docs/website/assets/dashboard-strangers-ranch.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
docs/website/assets/device-detail.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
docs/website/assets/device-list.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
docs/website/assets/login.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
docs/website/assets/topology.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
1412
docs/website/docs.html
Normal file
520
docs/website/index.html
Normal file
@@ -0,0 +1,520 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>The Other Dude — Fleet Management for MikroTik RouterOS</title>
|
||||
<meta name="description" content="Manage hundreds of MikroTik routers from a single pane of glass. Zero-knowledge security, real-time monitoring, and configuration management — built for MSPs.">
|
||||
<meta name="keywords" content="MikroTik, RouterOS, fleet management, MSP, network management, WinBox alternative, MikroTik monitoring, MikroTik configuration, router management, network monitoring tool, MikroTik dashboard, multi-tenant network management">
|
||||
<meta name="author" content="The Other Dude">
|
||||
<meta name="robots" content="index, follow">
|
||||
<meta name="theme-color" content="#0F172A">
|
||||
<link rel="canonical" href="https://theotherdude.net/">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'><rect x='2' y='2' width='60' height='60' rx='8' fill='none' stroke='%238B1A1A' stroke-width='2'/><rect x='6' y='6' width='52' height='52' rx='5' fill='none' stroke='%23F5E6C8' stroke-width='1.5'/><rect x='8' y='8' width='48' height='48' rx='4' fill='%238B1A1A' opacity='0.15'/><path d='M32 8 L56 32 L32 56 L8 32 Z' fill='none' stroke='%238B1A1A' stroke-width='2'/><path d='M32 13 L51 32 L32 51 L13 32 Z' fill='none' stroke='%23F5E6C8' stroke-width='1.5'/><path d='M32 18 L46 32 L32 46 L18 32 Z' fill='%238B1A1A'/><path d='M32 19 L38 32 L32 45 L26 32 Z' fill='%232A9D8F'/><path d='M19 32 L32 26 L45 32 L32 38 Z' fill='%23F5E6C8'/><circle cx='32' cy='32' r='5' fill='%238B1A1A'/><circle cx='32' cy='32' r='2.5' fill='%232A9D8F'/><path d='M10 10 L16 10 L10 16 Z' fill='%232A9D8F' opacity='0.7'/><path d='M54 10 L54 16 L48 10 Z' fill='%232A9D8F' opacity='0.7'/><path d='M10 54 L16 54 L10 48 Z' fill='%232A9D8F' opacity='0.7'/><path d='M54 54 L48 54 L54 48 Z' fill='%232A9D8F' opacity='0.7'/></svg>">
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="The Other Dude — MikroTik Fleet Management for MSPs | Monitor, Configure, Secure">
|
||||
<meta property="og:description" content="Manage hundreds of MikroTik routers from a single pane of glass. Zero-knowledge security, real-time monitoring, and configuration management — built for MSPs.">
|
||||
<meta property="og:url" content="https://theotherdude.net/">
|
||||
<meta property="og:image" content="https://theotherdude.net/assets/og-image.png">
|
||||
<meta property="og:site_name" content="The Other Dude">
|
||||
<meta property="og:locale" content="en_US">
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="The Other Dude — MikroTik Fleet Management for MSPs">
|
||||
<meta name="twitter:description" content="Manage hundreds of MikroTik routers from a single pane of glass. Zero-knowledge security, real-time monitoring, and configuration management.">
|
||||
<meta name="twitter:image" content="https://theotherdude.net/assets/og-image.png">
|
||||
|
||||
<!-- Structured Data -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "The Other Dude",
|
||||
"applicationCategory": "NetworkApplication",
|
||||
"operatingSystem": "Linux, Docker",
|
||||
"description": "Open-source MikroTik RouterOS fleet management platform for MSPs. Real-time monitoring, zero-knowledge security, configuration management, and multi-tenant support.",
|
||||
"url": "https://theotherdude.net",
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "0",
|
||||
"priceCurrency": "USD"
|
||||
},
|
||||
"featureList": [
|
||||
"MikroTik RouterOS fleet management",
|
||||
"Real-time device monitoring via SSE",
|
||||
"Zero-knowledge SRP-6a authentication",
|
||||
"Per-tenant envelope encryption with OpenBao",
|
||||
"Two-phase configuration push with panic-revert",
|
||||
"Multi-tenant PostgreSQL Row-Level Security",
|
||||
"Internal Certificate Authority",
|
||||
"Firmware management and audit trail"
|
||||
],
|
||||
"softwareRequirements": "Docker, PostgreSQL 17, Redis, NATS"
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Fira+Code:wght@400;500&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Styles -->
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<!-- Navigation -->
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<nav class="site-nav site-nav--dark">
|
||||
<div class="nav-inner container">
|
||||
<a href="index.html" class="nav-logo">
|
||||
<svg class="nav-logo-mark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="32" height="32" aria-label="The Other Dude logo">
|
||||
<rect x="2" y="2" width="60" height="60" rx="8" fill="none" stroke="#8B1A1A" stroke-width="2"/>
|
||||
<rect x="6" y="6" width="52" height="52" rx="5" fill="none" stroke="#F5E6C8" stroke-width="1.5"/>
|
||||
<rect x="8" y="8" width="48" height="48" rx="4" fill="#8B1A1A" opacity="0.15"/>
|
||||
<path d="M32 8 L56 32 L32 56 L8 32 Z" fill="none" stroke="#8B1A1A" stroke-width="2"/>
|
||||
<path d="M32 13 L51 32 L32 51 L13 32 Z" fill="none" stroke="#F5E6C8" stroke-width="1.5"/>
|
||||
<path d="M32 18 L46 32 L32 46 L18 32 Z" fill="#8B1A1A"/>
|
||||
<path d="M32 19 L38 32 L32 45 L26 32 Z" fill="#2A9D8F"/>
|
||||
<path d="M19 32 L32 26 L45 32 L32 38 Z" fill="#F5E6C8"/>
|
||||
<circle cx="32" cy="32" r="5" fill="#8B1A1A"/>
|
||||
<circle cx="32" cy="32" r="2.5" fill="#2A9D8F"/>
|
||||
<path d="M10 10 L16 10 L10 16 Z" fill="#2A9D8F" opacity="0.7"/>
|
||||
<path d="M54 10 L54 16 L48 10 Z" fill="#2A9D8F" opacity="0.7"/>
|
||||
<path d="M10 54 L16 54 L10 48 Z" fill="#2A9D8F" opacity="0.7"/>
|
||||
<path d="M54 54 L48 54 L54 48 Z" fill="#2A9D8F" opacity="0.7"/>
|
||||
</svg>
|
||||
<span>The Other Dude</span>
|
||||
</a>
|
||||
<div class="nav-links">
|
||||
<a href="#features" class="nav-link">Features</a>
|
||||
<a href="#architecture" class="nav-link">Architecture</a>
|
||||
<a href="#screenshots" class="nav-link">Screenshots</a>
|
||||
<a href="docs.html" class="nav-link">Docs</a>
|
||||
<a href="docs.html#quickstart" class="nav-cta">Get Started</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<!-- Hero -->
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<section class="hero">
|
||||
<div class="hero-bg"></div>
|
||||
<div class="hero-content container">
|
||||
<div class="hero-rosette">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="96" height="96" aria-hidden="true">
|
||||
<rect x="2" y="2" width="60" height="60" rx="8" fill="none" stroke="#8B1A1A" stroke-width="2"/>
|
||||
<rect x="6" y="6" width="52" height="52" rx="5" fill="none" stroke="#F5E6C8" stroke-width="1.5"/>
|
||||
<rect x="8" y="8" width="48" height="48" rx="4" fill="#8B1A1A" opacity="0.15"/>
|
||||
<path d="M32 8 L56 32 L32 56 L8 32 Z" fill="none" stroke="#8B1A1A" stroke-width="2"/>
|
||||
<path d="M32 13 L51 32 L32 51 L13 32 Z" fill="none" stroke="#F5E6C8" stroke-width="1.5"/>
|
||||
<path d="M32 18 L46 32 L32 46 L18 32 Z" fill="#8B1A1A"/>
|
||||
<path d="M32 19 L38 32 L32 45 L26 32 Z" fill="#2A9D8F"/>
|
||||
<path d="M19 32 L32 26 L45 32 L32 38 Z" fill="#F5E6C8"/>
|
||||
<circle cx="32" cy="32" r="5" fill="#8B1A1A"/>
|
||||
<circle cx="32" cy="32" r="2.5" fill="#2A9D8F"/>
|
||||
<path d="M10 10 L16 10 L10 16 Z" fill="#2A9D8F" opacity="0.7"/>
|
||||
<path d="M54 10 L54 16 L48 10 Z" fill="#2A9D8F" opacity="0.7"/>
|
||||
<path d="M10 54 L16 54 L10 48 Z" fill="#2A9D8F" opacity="0.7"/>
|
||||
<path d="M54 54 L48 54 L54 48 Z" fill="#2A9D8F" opacity="0.7"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="hero-badge">Fleet Management for MikroTik</span>
|
||||
<h1 class="hero-title"><span class="gradient-text">MikroTik Fleet Management</span> for MSPs</h1>
|
||||
<p class="hero-subtitle">Manage hundreds of MikroTik routers from a single pane of glass. Zero-knowledge security, real-time monitoring, and configuration management — built for MSPs who demand more than WinBox.</p>
|
||||
<div class="hero-actions">
|
||||
<a href="docs.html#quickstart" class="btn btn-primary">Get Started</a>
|
||||
<a href="docs.html" class="btn btn-secondary">View Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<!-- Features -->
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<section id="features" class="features-section">
|
||||
<div class="container">
|
||||
<span class="section-label">FEATURES</span>
|
||||
<h2 class="section-title">Everything you need to manage your fleet</h2>
|
||||
<p class="section-desc">From device discovery to firmware upgrades, The Other Dude gives you complete control over your MikroTik infrastructure.</p>
|
||||
|
||||
<div class="features-grid">
|
||||
|
||||
<!-- Fleet Management -->
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="8" width="20" height="8" rx="2"/>
|
||||
<line x1="6" y1="12" x2="6" y2="12"/>
|
||||
<line x1="10" y1="12" x2="10" y2="12"/>
|
||||
<line x1="6" y1="16" x2="6" y2="20"/>
|
||||
<line x1="18" y1="16" x2="18" y2="20"/>
|
||||
<line x1="12" y1="4" x2="12" y2="8"/>
|
||||
<circle cx="12" cy="3" r="1"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Fleet Management</h3>
|
||||
<p class="feature-desc">Dashboard with real-time status, virtual-scrolled fleet table, subnet scanning, and per-device detail pages with live metrics.</p>
|
||||
</div>
|
||||
|
||||
<!-- Configuration -->
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Configuration</h3>
|
||||
<p class="feature-desc">Browse and edit RouterOS config in real-time. Two-phase push with panic-revert ensures you never brick a remote device. Batch templates for fleet-wide changes.</p>
|
||||
</div>
|
||||
|
||||
<!-- Monitoring -->
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Monitoring</h3>
|
||||
<p class="feature-desc">Real-time CPU, memory, and traffic via SSE. Threshold-based alerts with email, webhook, Slack, and webhook push notifications. Interactive topology map.</p>
|
||||
</div>
|
||||
|
||||
<!-- Zero-Knowledge Security -->
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||
<circle cx="12" cy="16" r="1"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Zero-Knowledge Security</h3>
|
||||
<p class="feature-desc">1Password-style SRP-6a auth — the server never sees your password. Per-tenant envelope encryption via OpenBao Transit. Internal CA for device TLS.</p>
|
||||
</div>
|
||||
|
||||
<!-- Multi-Tenant -->
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 21h18"/>
|
||||
<path d="M5 21V7l8-4v18"/>
|
||||
<path d="M19 21V11l-6-4"/>
|
||||
<line x1="9" y1="9" x2="9" y2="9.01"/>
|
||||
<line x1="9" y1="13" x2="9" y2="13.01"/>
|
||||
<line x1="9" y1="17" x2="9" y2="17.01"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Multi-Tenant</h3>
|
||||
<p class="feature-desc">PostgreSQL Row-Level Security isolates tenants at the database layer. RBAC with four roles. API keys for automation.</p>
|
||||
</div>
|
||||
|
||||
<!-- Operations -->
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Operations</h3>
|
||||
<p class="feature-desc">Firmware management, PDF reports, audit trail, maintenance windows, config backup with git-backed version history and diff.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<!-- Architecture -->
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<section id="architecture" class="arch-section">
|
||||
<div class="container">
|
||||
<span class="section-label">ARCHITECTURE</span>
|
||||
<h2 class="section-title">Built for reliability at scale</h2>
|
||||
|
||||
<div class="arch-visual">
|
||||
<!-- Row 1: Frontend -->
|
||||
<div class="arch-row">
|
||||
<div class="arch-node arch-node--app">
|
||||
<div class="arch-node-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
|
||||
</div>
|
||||
<div class="arch-node-label">Frontend</div>
|
||||
<div class="arch-node-tech">React 19 · nginx</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="arch-connector"><div class="arch-connector-line"></div><span class="arch-connector-label">/api/ proxy</span></div>
|
||||
|
||||
<!-- Row 2: Backend API -->
|
||||
<div class="arch-row">
|
||||
<div class="arch-node arch-node--app">
|
||||
<div class="arch-node-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg>
|
||||
</div>
|
||||
<div class="arch-node-label">Backend API</div>
|
||||
<div class="arch-node-tech">FastAPI · Python 3.12</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="arch-connector"><div class="arch-connector-line arch-connector-line--triple"></div></div>
|
||||
|
||||
<!-- Row 3: Infrastructure (3 nodes) -->
|
||||
<div class="arch-row arch-row--triple">
|
||||
<div class="arch-node arch-node--infra">
|
||||
<div class="arch-node-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>
|
||||
</div>
|
||||
<div class="arch-node-label">PostgreSQL</div>
|
||||
<div class="arch-node-tech">TimescaleDB · RLS</div>
|
||||
</div>
|
||||
<div class="arch-node arch-node--infra">
|
||||
<div class="arch-node-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>
|
||||
</div>
|
||||
<div class="arch-node-label">Redis</div>
|
||||
<div class="arch-node-tech">Locks · Cache</div>
|
||||
</div>
|
||||
<div class="arch-node arch-node--infra">
|
||||
<div class="arch-node-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
|
||||
</div>
|
||||
<div class="arch-node-label">NATS</div>
|
||||
<div class="arch-node-tech">JetStream pub/sub</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="arch-connector"><div class="arch-connector-line arch-connector-line--triple"></div></div>
|
||||
|
||||
<!-- Row 4: Go Poller -->
|
||||
<div class="arch-row">
|
||||
<div class="arch-node arch-node--app">
|
||||
<div class="arch-node-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
|
||||
</div>
|
||||
<div class="arch-node-label">Go Poller</div>
|
||||
<div class="arch-node-tech">RouterOS binary API · port 8729</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="arch-connector"><div class="arch-connector-line"></div></div>
|
||||
|
||||
<!-- Row 5: Devices -->
|
||||
<div class="arch-row">
|
||||
<div class="arch-node arch-node--device">
|
||||
<div class="arch-node-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>
|
||||
</div>
|
||||
<div class="arch-node-label">RouterOS Fleet</div>
|
||||
<div class="arch-node-tech">Your MikroTik devices</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="arch-bullets">
|
||||
<li>Three-service stack: React frontend, Python API, Go poller — each independently scalable</li>
|
||||
<li>PostgreSQL RLS enforces tenant isolation at the database layer, not the application layer</li>
|
||||
<li>NATS JetStream delivers real-time events from poller to frontend via SSE</li>
|
||||
<li>OpenBao Transit provides per-tenant envelope encryption for zero-knowledge credential storage</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<!-- Tech Stack -->
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<section class="tech-section">
|
||||
<div class="container">
|
||||
<span class="section-label">TECH STACK</span>
|
||||
<h2 class="section-title">Modern tools, battle-tested foundations</h2>
|
||||
<div class="tech-grid">
|
||||
<span class="tech-badge">React 19</span>
|
||||
<span class="tech-badge">TypeScript</span>
|
||||
<span class="tech-badge">FastAPI</span>
|
||||
<span class="tech-badge">Python 3.12</span>
|
||||
<span class="tech-badge">Go 1.24</span>
|
||||
<span class="tech-badge">PostgreSQL 17</span>
|
||||
<span class="tech-badge">TimescaleDB</span>
|
||||
<span class="tech-badge">Redis</span>
|
||||
<span class="tech-badge">NATS</span>
|
||||
<span class="tech-badge">Docker</span>
|
||||
<span class="tech-badge">OpenBao</span>
|
||||
<span class="tech-badge">WireGuard</span>
|
||||
<span class="tech-badge">Tailwind CSS</span>
|
||||
<span class="tech-badge">Vite</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<!-- Screenshots -->
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<section id="screenshots" class="screenshots-section">
|
||||
<div class="container">
|
||||
<span class="section-label">IN ACTION</span>
|
||||
<h2 class="section-title">See it in action</h2>
|
||||
</div>
|
||||
<div class="screenshots-scroll">
|
||||
<div class="screenshots-track">
|
||||
|
||||
<figure class="screenshot-card">
|
||||
<img src="assets/login.png" alt="The Other Dude zero-knowledge SRP-6a login page" loading="lazy" width="800" height="500">
|
||||
<figcaption>Zero-Knowledge SRP-6a Login</figcaption>
|
||||
</figure>
|
||||
|
||||
<figure class="screenshot-card">
|
||||
<img src="assets/dashboard-lebowski-lanes.png" alt="Fleet dashboard showing Lebowski Lanes network overview" loading="lazy" width="800" height="500">
|
||||
<figcaption>Fleet Dashboard — Lebowski Lanes</figcaption>
|
||||
</figure>
|
||||
|
||||
<figure class="screenshot-card">
|
||||
<img src="assets/device-list.png" alt="Device fleet list with status monitoring across tenants" loading="lazy" width="800" height="500">
|
||||
<figcaption>Device Fleet List</figcaption>
|
||||
</figure>
|
||||
|
||||
<figure class="screenshot-card">
|
||||
<img src="assets/device-detail.png" alt="Device detail view for The Dude core router" loading="lazy" width="800" height="500">
|
||||
<figcaption>Device Detail View</figcaption>
|
||||
</figure>
|
||||
|
||||
<figure class="screenshot-card">
|
||||
<img src="assets/topology.png" alt="Network topology map with automatic device discovery" loading="lazy" width="800" height="500">
|
||||
<figcaption>Network Topology Map</figcaption>
|
||||
</figure>
|
||||
|
||||
<figure class="screenshot-card">
|
||||
<img src="assets/config-editor.png" alt="RouterOS configuration editor with diff preview" loading="lazy" width="800" height="500">
|
||||
<figcaption>Configuration Editor</figcaption>
|
||||
</figure>
|
||||
|
||||
<figure class="screenshot-card">
|
||||
<img src="assets/alerts.png" alt="Alert rules and notification channel management" loading="lazy" width="800" height="500">
|
||||
<figcaption>Alert Rules & Notifications</figcaption>
|
||||
</figure>
|
||||
|
||||
<figure class="screenshot-card">
|
||||
<img src="assets/dashboard-strangers-ranch.png" alt="Multi-tenant view showing The Stranger's Ranch network" loading="lazy" width="800" height="500">
|
||||
<figcaption>Multi-Tenant — The Stranger’s Ranch</figcaption>
|
||||
</figure>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<!-- Quick Start -->
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<section class="quickstart-section">
|
||||
<div class="container">
|
||||
<span class="section-label">QUICK START</span>
|
||||
<h2 class="section-title">Up and running in minutes</h2>
|
||||
|
||||
<div class="code-window">
|
||||
<div class="code-window-header">
|
||||
<span class="code-dot code-dot--red"></span>
|
||||
<span class="code-dot code-dot--yellow"></span>
|
||||
<span class="code-dot code-dot--green"></span>
|
||||
<span class="code-window-title">Terminal</span>
|
||||
</div>
|
||||
<pre class="code-window-body"><code><span class="code-comment"># Clone and configure</span>
|
||||
<span class="code-cmd">cp .env.example .env</span>
|
||||
|
||||
<span class="code-comment"># Start infrastructure</span>
|
||||
<span class="code-cmd">docker compose up -d</span>
|
||||
|
||||
<span class="code-comment"># Build app images</span>
|
||||
<span class="code-cmd">docker compose build api && docker compose build poller && docker compose build frontend</span>
|
||||
|
||||
<span class="code-comment"># Launch</span>
|
||||
<span class="code-cmd">docker compose up -d</span>
|
||||
|
||||
<span class="code-comment"># Open TOD</span>
|
||||
<span class="code-cmd">open http://localhost:3000</span></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<!-- CTA -->
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<section class="cta-section">
|
||||
<div class="container">
|
||||
<h2 class="cta-title">Ready to manage your fleet?</h2>
|
||||
<p class="cta-desc">Get started in minutes. Self-hosted, open-source, and built for the MikroTik community.</p>
|
||||
<div class="cta-actions">
|
||||
<a href="docs.html" class="btn btn-primary">Read the Docs</a>
|
||||
<a href="docs.html#quickstart" class="btn btn-secondary">Quick Start Guide</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<!-- Footer -->
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="footer-inner container">
|
||||
<div class="footer-brand">
|
||||
<span class="footer-logo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="24" height="24" aria-hidden="true" style="vertical-align: middle; margin-right: 8px;">
|
||||
<rect x="2" y="2" width="60" height="60" rx="8" fill="none" stroke="#8B1A1A" stroke-width="2"/>
|
||||
<rect x="6" y="6" width="52" height="52" rx="5" fill="none" stroke="#F5E6C8" stroke-width="1.5"/>
|
||||
<rect x="8" y="8" width="48" height="48" rx="4" fill="#8B1A1A" opacity="0.15"/>
|
||||
<path d="M32 8 L56 32 L32 56 L8 32 Z" fill="none" stroke="#8B1A1A" stroke-width="2"/>
|
||||
<path d="M32 13 L51 32 L32 51 L13 32 Z" fill="none" stroke="#F5E6C8" stroke-width="1.5"/>
|
||||
<path d="M32 18 L46 32 L32 46 L18 32 Z" fill="#8B1A1A"/>
|
||||
<path d="M32 19 L38 32 L32 45 L26 32 Z" fill="#2A9D8F"/>
|
||||
<path d="M19 32 L32 26 L45 32 L32 38 Z" fill="#F5E6C8"/>
|
||||
<circle cx="32" cy="32" r="5" fill="#8B1A1A"/>
|
||||
<circle cx="32" cy="32" r="2.5" fill="#2A9D8F"/>
|
||||
</svg>
|
||||
The Other Dude
|
||||
</span>
|
||||
<span class="footer-copy">© 2026 The Other Dude. All rights reserved.</span>
|
||||
</div>
|
||||
<nav class="footer-links">
|
||||
<a href="docs.html">Docs</a>
|
||||
<a href="#architecture">Architecture</a>
|
||||
<a href="docs.html#security">Security</a>
|
||||
<a href="docs.html#api">API Reference</a>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Lightbox -->
|
||||
<div class="lightbox" id="lightbox">
|
||||
<img src="" alt="">
|
||||
<div class="lightbox-caption"></div>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
<script>
|
||||
(function() {
|
||||
var lb = document.getElementById('lightbox');
|
||||
var lbImg = lb.querySelector('img');
|
||||
var lbCap = lb.querySelector('.lightbox-caption');
|
||||
|
||||
document.querySelectorAll('.screenshot-card img').forEach(function(img) {
|
||||
img.addEventListener('click', function() {
|
||||
lbImg.src = img.src;
|
||||
lbImg.alt = img.alt;
|
||||
var cap = img.closest('.screenshot-card').querySelector('figcaption');
|
||||
lbCap.textContent = cap ? cap.textContent : '';
|
||||
lb.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
lb.addEventListener('click', function() {
|
||||
lb.classList.remove('active');
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') lb.classList.remove('active');
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
4
docs/website/robots.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://theotherdude.net/sitemap.xml
|
||||
241
docs/website/script.js
Normal file
@@ -0,0 +1,241 @@
|
||||
/* TOD Documentation Website — Shared JavaScript */
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* 1. Scroll Spy (docs page) */
|
||||
/* -------------------------------------------------- */
|
||||
function initScrollSpy() {
|
||||
const sidebar = document.querySelector('.sidebar-nav');
|
||||
if (!sidebar) return;
|
||||
|
||||
const links = Array.from(document.querySelectorAll('.sidebar-link'));
|
||||
const sections = links
|
||||
.map(function (link) {
|
||||
var id = link.getAttribute('data-section');
|
||||
return id ? document.getElementById(id) : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (!sections.length) return;
|
||||
|
||||
var current = null;
|
||||
|
||||
var observer = new IntersectionObserver(
|
||||
function (entries) {
|
||||
entries.forEach(function (entry) {
|
||||
if (entry.isIntersecting) {
|
||||
var id = entry.target.id;
|
||||
if (id !== current) {
|
||||
current = id;
|
||||
links.forEach(function (l) {
|
||||
l.classList.toggle(
|
||||
'sidebar-link--active',
|
||||
l.getAttribute('data-section') === id
|
||||
);
|
||||
});
|
||||
|
||||
/* keep active link visible in sidebar */
|
||||
var active = sidebar.querySelector('.sidebar-link--active');
|
||||
if (active) {
|
||||
active.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: '-80px 0px -60% 0px', threshold: 0 }
|
||||
);
|
||||
|
||||
sections.forEach(function (s) {
|
||||
observer.observe(s);
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* 2. Docs Search */
|
||||
/* -------------------------------------------------- */
|
||||
function initDocsSearch() {
|
||||
var input = document.getElementById('docs-search-input');
|
||||
if (!input) return;
|
||||
|
||||
var content = document.getElementById('docs-content');
|
||||
if (!content) return;
|
||||
|
||||
var sections = Array.from(content.querySelectorAll('section[id]'));
|
||||
var links = Array.from(document.querySelectorAll('.sidebar-link'));
|
||||
|
||||
input.addEventListener('input', function () {
|
||||
var q = input.value.trim().toLowerCase();
|
||||
|
||||
if (!q) {
|
||||
sections.forEach(function (s) { s.style.display = ''; });
|
||||
links.forEach(function (l) { l.style.display = ''; });
|
||||
return;
|
||||
}
|
||||
|
||||
sections.forEach(function (s) {
|
||||
var text = s.textContent.toLowerCase();
|
||||
var match = text.indexOf(q) !== -1;
|
||||
s.style.display = match ? '' : 'none';
|
||||
});
|
||||
|
||||
links.forEach(function (l) {
|
||||
var sectionId = l.getAttribute('data-section');
|
||||
var section = sectionId ? document.getElementById(sectionId) : null;
|
||||
if (section) {
|
||||
l.style.display = section.style.display;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* 3. Back to Top */
|
||||
/* -------------------------------------------------- */
|
||||
function initBackToTop() {
|
||||
var btn = document.getElementById('back-to-top');
|
||||
if (!btn) return;
|
||||
|
||||
window.addEventListener('scroll', function () {
|
||||
btn.classList.toggle('back-to-top--visible', window.scrollY > 400);
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
window.scrollToTop = function () {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* 4. Sidebar Toggle (mobile) */
|
||||
/* -------------------------------------------------- */
|
||||
window.toggleSidebar = function () {
|
||||
var sidebar = document.getElementById('docs-sidebar');
|
||||
if (!sidebar) return;
|
||||
sidebar.classList.toggle('docs-sidebar--open');
|
||||
};
|
||||
|
||||
function initSidebarClose() {
|
||||
var sidebar = document.getElementById('docs-sidebar');
|
||||
if (!sidebar) return;
|
||||
|
||||
/* close on outside click */
|
||||
document.addEventListener('click', function (e) {
|
||||
if (
|
||||
sidebar.classList.contains('docs-sidebar--open') &&
|
||||
!sidebar.contains(e.target) &&
|
||||
!e.target.closest('.docs-hamburger')
|
||||
) {
|
||||
sidebar.classList.remove('docs-sidebar--open');
|
||||
}
|
||||
});
|
||||
|
||||
/* close on link click (mobile) */
|
||||
sidebar.addEventListener('click', function (e) {
|
||||
if (e.target.closest('.sidebar-link')) {
|
||||
sidebar.classList.remove('docs-sidebar--open');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* 5. Reveal Animation (landing page) */
|
||||
/* -------------------------------------------------- */
|
||||
function initReveal() {
|
||||
var els = document.querySelectorAll('.reveal');
|
||||
if (!els.length) return;
|
||||
|
||||
var observer = new IntersectionObserver(
|
||||
function (entries) {
|
||||
entries.forEach(function (entry) {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('reveal--visible');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
els.forEach(function (el) {
|
||||
observer.observe(el);
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* 6. Smooth scroll for anchor links */
|
||||
/* -------------------------------------------------- */
|
||||
function initSmoothScroll() {
|
||||
document.addEventListener('click', function (e) {
|
||||
var link = e.target.closest('a[href^="#"]');
|
||||
if (!link) return;
|
||||
|
||||
var id = link.getAttribute('href').slice(1);
|
||||
var target = document.getElementById(id);
|
||||
if (!target) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
var offset = 80;
|
||||
var top = target.getBoundingClientRect().top + window.pageYOffset - offset;
|
||||
window.scrollTo({ top: top, behavior: 'smooth' });
|
||||
|
||||
/* update URL without jump */
|
||||
history.pushState(null, '', '#' + id);
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* 7. Active nav link (landing page) */
|
||||
/* -------------------------------------------------- */
|
||||
function initActiveNav() {
|
||||
var navLinks = document.querySelectorAll('.nav-link[href^="index.html#"]');
|
||||
if (!navLinks.length) return;
|
||||
|
||||
/* only run on landing page */
|
||||
if (document.body.classList.contains('docs-page')) return;
|
||||
|
||||
var sectionIds = [];
|
||||
navLinks.forEach(function (l) {
|
||||
var hash = l.getAttribute('href').split('#')[1];
|
||||
if (hash) sectionIds.push({ id: hash, link: l });
|
||||
});
|
||||
|
||||
if (!sectionIds.length) return;
|
||||
|
||||
var observer = new IntersectionObserver(
|
||||
function (entries) {
|
||||
entries.forEach(function (entry) {
|
||||
if (entry.isIntersecting) {
|
||||
sectionIds.forEach(function (item) {
|
||||
item.link.classList.toggle(
|
||||
'nav-link--active',
|
||||
item.id === entry.target.id
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: '-80px 0px -60% 0px', threshold: 0 }
|
||||
);
|
||||
|
||||
sectionIds.forEach(function (item) {
|
||||
var el = document.getElementById(item.id);
|
||||
if (el) observer.observe(el);
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Init on DOMContentLoaded */
|
||||
/* -------------------------------------------------- */
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
initScrollSpy();
|
||||
initDocsSearch();
|
||||
initBackToTop();
|
||||
initSidebarClose();
|
||||
initReveal();
|
||||
initSmoothScroll();
|
||||
initActiveNav();
|
||||
});
|
||||
})();
|
||||
15
docs/website/sitemap.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://theotherdude.net/</loc>
|
||||
<lastmod>2026-03-07</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://theotherdude.net/docs.html</loc>
|
||||
<lastmod>2026-03-07</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
</urlset>
|
||||