fix(a11y): WCAG 2.1 AA compliance fixes for website
Contrast: - Increase --text-muted from #64748B to #8494A7 (~5.3:1 on dark bg) - Darken --docs-text-muted from #94A3B8 to #6B7B8D (~5.3:1 on white) - Increase sidebar section title opacity from 0.5 to 0.7 Keyboard & focus: - Add skip navigation link to both pages - Make screenshot images keyboard-focusable (tabindex, role=button) - Add visible lightbox close button with focus styles - Fix search input focus: proper outline instead of outline:none Structure: - Fix heading hierarchy: single h1 per page, demote sections to h2/h3 - Replace sidebar h4 headings with p.sidebar-section-title - Add aria-labels to all nav landmarks (main, sidebar, footer) - Fix broken footer anchor links (#security-model, #api-endpoints) Accessibility: - Add sr-only label for docs search input - Add aria-label to back-to-top button - Change nav logo SVG from aria-label to aria-hidden (redundant) - Add role=dialog and aria-label to lightbox - Fix docs.html theme-color meta to #FAFBFC (matches light theme) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
<meta name="keywords" content="MikroTik documentation, RouterOS fleet management guide, MSP network management setup, MikroTik API, RouterOS configuration management">
|
||||
<meta name="robots" content="index, follow">
|
||||
<meta name="google-site-verification" content="d2QVuWrLJlzOQPnA-SAJuvajEHGYbusvJ4eDdZbWSBU">
|
||||
<meta name="theme-color" content="#0F172A">
|
||||
<meta name="theme-color" content="#FAFBFC">
|
||||
<link rel="canonical" href="https://theotherdude.net/docs.html">
|
||||
<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>">
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
</head>
|
||||
<body class="docs-page">
|
||||
|
||||
<a href="#docs-content" class="skip-link">Skip to main content</a>
|
||||
|
||||
<!-- ===== TESTING BANNER ===== -->
|
||||
<div class="testing-banner testing-banner--light">
|
||||
<div class="container">
|
||||
@@ -43,10 +45,10 @@
|
||||
</div>
|
||||
|
||||
<!-- ===== NAV ===== -->
|
||||
<nav class="site-nav site-nav--light">
|
||||
<nav class="site-nav site-nav--light" aria-label="Main navigation">
|
||||
<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">
|
||||
<svg class="nav-logo-mark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="32" height="32" 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"/>
|
||||
@@ -81,23 +83,24 @@
|
||||
<div class="docs-layout">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="docs-sidebar" id="docs-sidebar">
|
||||
<aside class="docs-sidebar" id="docs-sidebar" aria-label="Documentation navigation">
|
||||
<div class="docs-search">
|
||||
<label for="docs-search-input" class="sr-only">Search documentation</label>
|
||||
<input type="text" placeholder="Search docs..." id="docs-search-input" />
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
|
||||
<h4 class="sidebar-heading">Getting Started</h4>
|
||||
<p class="sidebar-section-title">Getting Started</p>
|
||||
<a href="#overview" class="sidebar-link" data-section="overview">Overview</a>
|
||||
<a href="#quickstart" class="sidebar-link" data-section="quickstart">Quick Start</a>
|
||||
<a href="#deployment" class="sidebar-link" data-section="deployment">Deployment</a>
|
||||
|
||||
<h4 class="sidebar-heading">Architecture</h4>
|
||||
<p class="sidebar-section-title">Architecture</p>
|
||||
<a href="#system-overview" class="sidebar-link" data-section="system-overview">System Overview</a>
|
||||
<a href="#data-flow" class="sidebar-link" data-section="data-flow">Data Flow</a>
|
||||
<a href="#multi-tenancy" class="sidebar-link" data-section="multi-tenancy">Multi-Tenancy</a>
|
||||
|
||||
<h4 class="sidebar-heading">User Guide</h4>
|
||||
<p class="sidebar-section-title">User Guide</p>
|
||||
<a href="#first-login" class="sidebar-link" data-section="first-login">First Login</a>
|
||||
<a href="#navigation" class="sidebar-link" data-section="navigation">Navigation</a>
|
||||
<a href="#device-management" class="sidebar-link" data-section="device-management">Device Management</a>
|
||||
@@ -106,18 +109,18 @@
|
||||
<a href="#monitoring" class="sidebar-link" data-section="monitoring">Monitoring & Alerts</a>
|
||||
<a href="#reports" class="sidebar-link" data-section="reports">Reports</a>
|
||||
|
||||
<h4 class="sidebar-heading">Security</h4>
|
||||
<p class="sidebar-section-title">Security</p>
|
||||
<a href="#security-model" class="sidebar-link" data-section="security-model">Security Model</a>
|
||||
<a href="#authentication" class="sidebar-link" data-section="authentication">Authentication</a>
|
||||
<a href="#encryption" class="sidebar-link" data-section="encryption">Encryption</a>
|
||||
<a href="#rbac" class="sidebar-link" data-section="rbac">RBAC & Tenants</a>
|
||||
|
||||
<h4 class="sidebar-heading">API Reference</h4>
|
||||
<p class="sidebar-section-title">API Reference</p>
|
||||
<a href="#api-endpoints" class="sidebar-link" data-section="api-endpoints">Endpoints</a>
|
||||
<a href="#api-auth" class="sidebar-link" data-section="api-auth">Authentication</a>
|
||||
<a href="#api-errors" class="sidebar-link" data-section="api-errors">Error Handling</a>
|
||||
|
||||
<h4 class="sidebar-heading">Configuration</h4>
|
||||
<p class="sidebar-section-title">Configuration</p>
|
||||
<a href="#env-vars" class="sidebar-link" data-section="env-vars">Environment Variables</a>
|
||||
<a href="#docker-compose" class="sidebar-link" data-section="docker-compose">Docker Compose</a>
|
||||
|
||||
@@ -137,7 +140,7 @@
|
||||
<p>Fleet management for MikroTik RouterOS devices. Built for MSPs who manage hundreds of routers across multiple tenants. Think “UniFi Controller, but for MikroTik.”</p>
|
||||
<p>TOD 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.</p>
|
||||
|
||||
<h2>Features</h2>
|
||||
<h3>Features</h3>
|
||||
<ul>
|
||||
<li><strong>Fleet</strong> — Dashboard with at-a-glance fleet health, virtual-scrolled device table, geographic map, and subnet scanner for device discovery.</li>
|
||||
<li><strong>Configuration</strong> — Config Editor with two-phase safe apply, batch configuration across devices, bulk CLI commands, reusable templates, Simple Config (Linksys/Ubiquiti-style UI), and git-backed config backup with diff viewer.</li>
|
||||
@@ -148,7 +151,7 @@
|
||||
<li><strong>UX</strong> — Command palette (<kbd>Cmd+K</kbd>), Vim-style keyboard shortcuts, dark/light mode, Framer Motion page transitions, and shimmer skeleton loaders.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Tech Stack</h2>
|
||||
<h3>Tech Stack</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Layer</th><th>Technology</th></tr>
|
||||
@@ -168,7 +171,7 @@
|
||||
|
||||
<!-- QUICK START -->
|
||||
<section id="quickstart">
|
||||
<h1>Quick Start</h1>
|
||||
<h2>Quick Start</h2>
|
||||
<pre><code># Clone and run the setup wizard
|
||||
git clone https://github.com/staack/the-other-dude.git
|
||||
cd the-other-dude
|
||||
@@ -188,7 +191,7 @@ python3 setup.py</code></pre>
|
||||
</ul>
|
||||
<p>No manual <code>.env</code> editing required. The wizard generates <code>.env.prod</code> with production-strength secrets and starts the full stack.</p>
|
||||
|
||||
<h2>Environment Profiles</h2>
|
||||
<h3>Environment Profiles</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Environment</th><th>Frontend</th><th>API</th><th>Notes</th></tr>
|
||||
@@ -203,9 +206,9 @@ python3 setup.py</code></pre>
|
||||
|
||||
<!-- DEPLOYMENT -->
|
||||
<section id="deployment">
|
||||
<h1>Deployment</h1>
|
||||
<h2>Deployment</h2>
|
||||
|
||||
<h2>Prerequisites</h2>
|
||||
<h3>Prerequisites</h3>
|
||||
<ul>
|
||||
<li>Docker Engine 24+ with Docker Compose v2</li>
|
||||
<li>At least 4 GB RAM (2 GB absolute minimum — builds are memory-intensive)</li>
|
||||
@@ -213,14 +216,14 @@ python3 setup.py</code></pre>
|
||||
<li>Network access to RouterOS devices on ports 8728 (API) and 8729 (API-SSL)</li>
|
||||
</ul>
|
||||
|
||||
<h2>1. Clone and Configure</h2>
|
||||
<h3>1. Clone and Configure</h3>
|
||||
<pre><code>git clone <repository-url> tod
|
||||
cd tod
|
||||
|
||||
# Copy environment template
|
||||
cp .env.example .env.prod</code></pre>
|
||||
|
||||
<h2>2. Generate Secrets</h2>
|
||||
<h3>2. Generate Secrets</h3>
|
||||
<pre><code># Generate JWT secret
|
||||
python3 -c "import secrets; print(secrets.token_urlsafe(64))"
|
||||
|
||||
@@ -236,16 +239,16 @@ POSTGRES_PASSWORD=<strong-password>
|
||||
FIRST_ADMIN_EMAIL=admin@example.com
|
||||
FIRST_ADMIN_PASSWORD=<strong-password></code></pre>
|
||||
|
||||
<h2>3. Build Images</h2>
|
||||
<h3>3. Build Images</h3>
|
||||
<p>Build images <strong>one at a time</strong> to avoid out-of-memory crashes on constrained hosts:</p>
|
||||
<pre><code>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</code></pre>
|
||||
|
||||
<h2>4. Start the Stack</h2>
|
||||
<h3>4. Start the Stack</h3>
|
||||
<pre><code>docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env.prod up -d</code></pre>
|
||||
|
||||
<h2>5. Verify</h2>
|
||||
<h3>5. Verify</h3>
|
||||
<pre><code># Check all services are running
|
||||
docker compose ps
|
||||
|
||||
@@ -259,7 +262,7 @@ curl http://localhost:8000/health/ready
|
||||
open http://localhost</code></pre>
|
||||
<p>Log in with the <code>FIRST_ADMIN_EMAIL</code> and <code>FIRST_ADMIN_PASSWORD</code> credentials set in step 2.</p>
|
||||
|
||||
<h2>Required Environment Variables</h2>
|
||||
<h3>Required Environment Variables</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Description</th><th>Example</th></tr>
|
||||
@@ -274,7 +277,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Optional Environment Variables</h2>
|
||||
<h3>Optional Environment Variables</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Default</th><th>Description</th></tr>
|
||||
@@ -296,7 +299,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Storage Configuration</h2>
|
||||
<h3>Storage Configuration</h3>
|
||||
<p>Docker volumes mount to the host filesystem. Default locations:</p>
|
||||
<ul>
|
||||
<li><strong>PostgreSQL data:</strong> <code>./docker-data/postgres</code></li>
|
||||
@@ -306,7 +309,7 @@ open http://localhost</code></pre>
|
||||
</ul>
|
||||
<p>To change storage locations, edit the volume mounts in <code>docker-compose.yml</code>.</p>
|
||||
|
||||
<h2>Resource Limits</h2>
|
||||
<h3>Resource Limits</h3>
|
||||
<p>Container memory limits are enforced in <code>docker-compose.prod.yml</code> to prevent OOM crashes:</p>
|
||||
<table>
|
||||
<thead>
|
||||
@@ -323,7 +326,7 @@ open http://localhost</code></pre>
|
||||
</table>
|
||||
<p>Adjust under <code>deploy.resources.limits.memory</code> in <code>docker-compose.prod.yml</code>.</p>
|
||||
|
||||
<h2>Monitoring (Optional)</h2>
|
||||
<h3>Monitoring (Optional)</h3>
|
||||
<p>Enable Prometheus and Grafana monitoring with the observability compose overlay:</p>
|
||||
<pre><code>docker compose \
|
||||
-f docker-compose.yml \
|
||||
@@ -351,7 +354,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Troubleshooting</h2>
|
||||
<h3>Troubleshooting</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Issue</th><th>Solution</th></tr>
|
||||
@@ -375,10 +378,10 @@ open http://localhost</code></pre>
|
||||
|
||||
<!-- SYSTEM OVERVIEW -->
|
||||
<section id="system-overview">
|
||||
<h1>System Overview</h1>
|
||||
<h2>System Overview</h2>
|
||||
<p>TOD 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).</p>
|
||||
|
||||
<h2>Architecture Diagram</h2>
|
||||
<h3>Architecture Diagram</h3>
|
||||
<pre><code>+--------------+ +------------------+ +---------------+
|
||||
| Frontend |---->| Backend API |<--->| Go Poller |
|
||||
| React/nginx | | FastAPI | | go-routeros |
|
||||
@@ -397,7 +400,7 @@ open http://localhost</code></pre>
|
||||
| Transit KMS |
|
||||
+--------------+</code></pre>
|
||||
|
||||
<h2>Services</h2>
|
||||
<h3>Services</h3>
|
||||
|
||||
<h3>Frontend (React / nginx)</h3>
|
||||
<ul>
|
||||
@@ -479,7 +482,7 @@ open http://localhost</code></pre>
|
||||
<li><strong>Memory limit:</strong> 256 MB</li>
|
||||
</ul>
|
||||
|
||||
<h2>Infrastructure Services</h2>
|
||||
<h3>Infrastructure Services</h3>
|
||||
|
||||
<h3>PostgreSQL 17 + TimescaleDB</h3>
|
||||
<ul>
|
||||
@@ -532,7 +535,7 @@ open http://localhost</code></pre>
|
||||
<li><strong>Memory limit:</strong> 128 MB</li>
|
||||
</ul>
|
||||
|
||||
<h2>Container Memory Limits</h2>
|
||||
<h3>Container Memory Limits</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Service</th><th>Limit</th></tr>
|
||||
@@ -549,7 +552,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Network Ports</h2>
|
||||
<h3>Network Ports</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Service</th><th>Internal Port</th><th>External Port</th><th>Protocol</th></tr>
|
||||
@@ -571,9 +574,9 @@ open http://localhost</code></pre>
|
||||
|
||||
<!-- DATA FLOW -->
|
||||
<section id="data-flow">
|
||||
<h1>Data Flow</h1>
|
||||
<h2>Data Flow</h2>
|
||||
|
||||
<h2>Device Polling Cycle</h2>
|
||||
<h3>Device Polling Cycle</h3>
|
||||
<pre><code>Go Poller Redis OpenBao RouterOS NATS API PostgreSQL
|
||||
| | | | | | |
|
||||
+--query list-->| | | | | |
|
||||
@@ -599,7 +602,7 @@ open http://localhost</code></pre>
|
||||
<li>Releases Redis lock</li>
|
||||
</ol>
|
||||
|
||||
<h2>Config Push (Two-Phase with Panic Revert)</h2>
|
||||
<h3>Config Push (Two-Phase with Panic Revert)</h3>
|
||||
<pre><code>Frontend API RouterOS
|
||||
| | |
|
||||
+--push config->| |
|
||||
@@ -623,7 +626,7 @@ open http://localhost</code></pre>
|
||||
</ol>
|
||||
<p>This pattern prevents lockouts from misconfigured firewall rules or IP changes.</p>
|
||||
|
||||
<h2>SRP-6a Authentication Flow</h2>
|
||||
<h3>SRP-6a Authentication Flow</h3>
|
||||
<pre><code>Browser API PostgreSQL
|
||||
| | |
|
||||
+--register---------------->| |
|
||||
@@ -647,10 +650,10 @@ open http://localhost</code></pre>
|
||||
|
||||
<!-- MULTI-TENANCY -->
|
||||
<section id="multi-tenancy">
|
||||
<h1>Multi-Tenancy</h1>
|
||||
<h2>Multi-Tenancy</h2>
|
||||
<p>TOD enforces tenant isolation at the database level using PostgreSQL Row-Level Security (RLS), making cross-tenant data access structurally impossible.</p>
|
||||
|
||||
<h2>How It Works</h2>
|
||||
<h3>How It Works</h3>
|
||||
<ul>
|
||||
<li>Every data table includes a <code>tenant_id</code> column.</li>
|
||||
<li>PostgreSQL RLS policies filter rows by <code>current_setting('app.tenant_id')</code>.</li>
|
||||
@@ -660,7 +663,7 @@ open http://localhost</code></pre>
|
||||
<li>Tenant isolation is enforced at the database level, not the application level — even a compromised API cannot leak cross-tenant data through <code>app_user</code> connections.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Database Roles</h2>
|
||||
<h3>Database Roles</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Role</th><th>RLS</th><th>Purpose</th></tr>
|
||||
@@ -672,7 +675,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Security Layers</h2>
|
||||
<h3>Security Layers</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Layer</th><th>Mechanism</th><th>Purpose</th></tr>
|
||||
@@ -697,7 +700,7 @@ open http://localhost</code></pre>
|
||||
|
||||
<!-- FIRST LOGIN -->
|
||||
<section id="first-login">
|
||||
<h1>First Login</h1>
|
||||
<h2>First Login</h2>
|
||||
<ol>
|
||||
<li>Navigate to the portal URL provided by your administrator.</li>
|
||||
<li>Log in with the admin credentials created during initial deployment.</li>
|
||||
@@ -706,7 +709,7 @@ open http://localhost</code></pre>
|
||||
<li>Complete the <strong>Setup Wizard</strong> to create your first organization and add your first device.</li>
|
||||
</ol>
|
||||
|
||||
<h2>Setup Wizard</h2>
|
||||
<h3>Setup Wizard</h3>
|
||||
<p>The Setup Wizard launches automatically for first-time super_admin users. It walks through three steps:</p>
|
||||
<ul>
|
||||
<li><strong>Step 1 — Create Organization:</strong> Enter a name for your tenant (organization). This is the top-level container for all your devices, users, and configuration.</li>
|
||||
@@ -718,10 +721,10 @@ open http://localhost</code></pre>
|
||||
|
||||
<!-- NAVIGATION -->
|
||||
<section id="navigation">
|
||||
<h1>Navigation</h1>
|
||||
<h2>Navigation</h2>
|
||||
<p>TOD uses a collapsible sidebar with four sections. Press <kbd>[</kbd> to toggle the sidebar between expanded (240px) and collapsed (48px) views. On mobile, the sidebar opens as an overlay.</p>
|
||||
|
||||
<h2>Fleet</h2>
|
||||
<h3>Fleet</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Item</th><th>Description</th></tr>
|
||||
@@ -733,7 +736,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Manage</h2>
|
||||
<h3>Manage</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Item</th><th>Description</th></tr>
|
||||
@@ -750,7 +753,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Monitor</h2>
|
||||
<h3>Monitor</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Item</th><th>Description</th></tr>
|
||||
@@ -765,7 +768,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Admin</h2>
|
||||
<h3>Admin</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Item</th><th>Description</th></tr>
|
||||
@@ -779,7 +782,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Keyboard Shortcuts</h2>
|
||||
<h3>Keyboard Shortcuts</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Shortcut</th><th>Action</th></tr>
|
||||
@@ -799,9 +802,9 @@ open http://localhost</code></pre>
|
||||
|
||||
<!-- DEVICE MANAGEMENT -->
|
||||
<section id="device-management">
|
||||
<h1>Device Management</h1>
|
||||
<h2>Device Management</h2>
|
||||
|
||||
<h2>Adding Devices</h2>
|
||||
<h3>Adding Devices</h3>
|
||||
<p>There are three ways to add devices to your fleet:</p>
|
||||
<ol>
|
||||
<li><strong>Setup Wizard</strong> — automatically offered on first login.</li>
|
||||
@@ -815,7 +818,7 @@ open http://localhost</code></pre>
|
||||
<li><strong>Credentials</strong> — username and password for the device. Credentials are encrypted at rest with AES-256-GCM.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Device Detail Tabs</h2>
|
||||
<h3>Device Detail Tabs</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Tab</th><th>Description</th></tr>
|
||||
@@ -832,7 +835,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Remote Access Buttons</h2>
|
||||
<h3>Remote Access Buttons</h3>
|
||||
<p>The device detail page includes <strong>WinBox</strong> and <strong>SSH</strong> buttons for one-click remote access:</p>
|
||||
<ul>
|
||||
<li><strong>WinBox</strong> — Opens a WinBox tunnel via NATS request-reply. The poller allocates a local TCP port and proxies traffic to the device’s WinBox port. A <code>winbox://</code> URI is returned to launch the WinBox application.</li>
|
||||
@@ -840,7 +843,7 @@ open http://localhost</code></pre>
|
||||
</ul>
|
||||
<p>Both session types have configurable idle timeouts (WinBox: 5 min, SSH: 15 min) and are fully audit-logged.</p>
|
||||
|
||||
<h2>Simple Config</h2>
|
||||
<h3>Simple Config</h3>
|
||||
<p>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.</p>
|
||||
<p>Seven category tabs:</p>
|
||||
<ol>
|
||||
@@ -857,14 +860,14 @@ open http://localhost</code></pre>
|
||||
|
||||
<!-- CONFIG EDITOR -->
|
||||
<section id="config-editor">
|
||||
<h1>Config Editor</h1>
|
||||
<h2>Config Editor</h2>
|
||||
<p>The Config Editor provides direct access to RouterOS configuration paths (e.g., <code>/ip/address</code>, <code>/ip/firewall/filter</code>, <code>/interface/bridge</code>).</p>
|
||||
<ul>
|
||||
<li>Select a device from the header dropdown.</li>
|
||||
<li>Navigate the configuration tree to browse, add, edit, or delete entries.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Apply Modes</h2>
|
||||
<h3>Apply Modes</h3>
|
||||
<ul>
|
||||
<li><strong>Standard Apply</strong> — changes are applied immediately.</li>
|
||||
<li><strong>Safe Apply</strong> — 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.</li>
|
||||
@@ -874,10 +877,10 @@ open http://localhost</code></pre>
|
||||
|
||||
<!-- REMOTE ACCESS -->
|
||||
<section id="remote-access">
|
||||
<h1>Remote Access</h1>
|
||||
<h2>Remote Access</h2>
|
||||
<p>TOD provides browser-based remote access to RouterOS devices without exposing management ports to the internet. Two access methods are available from the device detail page.</p>
|
||||
|
||||
<h2>WinBox Tunnels</h2>
|
||||
<h3>WinBox Tunnels</h3>
|
||||
<p>Click the <strong>WinBox</strong> button on any device to open a temporary TCP tunnel:</p>
|
||||
<ol>
|
||||
<li>The API sends a NATS request to the poller on <code>tunnel.open.{device_id}</code>.</li>
|
||||
@@ -886,7 +889,7 @@ open http://localhost</code></pre>
|
||||
<li>The tunnel closes automatically after 5 minutes of idle time, or when explicitly closed.</li>
|
||||
</ol>
|
||||
|
||||
<h2>SSH Terminal</h2>
|
||||
<h3>SSH Terminal</h3>
|
||||
<p>Click the <strong>SSH</strong> button to open an in-browser terminal:</p>
|
||||
<ol>
|
||||
<li>The API generates a single-use session token stored in Redis (60-second TTL).</li>
|
||||
@@ -896,7 +899,7 @@ open http://localhost</code></pre>
|
||||
<li>Sessions close after 15 minutes of idle time.</li>
|
||||
</ol>
|
||||
|
||||
<h2>Architecture</h2>
|
||||
<h3>Architecture</h3>
|
||||
<pre><code>Browser API NATS Poller RouterOS
|
||||
| | | | |
|
||||
+--WinBox btn->| | | |
|
||||
@@ -912,7 +915,7 @@ open http://localhost</code></pre>
|
||||
| | | +--SSH session->|
|
||||
|<-------- bidirectional PTY bridge -------->|<------------>|</code></pre>
|
||||
|
||||
<h2>Session Management</h2>
|
||||
<h3>Session Management</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Feature</th><th>WinBox Tunnel</th><th>SSH Terminal</th></tr>
|
||||
@@ -926,7 +929,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Security</h2>
|
||||
<h3>Security</h3>
|
||||
<ul>
|
||||
<li>WinBox tunnels are only accessible from the poller’s host (bound to <code>0.0.0.0</code> within the container network).</li>
|
||||
<li>SSH session tokens are single-use, expire in 60 seconds, and are validated + deleted atomically in Redis.</li>
|
||||
@@ -938,9 +941,9 @@ open http://localhost</code></pre>
|
||||
|
||||
<!-- MONITORING -->
|
||||
<section id="monitoring">
|
||||
<h1>Monitoring & Alerts</h1>
|
||||
<h2>Monitoring & Alerts</h2>
|
||||
|
||||
<h2>Alert Rules</h2>
|
||||
<h3>Alert Rules</h3>
|
||||
<p>Create threshold-based rules that fire when device metrics cross defined boundaries:</p>
|
||||
<ul>
|
||||
<li>Select the metric to monitor (CPU, memory, disk, interface traffic, wireless signal, wireless CCQ, uptime, etc.).</li>
|
||||
@@ -949,7 +952,7 @@ open http://localhost</code></pre>
|
||||
<li>Assign one or more notification channels.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Notification Channels</h2>
|
||||
<h3>Notification Channels</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Channel</th><th>Description</th></tr>
|
||||
@@ -961,7 +964,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Maintenance Windows</h2>
|
||||
<h3>Maintenance Windows</h3>
|
||||
<ul>
|
||||
<li>Define start and end times.</li>
|
||||
<li>Apply to specific devices or fleet-wide.</li>
|
||||
@@ -972,7 +975,7 @@ open http://localhost</code></pre>
|
||||
|
||||
<!-- REPORTS -->
|
||||
<section id="reports">
|
||||
<h1>Reports</h1>
|
||||
<h2>Reports</h2>
|
||||
<p>Generate PDF reports from the Reports page. Four report types are available:</p>
|
||||
<table>
|
||||
<thead>
|
||||
@@ -994,10 +997,10 @@ open http://localhost</code></pre>
|
||||
|
||||
<!-- SECURITY MODEL -->
|
||||
<section id="security-model">
|
||||
<h1>Security Model</h1>
|
||||
<h2>Security Model</h2>
|
||||
<p>TOD 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.</p>
|
||||
|
||||
<h2>Data Protection</h2>
|
||||
<h3>Data Protection</h3>
|
||||
<ul>
|
||||
<li><strong>Config backups:</strong> Encrypted at rest via OpenBao Transit envelope encryption before database storage.</li>
|
||||
<li><strong>Audit logs:</strong> Encrypted at rest via Transit encryption — audit log content is protected even from database administrators.</li>
|
||||
@@ -1012,7 +1015,7 @@ open http://localhost</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>Security Headers</h2>
|
||||
<h3>Security Headers</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Header</th><th>Value</th><th>Purpose</th></tr>
|
||||
@@ -1026,7 +1029,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Audit Trail</h2>
|
||||
<h3>Audit Trail</h3>
|
||||
<ul>
|
||||
<li><strong>Immutable audit log:</strong> All significant actions are recorded — logins, configuration changes, device operations, admin actions.</li>
|
||||
<li><strong>Fire-and-forget logging:</strong> The <code>log_action()</code> function records audit events asynchronously without blocking the main request.</li>
|
||||
@@ -1036,7 +1039,7 @@ open http://localhost</code></pre>
|
||||
<li><strong>Account deletion:</strong> When a user deletes their account, audit log entries are anonymized (PII removed) but the action records are retained for security compliance.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Data Retention</h2>
|
||||
<h3>Data Retention</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Data Type</th><th>Retention</th><th>Notes</th></tr>
|
||||
@@ -1054,7 +1057,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>GDPR Compliance</h2>
|
||||
<h3>GDPR Compliance</h3>
|
||||
<ul>
|
||||
<li><strong>Right of Access (Art. 15):</strong> Users can view their account information on the Settings page.</li>
|
||||
<li><strong>Right to Data Portability (Art. 20):</strong> Users can export all personal data in JSON format from Settings.</li>
|
||||
@@ -1066,9 +1069,9 @@ open http://localhost</code></pre>
|
||||
|
||||
<!-- AUTHENTICATION -->
|
||||
<section id="authentication">
|
||||
<h1>Authentication</h1>
|
||||
<h2>Authentication</h2>
|
||||
|
||||
<h2>SRP-6a Zero-Knowledge Proof</h2>
|
||||
<h3>SRP-6a Zero-Knowledge Proof</h3>
|
||||
<p>TOD uses the Secure Remote Password (SRP-6a) protocol for authentication, ensuring the server never receives, transmits, or stores user passwords.</p>
|
||||
<ul>
|
||||
<li><strong>SRP-6a protocol:</strong> 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.</li>
|
||||
@@ -1093,7 +1096,7 @@ open http://localhost</code></pre>
|
||||
| {M2, access_token, refresh_token} |
|
||||
|<------------------------------------|</code></pre>
|
||||
|
||||
<h2>Two-Secret Key Derivation (2SKD)</h2>
|
||||
<h3>Two-Secret Key Derivation (2SKD)</h3>
|
||||
<p>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:</p>
|
||||
<ul>
|
||||
<li><strong>PBKDF2</strong> with 650,000 iterations stretches the password.</li>
|
||||
@@ -1101,7 +1104,7 @@ open http://localhost</code></pre>
|
||||
<li><strong>XOR</strong> combination of both factors produces the verifier input.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Secret Key & Emergency Kit</h2>
|
||||
<h3>Secret Key & Emergency Kit</h3>
|
||||
<ul>
|
||||
<li><strong>Secret Key format:</strong> <code>A3-XXXXXX</code> (128-bit), stored exclusively in the browser’s IndexedDB. The server never sees or stores the Secret Key.</li>
|
||||
<li><strong>Emergency Kit:</strong> Downloadable PDF containing the Secret Key for account recovery. Generated client-side.</li>
|
||||
@@ -1110,9 +1113,9 @@ open http://localhost</code></pre>
|
||||
|
||||
<!-- ENCRYPTION -->
|
||||
<section id="encryption">
|
||||
<h1>Encryption</h1>
|
||||
<h2>Encryption</h2>
|
||||
|
||||
<h2>Credential Encryption</h2>
|
||||
<h3>Credential Encryption</h3>
|
||||
<p>Device credentials (RouterOS usernames and passwords) are encrypted at rest using envelope encryption:</p>
|
||||
<ul>
|
||||
<li><strong>Encryption algorithm:</strong> AES-256-GCM (via Fernet symmetric encryption).</li>
|
||||
@@ -1121,10 +1124,10 @@ open http://localhost</code></pre>
|
||||
<li><strong>Envelope encryption:</strong> Data is encrypted with a data encryption key (DEK), which is itself encrypted by the tenant’s Transit key.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Go Poller LRU Cache</h2>
|
||||
<h3>Go Poller LRU Cache</h3>
|
||||
<p>The Go poller decrypts credentials at runtime via the Transit API, with an LRU cache (1,024 entries, 5-minute TTL) to reduce KMS round-trips. Cache hits avoid OpenBao API calls entirely.</p>
|
||||
|
||||
<h2>Additional Encryption</h2>
|
||||
<h3>Additional Encryption</h3>
|
||||
<ul>
|
||||
<li><strong>CA private keys:</strong> Encrypted with AES-256-GCM before database storage. PEM key material is never logged.</li>
|
||||
<li><strong>Config backups:</strong> Encrypted at rest via OpenBao Transit before database storage.</li>
|
||||
@@ -1134,9 +1137,9 @@ open http://localhost</code></pre>
|
||||
|
||||
<!-- RBAC -->
|
||||
<section id="rbac">
|
||||
<h1>RBAC & Tenants</h1>
|
||||
<h2>RBAC & Tenants</h2>
|
||||
|
||||
<h2>Role-Based Access Control</h2>
|
||||
<h3>Role-Based Access Control</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Role</th><th>Scope</th><th>Capabilities</th></tr>
|
||||
@@ -1154,10 +1157,10 @@ open http://localhost</code></pre>
|
||||
<li>API key tokens use the <code>mktp_</code> prefix and are stored as SHA-256 hashes (the plaintext token is shown once at creation and never stored).</li>
|
||||
</ul>
|
||||
|
||||
<h2>Tenant Isolation via RLS</h2>
|
||||
<h3>Tenant Isolation via RLS</h3>
|
||||
<p>Multi-tenancy is enforced at the database level via PostgreSQL Row-Level Security (RLS). The <code>app_user</code> database role automatically filters all queries by the authenticated user’s <code>tenant_id</code>. Super admins operate outside tenant scope.</p>
|
||||
|
||||
<h2>Internal CA & TLS Fallback</h2>
|
||||
<h3>Internal CA & TLS Fallback</h3>
|
||||
<p>TOD includes a per-tenant Internal Certificate Authority for managing TLS certificates on RouterOS devices:</p>
|
||||
<ul>
|
||||
<li><strong>Per-tenant CA:</strong> Each tenant can generate its own self-signed Certificate Authority.</li>
|
||||
@@ -1179,9 +1182,9 @@ open http://localhost</code></pre>
|
||||
|
||||
<!-- API ENDPOINTS -->
|
||||
<section id="api-endpoints">
|
||||
<h1>API Endpoints</h1>
|
||||
<h2>API Endpoints</h2>
|
||||
|
||||
<h2>Overview</h2>
|
||||
<h3>Overview</h3>
|
||||
<p>TOD exposes a REST API built with FastAPI. Interactive documentation is available at:</p>
|
||||
<ul>
|
||||
<li><strong>Swagger UI:</strong> <code>http://<host>:<port>/docs</code> (dev environment only)</li>
|
||||
@@ -1189,7 +1192,7 @@ open http://localhost</code></pre>
|
||||
</ul>
|
||||
<p>Both Swagger and ReDoc are disabled in staging/production environments.</p>
|
||||
|
||||
<h2>Endpoint Groups</h2>
|
||||
<h3>Endpoint Groups</h3>
|
||||
<p>All API routes are mounted under the <code>/api</code> prefix.</p>
|
||||
<table>
|
||||
<thead>
|
||||
@@ -1225,7 +1228,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Health Checks</h2>
|
||||
<h3>Health Checks</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Endpoint</th><th>Type</th><th>Description</th></tr>
|
||||
@@ -1240,9 +1243,9 @@ open http://localhost</code></pre>
|
||||
|
||||
<!-- API AUTH -->
|
||||
<section id="api-auth">
|
||||
<h1>API Authentication</h1>
|
||||
<h2>API Authentication</h2>
|
||||
|
||||
<h2>SRP-6a Login</h2>
|
||||
<h3>SRP-6a Login</h3>
|
||||
<ul>
|
||||
<li><code>POST /api/auth/login</code> — SRP-6a authentication (returns JWT access + refresh tokens)</li>
|
||||
<li><code>POST /api/auth/refresh</code> — Refresh an expired access token</li>
|
||||
@@ -1255,7 +1258,7 @@ open http://localhost</code></pre>
|
||||
</ul>
|
||||
<p>Access tokens expire after 15 minutes. Refresh tokens are valid for 7 days.</p>
|
||||
|
||||
<h2>API Key Authentication</h2>
|
||||
<h3>API Key Authentication</h3>
|
||||
<ul>
|
||||
<li>Create API keys in <strong>Admin > API Keys</strong></li>
|
||||
<li>Use header: <code>X-API-Key: mktp_<key></code></li>
|
||||
@@ -1263,14 +1266,14 @@ open http://localhost</code></pre>
|
||||
<li>Prefix: <code>mktp_</code>, stored as SHA-256 hash</li>
|
||||
</ul>
|
||||
|
||||
<h2>Rate Limiting</h2>
|
||||
<h3>Rate Limiting</h3>
|
||||
<ul>
|
||||
<li>Auth endpoints: 5 requests/minute per IP</li>
|
||||
<li>General endpoints: no global rate limit (per-route limits may apply)</li>
|
||||
</ul>
|
||||
<p>Rate limit violations return HTTP 429 with a JSON error body.</p>
|
||||
|
||||
<h2>RBAC Roles</h2>
|
||||
<h3>RBAC Roles</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Role</th><th>Scope</th><th>Description</th></tr>
|
||||
@@ -1286,15 +1289,15 @@ open http://localhost</code></pre>
|
||||
|
||||
<!-- API ERRORS -->
|
||||
<section id="api-errors">
|
||||
<h1>Error Handling</h1>
|
||||
<h2>Error Handling</h2>
|
||||
|
||||
<h2>Error Format</h2>
|
||||
<h3>Error Format</h3>
|
||||
<p>All error responses use a standard JSON format:</p>
|
||||
<pre><code>{
|
||||
"detail": "Human-readable error message"
|
||||
}</code></pre>
|
||||
|
||||
<h2>Status Codes</h2>
|
||||
<h3>Status Codes</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Code</th><th>Meaning</th></tr>
|
||||
@@ -1319,10 +1322,10 @@ open http://localhost</code></pre>
|
||||
|
||||
<!-- ENV VARS -->
|
||||
<section id="env-vars">
|
||||
<h1>Environment Variables</h1>
|
||||
<h2>Environment Variables</h2>
|
||||
<p>TOD uses Pydantic Settings for configuration. All values can be set via environment variables or a <code>.env</code> file in the backend working directory.</p>
|
||||
|
||||
<h2>Application</h2>
|
||||
<h3>Application</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Default</th><th>Description</th></tr>
|
||||
@@ -1337,7 +1340,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Authentication & JWT</h2>
|
||||
<h3>Authentication & JWT</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Default</th><th>Description</th></tr>
|
||||
@@ -1351,7 +1354,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Database</h2>
|
||||
<h3>Database</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Default</th><th>Description</th></tr>
|
||||
@@ -1367,7 +1370,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Security</h2>
|
||||
<h3>Security</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Default</th><th>Description</th></tr>
|
||||
@@ -1377,7 +1380,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>OpenBao / Vault (KMS)</h2>
|
||||
<h3>OpenBao / Vault (KMS)</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Default</th><th>Description</th></tr>
|
||||
@@ -1388,7 +1391,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>NATS</h2>
|
||||
<h3>NATS</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Default</th><th>Description</th></tr>
|
||||
@@ -1398,7 +1401,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Redis</h2>
|
||||
<h3>Redis</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Default</th><th>Description</th></tr>
|
||||
@@ -1408,7 +1411,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>SMTP (Notifications)</h2>
|
||||
<h3>SMTP (Notifications)</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Default</th><th>Description</th></tr>
|
||||
@@ -1423,7 +1426,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Firmware</h2>
|
||||
<h3>Firmware</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Default</th><th>Description</th></tr>
|
||||
@@ -1434,7 +1437,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Storage Paths</h2>
|
||||
<h3>Storage Paths</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Default</th><th>Description</th></tr>
|
||||
@@ -1445,7 +1448,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Bootstrap</h2>
|
||||
<h3>Bootstrap</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Default</th><th>Description</th></tr>
|
||||
@@ -1456,7 +1459,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Production Safety</h2>
|
||||
<h3>Production Safety</h3>
|
||||
<p>TOD refuses to start in <code>staging</code> or <code>production</code> environments if any of these variables still have their insecure dev defaults:</p>
|
||||
<ul>
|
||||
<li><code>JWT_SECRET_KEY</code></li>
|
||||
@@ -1468,9 +1471,9 @@ open http://localhost</code></pre>
|
||||
|
||||
<!-- DOCKER COMPOSE -->
|
||||
<section id="docker-compose">
|
||||
<h1>Docker Compose</h1>
|
||||
<h2>Docker Compose</h2>
|
||||
|
||||
<h2>Profiles</h2>
|
||||
<h3>Profiles</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Profile</th><th>Command</th><th>Services</th></tr>
|
||||
@@ -1481,7 +1484,7 @@ open http://localhost</code></pre>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Container Memory Limits</h2>
|
||||
<h3>Container Memory Limits</h3>
|
||||
<p>All containers have enforced memory limits to prevent OOM on the host:</p>
|
||||
<table>
|
||||
<thead>
|
||||
@@ -1503,7 +1506,7 @@ open http://localhost</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- Back to Top -->
|
||||
<button class="back-to-top" id="back-to-top" onclick="scrollToTop()">↑</button>
|
||||
<button class="back-to-top" id="back-to-top" onclick="scrollToTop()" aria-label="Back to top">↑</button>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="footer-inner container">
|
||||
@@ -1525,12 +1528,12 @@ open http://localhost</code></pre>
|
||||
</span>
|
||||
<span class="footer-copy">© 2026 The Other Dude. All rights reserved.</span>
|
||||
</div>
|
||||
<nav class="footer-links">
|
||||
<nav class="footer-links" aria-label="Footer navigation">
|
||||
<a href="index.html">Home</a>
|
||||
<a href="blog/">Blog</a>
|
||||
<a href="#quickstart">Quick Start</a>
|
||||
<a href="#security">Security</a>
|
||||
<a href="#api">API Reference</a>
|
||||
<a href="#security-model">Security</a>
|
||||
<a href="#api-endpoints">API Reference</a>
|
||||
<a href="https://github.com/staack/the-other-dude" rel="noopener">GitHub</a>
|
||||
<a href="mailto:license@theotherdude.net">Licensing</a>
|
||||
<a href="mailto:support@theotherdude.net">Support</a>
|
||||
|
||||
@@ -85,13 +85,15 @@
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a href="#main-content" class="skip-link">Skip to main content</a>
|
||||
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<!-- Navigation -->
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<nav class="site-nav site-nav--dark">
|
||||
<nav class="site-nav site-nav--dark" aria-label="Main navigation">
|
||||
<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">
|
||||
<svg class="nav-logo-mark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="32" height="32" 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"/>
|
||||
@@ -130,7 +132,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<main id="main-content">
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
<!-- Hero -->
|
||||
<!-- ═══════════════════════════════════════════ -->
|
||||
@@ -410,7 +412,7 @@
|
||||
</span>
|
||||
<span class="footer-copy">© 2026 The Other Dude. All rights reserved.</span>
|
||||
</div>
|
||||
<nav class="footer-links">
|
||||
<nav class="footer-links" aria-label="Footer navigation">
|
||||
<a href="docs.html">Docs</a>
|
||||
<a href="blog/">Blog</a>
|
||||
<a href="#what-it-does">Features</a>
|
||||
@@ -424,7 +426,8 @@
|
||||
</footer>
|
||||
|
||||
<!-- Lightbox -->
|
||||
<div class="lightbox" id="lightbox">
|
||||
<div class="lightbox" id="lightbox" role="dialog" aria-label="Image preview">
|
||||
<button class="lightbox-close" aria-label="Close image preview">×</button>
|
||||
<img src="" alt="The Other Dude dashboard managing multiple MikroTik routers">
|
||||
<div class="lightbox-caption"></div>
|
||||
</div>
|
||||
@@ -436,23 +439,42 @@
|
||||
var lb = document.getElementById('lightbox');
|
||||
var lbImg = lb.querySelector('img');
|
||||
var lbCap = lb.querySelector('.lightbox-caption');
|
||||
var lbClose = lb.querySelector('.lightbox-close');
|
||||
|
||||
function openLightbox(img) {
|
||||
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');
|
||||
lbClose.focus();
|
||||
}
|
||||
|
||||
function closeLightbox() {
|
||||
lb.classList.remove('active');
|
||||
}
|
||||
|
||||
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');
|
||||
img.setAttribute('tabindex', '0');
|
||||
img.setAttribute('role', 'button');
|
||||
img.addEventListener('click', function() { openLightbox(img); });
|
||||
img.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
openLightbox(img);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
lb.addEventListener('click', function() {
|
||||
lb.classList.remove('active');
|
||||
lbClose.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
closeLightbox();
|
||||
});
|
||||
|
||||
lb.addEventListener('click', closeLightbox);
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') lb.classList.remove('active');
|
||||
if (e.key === 'Escape') closeLightbox();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
--bg-elevated: #182438;
|
||||
--text-primary: #F1F5F9;
|
||||
--text-secondary: #94A3B8;
|
||||
--text-muted: #64748B;
|
||||
--text-muted: #8494A7;
|
||||
--accent: #2A9D8F;
|
||||
--accent-hover: #3DB8A9;
|
||||
--accent-glow: rgba(42, 157, 143, 0.12);
|
||||
@@ -30,7 +30,7 @@
|
||||
--docs-surface: #FFFFFF;
|
||||
--docs-text: #1E293B;
|
||||
--docs-text-secondary: #475569;
|
||||
--docs-text-muted: #94A3B8;
|
||||
--docs-text-muted: #6B7B8D;
|
||||
--docs-border: #E2E8F0;
|
||||
--docs-accent: #1F7A6F;
|
||||
--docs-sidebar-bg: #F8FAFC;
|
||||
@@ -899,6 +899,30 @@ ul, ol {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.lightbox-close {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 28px;
|
||||
line-height: 1;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.lightbox-close:hover,
|
||||
.lightbox-close:focus-visible {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.lightbox-caption {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
@@ -1169,7 +1193,7 @@ ul, ol {
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 8px;
|
||||
padding: 0 12px;
|
||||
opacity: 0.5;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.sidebar-link {
|
||||
@@ -1214,7 +1238,6 @@ ul, ol {
|
||||
font-size: 14px;
|
||||
color: var(--docs-text);
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.docs-search input::placeholder {
|
||||
@@ -1223,7 +1246,9 @@ ul, ol {
|
||||
|
||||
.docs-search input:focus {
|
||||
border-color: var(--docs-accent);
|
||||
box-shadow: 0 0 0 3px rgba(2, 132, 199, 0.1);
|
||||
box-shadow: 0 0 0 3px rgba(31, 122, 111, 0.15);
|
||||
outline: 2px solid var(--docs-accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* Docs content area */
|
||||
@@ -1239,7 +1264,7 @@ ul, ol {
|
||||
.docs-content h1 {
|
||||
font-family: "Outfit", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 2rem;
|
||||
font-size: 2.25rem;
|
||||
color: var(--docs-text);
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: -0.02em;
|
||||
@@ -1254,14 +1279,14 @@ ul, ol {
|
||||
|
||||
.docs-content h2 {
|
||||
font-family: "Outfit", sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
font-size: 2rem;
|
||||
color: var(--docs-text);
|
||||
margin-top: 56px;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 8px;
|
||||
padding-top: 32px;
|
||||
border-top: 1px solid var(--docs-border);
|
||||
letter-spacing: -0.01em;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.docs-content h2:first-of-type {
|
||||
@@ -1273,9 +1298,19 @@ ul, ol {
|
||||
.docs-content h3 {
|
||||
font-family: "Outfit", sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 1.15rem;
|
||||
font-size: 1.5rem;
|
||||
color: var(--docs-text);
|
||||
margin-top: 40px;
|
||||
margin-bottom: 16px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.docs-content h4 {
|
||||
font-family: "Outfit", sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 1.15rem;
|
||||
color: var(--docs-text);
|
||||
margin-top: 32px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
@@ -1653,6 +1688,24 @@ ul, ol {
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -100%;
|
||||
left: 16px;
|
||||
z-index: 9999;
|
||||
padding: 8px 16px;
|
||||
background: var(--accent);
|
||||
color: #0A1628;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
top: 8px;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
18. Responsive — Tablet (max-width: 768px)
|
||||
-------------------------------------------------------------------------- */
|
||||
@@ -1808,15 +1861,19 @@ ul, ol {
|
||||
|
||||
/* Docs content */
|
||||
.docs-content h1 {
|
||||
font-size: 1.65rem;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.docs-content h2 {
|
||||
font-size: 1.3rem;
|
||||
font-size: 1.65rem;
|
||||
margin-top: 40px;
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.docs-content h3 {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
/* Nav — on very small screens keep only Docs, Blog, and CTA */
|
||||
.nav-links .nav-link {
|
||||
display: none;
|
||||
|
||||
Reference in New Issue
Block a user