Files
the-other-dude/docs/website/index.html
Jason Staack b1ac1cce24 feat: v9.8.1 pre-built Docker images and GHCR release workflow
Setup.py now asks whether to pull pre-built images from GHCR
(recommended) or build from source. Pre-built mode skips the
15-minute compile step entirely.

- Add .github/workflows/release.yml (builds+pushes 4 images on tag)
- Add docker-compose.build.yml (source-build overlay)
- Switch docker-compose.prod.yml from build: to image: refs
- Add --build-mode CLI arg and wizard step to setup.py
- Bump version to 9.8.1 across all files
- Document TOD_VERSION env var in CONFIGURATION.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 18:33:12 -05:00

739 lines
25 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="Network fleet management for MikroTik and SNMP devices. Self-hosted. Source-available. Monitor MikroTik routers alongside switches, APs, and UPSes from a single pane of glass.">
<meta name="keywords" content="MikroTik, RouterOS, SNMP monitoring, fleet management, network management, WinBox browser, MikroTik monitoring, MikroTik configuration, SNMP poller, multi-vendor NMS, 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="Network fleet management for MikroTik and SNMP devices. 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="Network fleet management for MikroTik and SNMP devices. 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": "Network fleet management for MikroTik and SNMP devices. 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",
"SNMP device monitoring alongside MikroTik",
"VPN overlay for NAT traversal",
"Multi-tenant with row-level security",
"Zero-knowledge authentication (SRP-6a)"
],
"softwareRequirements": "Docker, PostgreSQL 17, Redis, NATS",
"softwareVersion": "9.8.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: 2px;
overflow: hidden;
position: relative;
cursor: pointer;
}
.wp-screenshot-card .ss-light,
.wp-screenshot-card .ss-dark {
width: 100%;
display: block;
}
.wp-screenshot-card .ss-dark {
position: absolute;
top: 0; left: 0;
opacity: 0;
transition: opacity 150ms linear;
}
.wp-screenshot-card:hover .ss-dark {
opacity: 1;
}
.wp-screenshot-caption {
padding: 8px 14px;
font-size: 0.75rem;
color: #8a8578;
font-weight: 500;
}
.wp-screenshot-expanded {
display: none;
position: fixed;
inset: 0;
z-index: 100;
background: rgba(14,13,11,0.85);
padding: 40px;
cursor: pointer;
align-items: center;
justify-content: center;
gap: 12px;
}
.wp-screenshot-expanded.active { display: flex; }
.wp-screenshot-expanded img {
max-width: 48%;
max-height: 90vh;
border-radius: 2px;
border: 1px solid rgba(180,170,150,0.15);
}
.wp-screenshot-expanded .ss-label {
position: absolute;
bottom: 16px;
font-size: 11px;
color: #948e80;
font-family: 'IBM Plex Mono', monospace;
}
.wp-screenshot-expanded .ss-label-left { left: 40px; }
.wp-screenshot-expanded .ss-label-right { right: 40px; }
/* ---- 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: 2px;
padding: 20px 24px;
max-width: 640px;
margin-top: 16px;
overflow-x: auto;
}
.wp-quickstart code {
font-family: "IBM Plex Mono", "SF Mono", monospace;
font-size: 0.85rem;
color: #1a1810;
display: block;
line-height: 1.8;
white-space: pre;
}
.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" 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. Now with SNMP support. 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>SNMP device monitoring — switches, APs, UPSes alongside your Tiks</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" data-light="assets/01-overview-light.png" data-dark="assets/01-overview-dark.png" onclick="expandScreenshot(this)">
<img class="ss-light" src="assets/01-overview-light.png" alt="Fleet overview" loading="lazy" width="1440" height="900">
<img class="ss-dark" src="assets/01-overview-dark.png" alt="Fleet overview — dark" loading="lazy" width="1440" height="900">
<figcaption class="wp-screenshot-caption">Fleet overview</figcaption>
</figure>
<figure class="wp-screenshot-card" data-light="assets/02-device-detail-light.png" data-dark="assets/02-device-detail-dark.png" onclick="expandScreenshot(this)">
<img class="ss-light" src="assets/02-device-detail-light.png" alt="Device management" loading="lazy" width="1440" height="900">
<img class="ss-dark" src="assets/02-device-detail-dark.png" alt="Device management — dark" loading="lazy" width="1440" height="900">
<figcaption class="wp-screenshot-caption">Device management</figcaption>
</figure>
<figure class="wp-screenshot-card" data-light="assets/03-interfaces-light.png" data-dark="assets/03-interfaces-dark.png" onclick="expandScreenshot(this)">
<img class="ss-light" src="assets/03-interfaces-light.png" alt="Interface configuration" loading="lazy" width="1440" height="900">
<img class="ss-dark" src="assets/03-interfaces-dark.png" alt="Interface configuration — dark" loading="lazy" width="1440" height="900">
<figcaption class="wp-screenshot-caption">Interface configuration</figcaption>
</figure>
<figure class="wp-screenshot-card" data-light="assets/04-firmware-light.png" data-dark="assets/04-firmware-dark.png" onclick="expandScreenshot(this)">
<img class="ss-light" src="assets/04-firmware-light.png" alt="Firmware management" loading="lazy" width="1440" height="900">
<img class="ss-dark" src="assets/04-firmware-dark.png" alt="Firmware management — dark" loading="lazy" width="1440" height="900">
<figcaption class="wp-screenshot-caption">Firmware management</figcaption>
</figure>
</div>
<div class="wp-screenshot-expanded" id="ss-expanded" onclick="this.classList.remove('active')">
<img id="ss-exp-light" src="" alt="Light mode">
<img id="ss-exp-dark" src="" alt="Dark mode">
<span class="ss-label ss-label-left">light</span>
<span class="ss-label ss-label-right">dark</span>
</div>
</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.8.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">&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">&times;</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();
document.getElementById('ss-expanded').classList.remove('active');
}
});
})();
function expandScreenshot(card) {
var overlay = document.getElementById('ss-expanded');
document.getElementById('ss-exp-light').src = card.dataset.light;
document.getElementById('ss-exp-dark').src = card.dataset.dark;
overlay.classList.add('active');
}
</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>