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>
This commit is contained in:
Jason Staack
2026-03-08 17:46:37 -05:00
commit b840047e19
511 changed files with 106948 additions and 0 deletions

117
docs/API.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
theotherdude.net

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

1412
docs/website/docs.html Normal file

File diff suppressed because it is too large Load Diff

520
docs/website/index.html Normal file
View 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&nbsp;&mdash; 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&nbsp;&mdash; 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 &middot; 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 &middot; 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 &middot; 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 &middot; 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 &middot; 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&nbsp;&mdash; 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 &mdash; 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 &amp; 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 &mdash; The Stranger&rsquo;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 &amp;&amp; docker compose build poller &amp;&amp; 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">&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
View File

@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://theotherdude.net/sitemap.xml

241
docs/website/script.js Normal file
View 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
View 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>

1868
docs/website/style.css Normal file

File diff suppressed because it is too large Load Diff