700 lines
23 KiB
HTML
700 lines
23 KiB
HTML
<!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 — MikroTik Fleet Management</title>
|
|
<meta name="description" content="MikroTik fleet management. Self-hosted. Source-available. Monitor devices, push configuration, track changes in git.">
|
|
<meta name="keywords" content="MikroTik, RouterOS, fleet management, network management, WinBox browser, MikroTik monitoring, MikroTik configuration, router management, self-hosted, open source, source-available">
|
|
<meta name="author" content="The Other Dude">
|
|
<meta name="robots" content="index, follow">
|
|
<meta name="google-site-verification" content="d2QVuWrLJlzOQPnA-SAJuvajEHGYbusvJ4eDdZbWSBU">
|
|
<meta name="theme-color" content="#eae7de">
|
|
<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">
|
|
<meta property="og:description" content="MikroTik fleet management. Self-hosted. Source-available.">
|
|
<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">
|
|
<meta name="twitter:description" content="MikroTik fleet management. Self-hosted. Source-available.">
|
|
<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": "MikroTik RouterOS fleet management. Self-hosted. Source-available under BSL 1.1.",
|
|
"url": "https://theotherdude.net",
|
|
"offers": {
|
|
"@type": "Offer",
|
|
"price": "0",
|
|
"priceCurrency": "USD"
|
|
},
|
|
"featureList": [
|
|
"Monitor device state across fleet",
|
|
"Push configuration with automatic rollback",
|
|
"Track config changes in git",
|
|
"Manage firmware versions",
|
|
"WinBox in the browser",
|
|
"VPN overlay for NAT traversal",
|
|
"Multi-tenant with row-level security",
|
|
"Zero-knowledge authentication (SRP-6a)"
|
|
],
|
|
"softwareRequirements": "Docker, PostgreSQL 17, Redis, NATS",
|
|
"softwareVersion": "9.7.1",
|
|
"license": "https://mariadb.com/bsl11/"
|
|
}
|
|
</script>
|
|
|
|
<!-- Organization Schema -->
|
|
<script type="application/ld+json">
|
|
{
|
|
"@context": "https://schema.org",
|
|
"@type": "Organization",
|
|
"name": "The Other Dude",
|
|
"url": "https://theotherdude.net",
|
|
"logo": "https://theotherdude.net/assets/og-image.png",
|
|
"sameAs": [
|
|
"https://github.com/staack/the-other-dude"
|
|
]
|
|
}
|
|
</script>
|
|
|
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
|
<link rel="stylesheet" href="style.css?v=3">
|
|
|
|
<style>
|
|
/* ================================================================
|
|
Warm Precision — Homepage overrides
|
|
Light mode only. Overrides the Deep Space base theme.
|
|
================================================================ */
|
|
|
|
:root {
|
|
--bg-deep: #eae7de;
|
|
--bg-primary: #eae7de;
|
|
--bg-surface: #f6f4ec;
|
|
--bg-elevated: #f0ede4;
|
|
--text-primary: #1a1810;
|
|
--text-secondary: #5e5a4e;
|
|
--text-muted: #8a8578;
|
|
--accent: #8a7a48;
|
|
--accent-hover: #7a6a38;
|
|
--accent-glow: rgba(138, 122, 72, 0.10);
|
|
--accent-secondary: #8a7a48;
|
|
--border: rgba(40, 36, 28, 0.12);
|
|
--border-accent: rgba(40, 36, 28, 0.20);
|
|
}
|
|
|
|
body {
|
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
font-size: 16px;
|
|
background: #eae7de;
|
|
color: #1a1810;
|
|
}
|
|
|
|
h1, h2, h3, h4, h5, h6 {
|
|
font-family: "Manrope", system-ui, sans-serif;
|
|
color: #1a1810;
|
|
}
|
|
|
|
::selection {
|
|
background: rgba(138, 122, 72, 0.20);
|
|
color: #1a1810;
|
|
}
|
|
|
|
/* ---- Nav overrides ---- */
|
|
.site-nav--dark {
|
|
background: rgba(234, 231, 222, 0.92);
|
|
backdrop-filter: blur(12px) saturate(180%);
|
|
-webkit-backdrop-filter: blur(12px) saturate(180%);
|
|
border-bottom: 1px solid rgba(40, 36, 28, 0.12);
|
|
}
|
|
|
|
.nav-logo { color: #1a1810; }
|
|
.nav-link { color: #5e5a4e; opacity: 1; }
|
|
.nav-link:hover { color: #1a1810; }
|
|
|
|
/* ---- Header section ---- */
|
|
.wp-header {
|
|
padding: 80px 0 48px;
|
|
text-align: left;
|
|
}
|
|
|
|
.wp-header h1 {
|
|
font-size: 1.75rem;
|
|
font-weight: 700;
|
|
letter-spacing: -0.02em;
|
|
margin-bottom: 12px;
|
|
color: #1a1810;
|
|
}
|
|
|
|
.wp-header .wp-tagline {
|
|
font-size: 1.05rem;
|
|
color: #5e5a4e;
|
|
margin-bottom: 24px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.wp-header-links {
|
|
display: flex;
|
|
gap: 24px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.wp-header-links a {
|
|
font-size: 0.9rem;
|
|
font-weight: 500;
|
|
color: #8a7a48;
|
|
text-decoration: none;
|
|
border-bottom: 1px solid transparent;
|
|
transition: border-color 0.15s ease;
|
|
}
|
|
|
|
.wp-header-links a:hover {
|
|
border-bottom-color: #8a7a48;
|
|
}
|
|
|
|
/* ---- Sections ---- */
|
|
.wp-section {
|
|
padding: 48px 0;
|
|
border-top: 1px solid rgba(40, 36, 28, 0.10);
|
|
}
|
|
|
|
.wp-section h2 {
|
|
font-size: 1.15rem;
|
|
font-weight: 600;
|
|
letter-spacing: -0.01em;
|
|
margin-bottom: 20px;
|
|
color: #1a1810;
|
|
}
|
|
|
|
.wp-section p {
|
|
color: #5e5a4e;
|
|
line-height: 1.7;
|
|
max-width: 640px;
|
|
}
|
|
|
|
/* ---- Capability list ---- */
|
|
.wp-list {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
max-width: 520px;
|
|
}
|
|
|
|
.wp-list li {
|
|
position: relative;
|
|
padding-left: 20px;
|
|
margin-bottom: 10px;
|
|
color: #5e5a4e;
|
|
font-size: 0.95rem;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.wp-list li::before {
|
|
content: "";
|
|
position: absolute;
|
|
left: 0;
|
|
top: 10px;
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
background: #8a7a48;
|
|
}
|
|
|
|
/* ---- Screenshots ---- */
|
|
.wp-screenshots {
|
|
padding: 48px 0;
|
|
border-top: 1px solid rgba(40, 36, 28, 0.10);
|
|
}
|
|
|
|
.wp-screenshots h2 {
|
|
font-size: 1.15rem;
|
|
font-weight: 600;
|
|
margin-bottom: 24px;
|
|
color: #1a1810;
|
|
}
|
|
|
|
.wp-screenshot-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 24px;
|
|
}
|
|
|
|
.wp-screenshot-card {
|
|
background: #f6f4ec;
|
|
border: 1px solid rgba(40, 36, 28, 0.12);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.wp-screenshot-card img {
|
|
width: 100%;
|
|
display: block;
|
|
cursor: zoom-in;
|
|
transition: opacity 0.2s ease;
|
|
}
|
|
|
|
.wp-screenshot-card img:hover {
|
|
opacity: 0.92;
|
|
}
|
|
|
|
.wp-screenshot-caption {
|
|
padding: 10px 14px;
|
|
font-size: 0.8rem;
|
|
color: #8a8578;
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* ---- Caveats / what-it-is-not ---- */
|
|
.wp-caveats {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
.wp-caveats li {
|
|
color: #5e5a4e;
|
|
font-size: 0.95rem;
|
|
line-height: 1.6;
|
|
margin-bottom: 8px;
|
|
padding-left: 20px;
|
|
position: relative;
|
|
}
|
|
|
|
.wp-caveats li::before {
|
|
content: "\2014";
|
|
position: absolute;
|
|
left: 0;
|
|
color: #8a8578;
|
|
}
|
|
|
|
/* ---- Status table ---- */
|
|
.wp-status-table {
|
|
border-collapse: collapse;
|
|
font-size: 0.9rem;
|
|
max-width: 400px;
|
|
}
|
|
|
|
.wp-status-table td {
|
|
padding: 6px 0;
|
|
vertical-align: top;
|
|
}
|
|
|
|
.wp-status-table td:first-child {
|
|
color: #8a8578;
|
|
padding-right: 24px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.wp-status-table td:last-child {
|
|
color: #1a1810;
|
|
}
|
|
|
|
/* ---- Quick start ---- */
|
|
.wp-quickstart {
|
|
background: #f6f4ec;
|
|
border: 1px solid rgba(40, 36, 28, 0.12);
|
|
border-radius: 8px;
|
|
padding: 20px 24px;
|
|
max-width: 480px;
|
|
margin-top: 16px;
|
|
}
|
|
|
|
.wp-quickstart code {
|
|
font-family: "IBM Plex Mono", "SF Mono", monospace;
|
|
font-size: 0.85rem;
|
|
color: #1a1810;
|
|
display: block;
|
|
line-height: 1.8;
|
|
}
|
|
|
|
.wp-quickstart .wp-comment {
|
|
color: #8a8578;
|
|
}
|
|
|
|
.wp-requirements {
|
|
margin-top: 12px;
|
|
font-size: 0.85rem;
|
|
color: #8a8578;
|
|
}
|
|
|
|
/* ---- Footer overrides ---- */
|
|
.site-footer {
|
|
background: #f0ede4;
|
|
border-top: 1px solid rgba(40, 36, 28, 0.12);
|
|
}
|
|
|
|
.footer-brand { color: #1a1810; }
|
|
.footer-copy { color: #8a8578; }
|
|
.footer-links a { color: #5e5a4e; }
|
|
.footer-links a:hover { color: #1a1810; }
|
|
|
|
/* ---- Lightbox ---- */
|
|
.lightbox {
|
|
background: rgba(26, 24, 16, 0.92);
|
|
}
|
|
|
|
/* ---- Responsive ---- */
|
|
@media (max-width: 768px) {
|
|
.wp-screenshot-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.wp-header {
|
|
padding: 48px 0 32px;
|
|
}
|
|
|
|
.wp-header h1 {
|
|
font-size: 1.5rem;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.wp-header-links {
|
|
gap: 16px;
|
|
}
|
|
|
|
.wp-section {
|
|
padding: 32px 0;
|
|
}
|
|
}
|
|
|
|
/* ---- Kill inherited dark-theme artifacts ---- */
|
|
.hero, .hero-bg, .hero-bg::before, .hero-bg::after,
|
|
.testing-banner, .features-section, .cta-section,
|
|
.quickstart-section, .screenshots-section {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Skip link */
|
|
.skip-link {
|
|
color: #1a1810;
|
|
background: #f6f4ec;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<a href="#main-content" class="skip-link">Skip to main content</a>
|
|
|
|
<!-- Navigation -->
|
|
<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-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>
|
|
<span>The Other Dude</span>
|
|
</a>
|
|
<div class="nav-links">
|
|
<a href="docs.html" class="nav-link">Docs</a>
|
|
<a href="blog/" class="nav-link">Blog</a>
|
|
<a href="https://github.com/staack/the-other-dude" class="nav-link" rel="noopener">GitHub</a>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<main id="main-content">
|
|
|
|
<!-- 1. Header -->
|
|
<div class="container">
|
|
<header class="wp-header">
|
|
<h1>The Other Dude</h1>
|
|
<p class="wp-tagline">MikroTik fleet management. Self-hosted. Source-available.</p>
|
|
<div class="wp-header-links">
|
|
<a href="https://github.com/staack/the-other-dude" rel="noopener">GitHub</a>
|
|
<a href="docs.html">Documentation</a>
|
|
<a href="blog/">Blog</a>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- 2. What it does -->
|
|
<section class="wp-section" id="what-it-does">
|
|
<h2>What it does</h2>
|
|
<ul class="wp-list">
|
|
<li>Monitor device state across your fleet</li>
|
|
<li>Push configuration with automatic rollback</li>
|
|
<li>Track config changes in git</li>
|
|
<li>Manage firmware versions</li>
|
|
<li>WinBox in the browser</li>
|
|
<li>VPN overlay for NAT traversal</li>
|
|
<li>Multi-tenant with row-level security</li>
|
|
<li>Zero-knowledge authentication (SRP-6a)</li>
|
|
</ul>
|
|
</section>
|
|
</div>
|
|
|
|
<!-- 3. Screenshots -->
|
|
<section class="wp-screenshots">
|
|
<div class="container">
|
|
<h2>Screenshots</h2>
|
|
<div class="wp-screenshot-grid">
|
|
|
|
<figure class="wp-screenshot-card screenshot-card">
|
|
<img src="assets/01-overview-light.png" alt="Fleet overview — light mode" loading="lazy" width="1440" height="900">
|
|
<figcaption class="wp-screenshot-caption">Fleet overview — light</figcaption>
|
|
</figure>
|
|
|
|
<figure class="wp-screenshot-card screenshot-card">
|
|
<img src="assets/01-overview-dark.png" alt="Fleet overview — dark mode" loading="lazy" width="1440" height="900">
|
|
<figcaption class="wp-screenshot-caption">Fleet overview — dark</figcaption>
|
|
</figure>
|
|
|
|
<figure class="wp-screenshot-card screenshot-card">
|
|
<img src="assets/02-device-detail-light.png" alt="Device management — light mode" loading="lazy" width="1440" height="900">
|
|
<figcaption class="wp-screenshot-caption">Device management — light</figcaption>
|
|
</figure>
|
|
|
|
<figure class="wp-screenshot-card screenshot-card">
|
|
<img src="assets/02-device-detail-dark.png" alt="Device management — dark mode" loading="lazy" width="1440" height="900">
|
|
<figcaption class="wp-screenshot-caption">Device management — dark</figcaption>
|
|
</figure>
|
|
|
|
<figure class="wp-screenshot-card screenshot-card">
|
|
<img src="assets/03-interfaces-light.png" alt="Interface configuration — light mode" loading="lazy" width="1440" height="900">
|
|
<figcaption class="wp-screenshot-caption">Interface configuration — light</figcaption>
|
|
</figure>
|
|
|
|
<figure class="wp-screenshot-card screenshot-card">
|
|
<img src="assets/03-interfaces-dark.png" alt="Interface configuration — dark mode" loading="lazy" width="1440" height="900">
|
|
<figcaption class="wp-screenshot-caption">Interface configuration — dark</figcaption>
|
|
</figure>
|
|
|
|
<figure class="wp-screenshot-card screenshot-card">
|
|
<img src="assets/04-firmware-light.png" alt="Firmware management — light mode" loading="lazy" width="1440" height="900">
|
|
<figcaption class="wp-screenshot-caption">Firmware management — light</figcaption>
|
|
</figure>
|
|
|
|
<figure class="wp-screenshot-card screenshot-card">
|
|
<img src="assets/04-firmware-dark.png" alt="Firmware management — dark mode" loading="lazy" width="1440" height="900">
|
|
<figcaption class="wp-screenshot-caption">Firmware management — dark</figcaption>
|
|
</figure>
|
|
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="container">
|
|
|
|
<!-- 4. What it is not -->
|
|
<section class="wp-section">
|
|
<h2>What it is not</h2>
|
|
<ul class="wp-caveats">
|
|
<li>Not finished</li>
|
|
<li>Not stable</li>
|
|
<li>Not for everyone</li>
|
|
<li>Things break, APIs change. That is intentional before v11.</li>
|
|
</ul>
|
|
</section>
|
|
|
|
<!-- 5. Status -->
|
|
<section class="wp-section">
|
|
<h2>Status</h2>
|
|
<table class="wp-status-table">
|
|
<tr><td>Version</td><td>9.7.1</td></tr>
|
|
<tr><td>License</td><td>BSL 1.1 (converts to Apache 2.0 in 2030)</td></tr>
|
|
<tr><td>Free tier</td><td>250 devices</td></tr>
|
|
<tr><td>Stability</td><td>Breaking changes expected before v11</td></tr>
|
|
</table>
|
|
</section>
|
|
|
|
<!-- 6. Setup -->
|
|
<section class="wp-section">
|
|
<h2>Setup</h2>
|
|
<p>Requires Docker and PostgreSQL. See the <a href="docs.html#quickstart" style="color:#8a7a48;text-decoration:underline;text-underline-offset:3px">documentation</a> for full setup instructions.</p>
|
|
<div class="wp-quickstart">
|
|
<code><span class="wp-comment"># clone and run the setup wizard</span>
|
|
git clone https://github.com/staack/the-other-dude.git
|
|
cd the-other-dude
|
|
python3 setup.py</code>
|
|
</div>
|
|
<p class="wp-requirements">The setup wizard handles database, cryptographic keys, OpenBao, reverse proxy, and Docker images.</p>
|
|
</section>
|
|
|
|
</div>
|
|
|
|
</main>
|
|
|
|
<!-- Footer -->
|
|
<footer class="site-footer">
|
|
<div class="footer-inner container">
|
|
<div class="footer-brand">
|
|
<span class="footer-logo">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="24" height="24" aria-hidden="true" style="vertical-align: middle; margin-right: 8px;">
|
|
<rect x="2" y="2" width="60" height="60" rx="8" fill="none" stroke="#8B1A1A" stroke-width="2"/>
|
|
<rect x="6" y="6" width="52" height="52" rx="5" fill="none" stroke="#F5E6C8" stroke-width="1.5"/>
|
|
<rect x="8" y="8" width="48" height="48" rx="4" fill="#8B1A1A" opacity="0.15"/>
|
|
<path d="M32 8 L56 32 L32 56 L8 32 Z" fill="none" stroke="#8B1A1A" stroke-width="2"/>
|
|
<path d="M32 13 L51 32 L32 51 L13 32 Z" fill="none" stroke="#F5E6C8" stroke-width="1.5"/>
|
|
<path d="M32 18 L46 32 L32 46 L18 32 Z" fill="#8B1A1A"/>
|
|
<path d="M32 19 L38 32 L32 45 L26 32 Z" fill="#2A9D8F"/>
|
|
<path d="M19 32 L32 26 L45 32 L32 38 Z" fill="#F5E6C8"/>
|
|
<circle cx="32" cy="32" r="5" fill="#8B1A1A"/>
|
|
<circle cx="32" cy="32" r="2.5" fill="#2A9D8F"/>
|
|
</svg>
|
|
The Other Dude
|
|
</span>
|
|
<span class="footer-copy">© 2026 The Other Dude. All rights reserved.</span>
|
|
</div>
|
|
<nav class="footer-links" aria-label="Footer navigation">
|
|
<a href="docs.html">Docs</a>
|
|
<a href="blog/">Blog</a>
|
|
<a href="https://github.com/staack/the-other-dude" rel="noopener">GitHub</a>
|
|
<a href="mailto:license@theotherdude.net">Licensing</a>
|
|
</nav>
|
|
<p style="margin-top:12px;font-size:0.75em;color:#8a8578;text-align:center;">This site uses self-hosted, cookie-free analytics to measure page views and engagement. No personal data is collected or shared with third parties.</p>
|
|
</div>
|
|
</footer>
|
|
|
|
<!-- 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="Screenshot preview">
|
|
<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');
|
|
var lbClose = lb.querySelector('.lightbox-close');
|
|
|
|
function openLightbox(img) {
|
|
lbImg.src = img.src;
|
|
lbImg.alt = img.alt;
|
|
var card = img.closest('.screenshot-card');
|
|
var cap = card.querySelector('figcaption') || card.querySelector('div > div:first-child');
|
|
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.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);
|
|
}
|
|
});
|
|
});
|
|
|
|
lbClose.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
closeLightbox();
|
|
});
|
|
|
|
lb.addEventListener('click', closeLightbox);
|
|
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') closeLightbox();
|
|
});
|
|
})();
|
|
</script>
|
|
<script>
|
|
(function() {
|
|
var h = 'https://telemetry.theotherdude.net';
|
|
var p = location.pathname;
|
|
var t = document.title;
|
|
var r = document.referrer;
|
|
|
|
// Session page count via sessionStorage.
|
|
var sc = parseInt(sessionStorage.getItem('_tc_sc') || '0', 10) + 1;
|
|
sessionStorage.setItem('_tc_sc', sc);
|
|
|
|
// UTM params.
|
|
var sp = new URLSearchParams(location.search);
|
|
var us = sp.get('utm_source') || '';
|
|
var um = sp.get('utm_medium') || '';
|
|
var uc = sp.get('utm_campaign') || '';
|
|
|
|
// Pixel URL with all params.
|
|
var params = new URLSearchParams({
|
|
p: p, t: t, r: r,
|
|
sw: screen.width, sh: screen.height,
|
|
vw: innerWidth, vh: innerHeight,
|
|
tz: new Date().getTimezoneOffset(),
|
|
dpr: devicePixelRatio || 1,
|
|
touch: navigator.maxTouchPoints > 0 ? 1 : 0,
|
|
cd: screen.colorDepth,
|
|
plt: Math.round(performance.now()),
|
|
sc: sc
|
|
});
|
|
if (us) params.set('us', us);
|
|
if (um) params.set('um', um);
|
|
if (uc) params.set('uc', uc);
|
|
|
|
var ct = navigator.connection ? navigator.connection.effectiveType : '';
|
|
if (ct) params.set('ct', ct);
|
|
|
|
new Image().src = h + '/px?' + params.toString();
|
|
|
|
// Engagement tracking.
|
|
var startTime = performance.now();
|
|
var maxScroll = 0;
|
|
|
|
function getScrollDepth() {
|
|
var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
var docHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
|
|
var winHeight = innerHeight;
|
|
if (docHeight <= winHeight) return 100;
|
|
var pct = Math.round((scrollTop + winHeight) / docHeight * 100);
|
|
return Math.min(pct, 100);
|
|
}
|
|
|
|
window.addEventListener('scroll', function() {
|
|
var d = getScrollDepth();
|
|
if (d > maxScroll) maxScroll = d;
|
|
}, {passive: true});
|
|
|
|
// Send beacon on page hide.
|
|
function sendBeacon() {
|
|
var top = Math.round(performance.now() - startTime);
|
|
var data = new URLSearchParams({p: p, top: top, sd: maxScroll});
|
|
navigator.sendBeacon(h + '/px/beacon', data);
|
|
}
|
|
|
|
document.addEventListener('visibilitychange', function() {
|
|
if (document.visibilityState === 'hidden') sendBeacon();
|
|
});
|
|
window.addEventListener('pagehide', sendBeacon);
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|