Files
nuc/docs/monitoring-presentation.html
Alejandro Gutiérrez 8b503a549c Add operational documentation
CloudBeaver database manager guide, Ecija intranet deployment,
Gitea-Coolify auto-deploy and integration docs, monitoring setup
with presentation, remote access guide, security architecture,
and Turbostarter deployment procedure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 15:17:18 +01:00

666 lines
24 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NUC Monitoring & Recovery System</title>
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--background: #0C0A09;
--foreground: #FAFAF9;
--surface-card: #1C1917;
--surface-muted: #292524;
--accent-cyan: #06B6D4;
--accent-green: #22C55E;
--accent-orange: #F97316;
--accent-red: #EF4444;
--accent-purple: #A855F7;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', sans-serif;
background: var(--background);
color: var(--foreground);
min-height: 100vh;
overflow: hidden;
}
.slide {
min-height: 100vh;
padding: 3rem;
display: flex;
flex-direction: column;
justify-content: center;
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
h1 { font-size: 3rem; font-weight: 700; margin-bottom: 1rem; }
h2 { font-size: 2rem; font-weight: 600; margin-bottom: 1.5rem; color: var(--accent-cyan); }
h3 { font-size: 1.25rem; font-weight: 600; margin-bottom: 0.75rem; }
.subtitle { font-size: 1.25rem; color: #A8A29E; margin-bottom: 2rem; }
.card {
background: var(--surface-card);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1rem;
border: 1px solid var(--surface-muted);
}
.grid { display: grid; gap: 1rem; }
.grid-2 { grid-template-columns: repeat(2, 1fr); }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-4 { grid-template-columns: repeat(4, 1fr); }
.badge {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
gap: 0.375rem;
}
.badge-green { background: rgba(34, 197, 94, 0.2); color: var(--accent-green); }
.badge-cyan { background: rgba(6, 182, 212, 0.2); color: var(--accent-cyan); }
.badge-orange { background: rgba(249, 115, 22, 0.2); color: var(--accent-orange); }
.badge-red { background: rgba(239, 68, 68, 0.2); color: var(--accent-red); }
.badge-purple { background: rgba(168, 85, 247, 0.2); color: var(--accent-purple); }
.dot { width: 8px; height: 8px; border-radius: 50%; }
.dot-green { background: var(--accent-green); }
.dot-cyan { background: var(--accent-cyan); }
.dot-orange { background: var(--accent-orange); }
.dot-red { background: var(--accent-red); }
.icon { font-size: 2rem; margin-bottom: 0.5rem; }
.flow-diagram {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
padding: 2rem 0;
}
.flow-box {
background: var(--surface-card);
border: 2px solid var(--surface-muted);
border-radius: 8px;
padding: 1rem 1.5rem;
text-align: center;
min-width: 140px;
}
.flow-arrow {
color: var(--accent-cyan);
font-size: 1.5rem;
}
table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
}
th, td {
text-align: left;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--surface-muted);
}
th { color: #A8A29E; font-weight: 500; font-size: 0.875rem; }
code {
background: var(--surface-muted);
padding: 0.125rem 0.5rem;
border-radius: 4px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.875rem;
}
.nav {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 0.5rem;
background: var(--surface-card);
padding: 0.5rem;
border-radius: 9999px;
border: 1px solid var(--surface-muted);
z-index: 100;
}
.nav-btn {
width: 40px;
height: 40px;
border-radius: 50%;
border: none;
background: transparent;
color: var(--foreground);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
transition: background 0.2s;
}
.nav-btn:hover { background: var(--surface-muted); }
.nav-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.nav-dots {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0 0.5rem;
}
.nav-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--surface-muted);
cursor: pointer;
transition: all 0.2s;
}
.nav-dot.active { background: var(--accent-cyan); width: 24px; border-radius: 4px; }
.highlight { color: var(--accent-cyan); }
.text-muted { color: #A8A29E; }
.text-sm { font-size: 0.875rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-4 { margin-top: 1rem; }
.mb-4 { margin-bottom: 1rem; }
.text-center { text-align: center; }
.logo {
font-size: 1rem;
font-weight: 600;
color: var(--accent-cyan);
position: fixed;
top: 2rem;
left: 2rem;
}
.slide-number {
position: fixed;
top: 2rem;
right: 2rem;
color: #A8A29E;
font-size: 0.875rem;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
const slides = [
// Slide 1: Title
() => (
<div className="slide" style={{ justifyContent: 'center', alignItems: 'center', textAlign: 'center' }}>
<div style={{ fontSize: '4rem', marginBottom: '1rem' }}>🖥</div>
<h1>NUC Monitoring & Recovery</h1>
<p className="subtitle">Production-grade infrastructure monitoring for home lab</p>
<div style={{ display: 'flex', gap: '1rem', marginTop: '2rem' }}>
<span className="badge badge-cyan"><span className="dot dot-cyan"></span>Prometheus</span>
<span className="badge badge-green"><span className="dot dot-green"></span>Grafana</span>
<span className="badge badge-orange"><span className="dot dot-orange"></span>Alertmanager</span>
<span className="badge badge-purple"><span className="dot dot-purple"></span>Auto-Recovery</span>
</div>
</div>
),
// Slide 2: Architecture Overview
() => (
<div className="slide">
<h2>Architecture Overview</h2>
<div className="flow-diagram">
<div className="flow-box">
<div>📡</div>
<div>OpenWrt</div>
<div className="text-sm text-muted">:9100</div>
</div>
<div className="flow-arrow"></div>
<div className="flow-box" style={{ borderColor: 'var(--accent-cyan)' }}>
<div>📊</div>
<div>Prometheus</div>
<div className="text-sm text-muted">:9091</div>
</div>
<div className="flow-arrow"></div>
<div className="flow-box">
<div>🔔</div>
<div>Alertmanager</div>
<div className="text-sm text-muted">:9093</div>
</div>
<div className="flow-arrow"></div>
<div className="flow-box" style={{ borderColor: 'var(--accent-orange)' }}>
<div>📱</div>
<div>ntfy</div>
<div className="text-sm text-muted">push alerts</div>
</div>
</div>
<div className="flow-diagram">
<div className="flow-box">
<div>🖥</div>
<div>NUC Node</div>
<div className="text-sm text-muted">:9100</div>
</div>
<div className="flow-arrow"></div>
<div className="flow-box" style={{ borderColor: 'var(--accent-green)' }}>
<div>📈</div>
<div>Grafana</div>
<div className="text-sm text-muted">:3333</div>
</div>
<div className="flow-arrow"></div>
<div className="flow-box">
<div>🌐</div>
<div>Tailscale</div>
<div className="text-sm text-muted">Funnel</div>
</div>
<div className="flow-arrow"></div>
<div className="flow-box" style={{ borderColor: 'var(--accent-purple)' }}>
<div>📲</div>
<div>Remote Access</div>
<div className="text-sm text-muted">anywhere</div>
</div>
</div>
</div>
),
// Slide 3: Metrics Collection
() => (
<div className="slide">
<h2>Metrics Collection</h2>
<div className="grid grid-3">
<div className="card">
<div className="icon">📡</div>
<h3>OpenWrt Router</h3>
<p className="text-muted text-sm mb-4">prometheus-node-exporter-lua</p>
<table>
<tbody>
<tr><td>CPU</td><td className="highlight"></td></tr>
<tr><td>Memory</td><td className="highlight"></td></tr>
<tr><td>Network</td><td className="highlight"></td></tr>
<tr><td>Thermal</td><td className="highlight"></td></tr>
<tr><td>Conntrack</td><td className="highlight"></td></tr>
</tbody>
</table>
</div>
<div className="card">
<div className="icon">🖥</div>
<h3>NUC Server</h3>
<p className="text-muted text-sm mb-4">prometheus-node-exporter</p>
<table>
<tbody>
<tr><td>CPU / Load</td><td className="highlight"></td></tr>
<tr><td>Memory</td><td className="highlight"></td></tr>
<tr><td>Disk I/O</td><td className="highlight"></td></tr>
<tr><td>Network</td><td className="highlight"></td></tr>
<tr><td>Filesystem</td><td className="highlight"></td></tr>
</tbody>
</table>
</div>
<div className="card">
<div className="icon">📊</div>
<h3>Prometheus</h3>
<p className="text-muted text-sm mb-4">Scrape & Store</p>
<table>
<tbody>
<tr><td>Interval</td><td><code>15s</code></td></tr>
<tr><td>Retention</td><td><code>30 days</code></td></tr>
<tr><td>Targets</td><td><code>3</code></td></tr>
<tr><td>Alert Rules</td><td><code>6</code></td></tr>
</tbody>
</table>
</div>
</div>
</div>
),
// Slide 4: Alert Rules
() => (
<div className="slide">
<h2>Alert Rules</h2>
<div className="grid grid-2">
<div className="card">
<h3>🖥 NUC Alerts</h3>
<table>
<thead>
<tr><th>Alert</th><th>Condition</th><th>Severity</th></tr>
</thead>
<tbody>
<tr>
<td>NUCDown</td>
<td><code>up == 0</code> for 1m</td>
<td><span className="badge badge-red">critical</span></td>
</tr>
<tr>
<td>HighCPULoad</td>
<td><code>CPU > 80%</code> for 5m</td>
<td><span className="badge badge-orange">warning</span></td>
</tr>
<tr>
<td>HighMemory</td>
<td><code>Memory > 85%</code> for 5m</td>
<td><span className="badge badge-orange">warning</span></td>
</tr>
<tr>
<td>DiskSpaceLow</td>
<td><code>Disk > 85%</code> for 5m</td>
<td><span className="badge badge-orange">warning</span></td>
</tr>
</tbody>
</table>
</div>
<div className="card">
<h3>📡 OpenWrt Alerts</h3>
<table>
<thead>
<tr><th>Alert</th><th>Condition</th><th>Severity</th></tr>
</thead>
<tbody>
<tr>
<td>OpenWrtDown</td>
<td><code>up == 0</code> for 1m</td>
<td><span className="badge badge-red">critical</span></td>
</tr>
<tr>
<td>HighLoad</td>
<td><code>load > 2</code> for 5m</td>
<td><span className="badge badge-orange">warning</span></td>
</tr>
</tbody>
</table>
<div className="mt-4">
<h3>📱 Notification Flow</h3>
<p className="text-muted text-sm mt-2">
Alertmanager Bridge ntfy.sh/nuc-watchdog Phone
</p>
</div>
</div>
</div>
</div>
),
// Slide 5: Auto-Recovery System
() => (
<div className="slide">
<h2>Auto-Recovery System</h2>
<p className="subtitle">Multi-layer recovery ensures maximum uptime</p>
<div className="grid grid-4">
<div className="card text-center">
<div style={{ fontSize: '3rem' }}>1</div>
<h3 className="highlight">Wake-on-LAN</h3>
<p className="text-sm text-muted mt-2">OpenWrt sends magic packet every 2 min while NUC is down</p>
<div className="mt-4">
<code>etherwake 94:c6:91:1f:c9:c5</code>
</div>
</div>
<div className="card text-center">
<div style={{ fontSize: '3rem' }}>2</div>
<h3 className="highlight">SSH Reboot</h3>
<p className="text-sm text-muted mt-2">Passwordless sudo configured for remote reboot</p>
<div className="mt-4">
<code>sudo reboot</code>
</div>
</div>
<div className="card text-center">
<div style={{ fontSize: '3rem' }}>3</div>
<h3 className="highlight">Hardware Watchdog</h3>
<p className="text-sm text-muted mt-2">Kernel watchdog auto-reboots on system freeze</p>
<div className="mt-4">
<code>systemd watchdog</code>
</div>
</div>
<div className="card text-center">
<div style={{ fontSize: '3rem' }}>4</div>
<h3 className="highlight">Panic Reboot</h3>
<p className="text-sm text-muted mt-2">Kernel auto-reboots 10s after panic</p>
<div className="mt-4">
<code>kernel.panic=10</code>
</div>
</div>
</div>
</div>
),
// Slide 6: OpenWrt Monitor
() => (
<div className="slide">
<h2>OpenWrt Health Monitor</h2>
<div className="grid grid-2">
<div className="card">
<h3>🔍 Check Logic</h3>
<div style={{ background: 'var(--surface-muted)', padding: '1rem', borderRadius: '8px', marginTop: '1rem' }}>
<pre style={{ fontFamily: 'monospace', fontSize: '0.8rem', lineHeight: '1.6' }}>
{`Every 30 seconds:
├─ HTTP check (port 3000)
├─ Ping check
├─ Both OK → Reset failures
├─ Ping OK, HTTP fail → Service degraded
└─ Both fail → NUC down
├─ Alert via ntfy
└─ Send WoL (every 2 min)`}
</pre>
</div>
</div>
<div className="card">
<h3> Configuration</h3>
<table>
<tbody>
<tr><td>Location</td><td><code>/opt/monitor-nuc.sh</code></td></tr>
<tr><td>Check Interval</td><td><code>30 seconds</code></td></tr>
<tr><td>Fail Threshold</td><td><code>3 checks</code></td></tr>
<tr><td>WoL Retry</td><td><code>2 minutes</code></td></tr>
<tr><td>NUC MAC</td><td><code>94:c6:91:1f:c9:c5</code></td></tr>
<tr><td>Alert Channel</td><td><code>ntfy.sh/nuc-watchdog</code></td></tr>
</tbody>
</table>
</div>
</div>
</div>
),
// Slide 7: Remote Access
() => (
<div className="slide">
<h2>Remote Access</h2>
<div className="grid grid-2">
<div className="card">
<div className="icon">🔷</div>
<h3>Tailscale (NUC)</h3>
<p className="text-muted text-sm mb-4">Zero-config mesh VPN with Funnel</p>
<table>
<tbody>
<tr><td>Grafana URL</td><td><code>alezmad-nuc.tail58f5ad.ts.net</code></td></tr>
<tr><td>Access</td><td>Anywhere, no VPN app needed</td></tr>
<tr><td>Protocol</td><td>HTTPS (auto-cert)</td></tr>
</tbody>
</table>
<div className="mt-4">
<span className="badge badge-cyan">Best for: Services</span>
</div>
</div>
<div className="card">
<div className="icon">🔐</div>
<h3>WireGuard (OpenWrt)</h3>
<p className="text-muted text-sm mb-4">Full LAN access, works if NUC is down</p>
<table>
<tbody>
<tr><td>Endpoint</td><td><code>5.224.196.245:51820</code></td></tr>
<tr><td>VPN Subnet</td><td><code>10.10.10.0/24</code></td></tr>
<tr><td>Config</td><td><code>~/wireguard/home-vpn.conf</code></td></tr>
</tbody>
</table>
<div className="mt-4">
<span className="badge badge-green">Best for: Full LAN / Recovery</span>
</div>
</div>
</div>
</div>
),
// Slide 8: Access URLs
() => (
<div className="slide">
<h2>Quick Reference</h2>
<div className="card">
<h3>🔗 Access URLs</h3>
<table>
<thead>
<tr><th>Service</th><th>Local</th><th>Remote</th><th>Credentials</th></tr>
</thead>
<tbody>
<tr>
<td><span className="badge badge-green">Grafana</span></td>
<td><code>192.168.1.3:3333</code></td>
<td><code>alezmad-nuc.tail58f5ad.ts.net</code></td>
<td>admin / nucmonitoring</td>
</tr>
<tr>
<td><span className="badge badge-cyan">Prometheus</span></td>
<td><code>192.168.1.3:9091</code></td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td><span className="badge badge-orange">Alertmanager</span></td>
<td><code>192.168.1.3:9093</code></td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td><span className="badge badge-purple">ntfy</span></td>
<td colspan="2"><code>ntfy.sh/nuc-watchdog</code></td>
<td>Subscribe in app</td>
</tr>
</tbody>
</table>
</div>
<div className="card mt-4">
<h3>🛠 Maintenance Commands</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.5rem', marginTop: '1rem' }}>
<code>curl localhost:9091/api/v1/targets</code>
<span className="text-muted text-sm">Check Prometheus targets</span>
<code>curl localhost:9093/api/v2/alerts</code>
<span className="text-muted text-sm">View active alerts</span>
<code>docker restart prometheus-*</code>
<span className="text-muted text-sm">Restart monitoring</span>
<code>curl -d "test" ntfy.sh/nuc-watchdog</code>
<span className="text-muted text-sm">Test notification</span>
</div>
</div>
</div>
),
// Slide 9: Summary
() => (
<div className="slide" style={{ justifyContent: 'center', alignItems: 'center', textAlign: 'center' }}>
<h1>System Status</h1>
<div className="grid grid-4 mt-4" style={{ maxWidth: '900px' }}>
<div className="card text-center">
<span className="badge badge-green"><span className="dot dot-green"></span>Prometheus</span>
<div style={{ fontSize: '2rem', marginTop: '1rem' }}></div>
<p className="text-sm text-muted mt-2">3 targets scraped</p>
</div>
<div className="card text-center">
<span className="badge badge-green"><span className="dot dot-green"></span>Grafana</span>
<div style={{ fontSize: '2rem', marginTop: '1rem' }}></div>
<p className="text-sm text-muted mt-2">Remote access on</p>
</div>
<div className="card text-center">
<span className="badge badge-green"><span className="dot dot-green"></span>Alerts</span>
<div style={{ fontSize: '2rem', marginTop: '1rem' }}></div>
<p className="text-sm text-muted mt-2">6 rules active</p>
</div>
<div className="card text-center">
<span className="badge badge-green"><span className="dot dot-green"></span>Recovery</span>
<div style={{ fontSize: '2rem', marginTop: '1rem' }}></div>
<p className="text-sm text-muted mt-2">4 layers ready</p>
</div>
</div>
<p className="subtitle mt-4">Production-ready monitoring for home infrastructure</p>
</div>
),
];
function Presentation() {
const [currentSlide, setCurrentSlide] = useState(0);
useEffect(() => {
const handleKeyDown = (e) => {
if (e.key === 'ArrowRight' || e.key === ' ') {
setCurrentSlide(s => Math.min(s + 1, slides.length - 1));
} else if (e.key === 'ArrowLeft') {
setCurrentSlide(s => Math.max(s - 1, 0));
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
const SlideComponent = slides[currentSlide];
return (
<>
<div className="logo">NUC Portal</div>
<div className="slide-number">{currentSlide + 1} / {slides.length}</div>
<SlideComponent key={currentSlide} />
<nav className="nav">
<button
className="nav-btn"
onClick={() => setCurrentSlide(s => Math.max(s - 1, 0))}
disabled={currentSlide === 0}
>
</button>
<div className="nav-dots">
{slides.map((_, i) => (
<div
key={i}
className={`nav-dot ${i === currentSlide ? 'active' : ''}`}
onClick={() => setCurrentSlide(i)}
/>
))}
</div>
<button
className="nav-btn"
onClick={() => setCurrentSlide(s => Math.min(s + 1, slides.length - 1))}
disabled={currentSlide === slides.length - 1}
>
</button>
</nav>
</>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<Presentation />);
</script>
</body>
</html>