527 lines
21 KiB
HTML
527 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<!--
|
|
=============================================================
|
|
KINGDEZIGNS — CONVERSATION SUMMARY TEMPLATE
|
|
=============================================================
|
|
HOW TO USE:
|
|
Give this file to Claude at the end of any troubleshooting
|
|
session and say:
|
|
|
|
"Use my summary template to document this conversation."
|
|
|
|
Claude will fill in all sections below based on what
|
|
actually happened in the conversation. Remove or add
|
|
sections as needed per session. Sections marked OPTIONAL
|
|
can be deleted if not applicable.
|
|
=============================================================
|
|
-->
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>KingDezigns — CrowdSec Local Alert Digest Summary</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500&display=swap" rel="stylesheet">
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
:root {
|
|
--bg: #f7f6f2;
|
|
--surface: #ffffff;
|
|
--surface-alt: #f1f0eb;
|
|
--border: #e2e0d8;
|
|
--border-strong: #c8c5ba;
|
|
--text-primary: #1a1917;
|
|
--text-secondary: #6b6960;
|
|
--text-muted: #9b9890;
|
|
--green-bg: #eaf3de;
|
|
--green-border: #97c459;
|
|
--green-text: #2d5a0e;
|
|
--red-bg: #fcebeb;
|
|
--red-border: #f09595;
|
|
--red-text: #7a1f1f;
|
|
--amber-bg: #faeeda;
|
|
--amber-border: #fac775;
|
|
--amber-text: #633806;
|
|
--coral-bg: #faece7;
|
|
--coral-border: #f0997b;
|
|
--coral-text: #4a1b0c;
|
|
--mono: 'IBM Plex Mono', monospace;
|
|
--sans: 'IBM Plex Sans', sans-serif;
|
|
}
|
|
|
|
body {
|
|
font-family: var(--sans);
|
|
font-size: 14px;
|
|
background: var(--bg);
|
|
color: var(--text-primary);
|
|
line-height: 1.7;
|
|
padding: 2.5rem 1.5rem;
|
|
}
|
|
|
|
.page { max-width: 860px; margin: 0 auto; }
|
|
|
|
header {
|
|
border-bottom: 1px solid var(--border-strong);
|
|
padding-bottom: 1.25rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
header h1 {
|
|
font-family: var(--mono);
|
|
font-size: 18px;
|
|
font-weight: 500;
|
|
letter-spacing: -0.02em;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.meta {
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
font-family: var(--mono);
|
|
display: flex;
|
|
gap: 16px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.section { margin-bottom: 2.25rem; }
|
|
|
|
.section-title {
|
|
font-family: var(--mono);
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
color: var(--text-muted);
|
|
margin-bottom: 1rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.section-title::before {
|
|
content: attr(data-num);
|
|
background: var(--text-primary);
|
|
color: var(--bg);
|
|
font-size: 10px;
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 50%;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.section-title::after {
|
|
content: '';
|
|
flex: 1;
|
|
height: 1px;
|
|
background: var(--border);
|
|
}
|
|
|
|
p { margin-bottom: 0.75rem; color: var(--text-primary); }
|
|
p:last-child { margin-bottom: 0; }
|
|
|
|
.badge-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 1rem; }
|
|
|
|
.badge {
|
|
font-family: var(--mono);
|
|
font-size: 11px;
|
|
padding: 3px 10px;
|
|
border-radius: 3px;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.badge.red { background: var(--red-bg); color: var(--red-text); border-color: var(--red-border); }
|
|
.badge.green { background: var(--green-bg); color: var(--green-text); border-color: var(--green-border); }
|
|
.badge.amber { background: var(--amber-bg); color: var(--amber-text); border-color: var(--amber-border); }
|
|
|
|
.step {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 1rem 1.25rem;
|
|
margin-bottom: 0.6rem;
|
|
}
|
|
|
|
.step-header {
|
|
font-family: var(--mono);
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 0.6rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.step-num {
|
|
background: var(--surface-alt);
|
|
border: 1px solid var(--border);
|
|
border-radius: 50%;
|
|
width: 20px;
|
|
height: 20px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 10px;
|
|
flex-shrink: 0;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
pre {
|
|
background: var(--surface-alt);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
padding: 10px 12px;
|
|
font-family: var(--mono);
|
|
font-size: 12px;
|
|
overflow-x: auto;
|
|
margin: 0.5rem 0;
|
|
white-space: pre;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
code {
|
|
font-family: var(--mono);
|
|
font-size: 12px;
|
|
background: var(--surface-alt);
|
|
padding: 1px 5px;
|
|
border-radius: 3px;
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.why {
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
margin-top: 0.4rem;
|
|
padding-left: 2px;
|
|
}
|
|
.why::before { content: '↳ why: '; font-family: var(--mono); }
|
|
|
|
.result { font-size: 12px; margin-top: 0.35rem; padding-left: 2px; }
|
|
.result.pass { color: var(--green-text); }
|
|
.result.pass::before { content: '✓ '; font-family: var(--mono); }
|
|
.result.fail { color: var(--red-text); }
|
|
.result.fail::before { content: '✗ '; font-family: var(--mono); }
|
|
.result.neutral { color: var(--text-secondary); }
|
|
.result.neutral::before { content: '→ '; font-family: var(--mono); }
|
|
|
|
.callout { border-radius: 6px; padding: 1rem 1.25rem; margin-bottom: 0.75rem; }
|
|
.callout.coral { background: var(--coral-bg); border: 1px solid var(--coral-border); }
|
|
.callout.coral p { color: var(--coral-text); }
|
|
.callout.coral pre { background: #fdf0eb; border-color: var(--coral-border); }
|
|
.callout.amber { background: var(--amber-bg); border: 1px solid var(--amber-border); }
|
|
.callout.amber p { color: var(--amber-text); }
|
|
.callout.amber pre { background: #fdf5e6; border-color: var(--amber-border); }
|
|
.callout.amber code { background: #fdf5e6; border-color: var(--amber-border); color: var(--amber-text); }
|
|
.callout.green { background: var(--green-bg); border: 1px solid var(--green-border); }
|
|
.callout.green p { color: var(--green-text); }
|
|
.callout.green pre { background: #d8efc0; border-color: var(--green-border); }
|
|
|
|
.clue-label {
|
|
font-family: var(--mono);
|
|
font-size: 10px;
|
|
font-weight: 500;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.07em;
|
|
color: var(--amber-text);
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.script-box { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
|
|
.script-row { padding: 0.75rem 1.25rem; border-bottom: 1px solid var(--border); }
|
|
.script-row:last-child { border-bottom: none; }
|
|
.script-label { font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-muted); margin-bottom: 6px; }
|
|
.script-row pre { margin: 0; }
|
|
|
|
table { width: 100%; border-collapse: collapse; font-size: 13px; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
|
|
th { text-align: left; padding: 8px 14px; font-family: var(--mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); background: var(--surface-alt); border-bottom: 1px solid var(--border); font-weight: 500; }
|
|
td { padding: 9px 14px; border-bottom: 1px solid var(--border); vertical-align: top; line-height: 1.5; }
|
|
tr:last-child td { border-bottom: none; }
|
|
|
|
.footer {
|
|
font-family: var(--mono);
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
margin-top: 2.5rem;
|
|
padding-top: 1rem;
|
|
border-top: 1px solid var(--border);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
|
|
/* TEMPLATE ONLY — highlights placeholder text in browser, invisible in final output */
|
|
.placeholder {
|
|
background: #fff3cd;
|
|
border: 1px dashed #ffc107;
|
|
border-radius: 3px;
|
|
padding: 1px 6px;
|
|
font-family: var(--mono);
|
|
font-size: 12px;
|
|
color: #856404;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="page">
|
|
|
|
<!-- =============================================
|
|
HEADER
|
|
Fill in: topic title, date, system/device info
|
|
============================================= -->
|
|
<header>
|
|
<h1>KingDezigns — CrowdSec Local Alert Digest summary</h1>
|
|
<div class="meta">
|
|
<span>📅 2026-06-09</span>
|
|
<span>🖥 NAS16 (CrowdSec notifier) / HAS (CrowdSec LAPI)</span>
|
|
<span>⚙️ OpenMediaVault / OMV Scheduled Jobs</span>
|
|
<span>🐳 Bash + Python3 + SMTP_SSL</span>
|
|
<span>☁️ CrowdSec LAPI stream</span>
|
|
</div>
|
|
</header>
|
|
|
|
|
|
<!-- =============================================
|
|
SECTION 1 — THE PROBLEM
|
|
Describe what broke, what the symptoms were,
|
|
and what changed before it broke.
|
|
============================================= -->
|
|
<div class="section">
|
|
<div class="section-title" data-num="1">The problem</div>
|
|
|
|
<p>The CrowdSec local notifier (<code>cs_notifier.sh</code>) sent an immediate, fully-styled HTML email for every single locally-detected attack ban (origin = <code>crowdsec</code>). During active scanning/probing periods this produced dozens to hundreds of emails per week, which was far more volume than expected and made the inbox unmanageable.</p>
|
|
|
|
<div class="badge-row">
|
|
<span class="badge amber">"CrowdSec LIVE ATTACK Detected on HAS - $IP" (one email per ban)</span>
|
|
<span class="badge amber">Hundreds of emails received weekly</span>
|
|
</div>
|
|
|
|
<p>No infrastructure change caused this — it was the original by-design behavior of the notifier (immediate per-decision alerting), which became unmanageable at the observed local attack volume.</p>
|
|
</div>
|
|
|
|
|
|
<!-- =============================================
|
|
SECTION 2 — DIAGNOSTIC COMMANDS
|
|
One .step block per command or command group.
|
|
Only include commands that contributed to
|
|
the diagnosis. Remove dead ends.
|
|
|
|
.result classes:
|
|
pass = confirmed something working (green)
|
|
fail = confirmed something broken (red)
|
|
neutral = ruled something out (gray)
|
|
============================================= -->
|
|
<div class="section">
|
|
<div class="section-title" data-num="2">Build & test steps</div>
|
|
|
|
<div class="step">
|
|
<div class="step-header"><span class="step-num">1</span> Reviewed original cs_notifier.sh</div>
|
|
<pre>Reviewed full per-IP email-sending loop in cs_notifier.sh</pre>
|
|
<p class="why">Needed to understand existing LAPI polling, origin routing, and email format before changing behavior</p>
|
|
<p class="result neutral">Confirmed: every "crowdsec" origin decision triggered an immediate styled HTML email; CAPI decisions were already batched into a midnight digest</p>
|
|
</div>
|
|
|
|
<div class="step">
|
|
<div class="step-header"><span class="step-num">2</span> Split notifier into polling + digest</div>
|
|
<pre>cs_notifier.sh: append local "crowdsec" decisions to local_pending.json (no email)
|
|
cs_local_digest.sh (new): read local_pending.json, send one batched email, clear log</pre>
|
|
<p class="why">Decouples fast polling (every 5 min) from email sending (every 4 hours) so volume drops without losing any ban data</p>
|
|
<p class="result pass">cs_notifier.sh simplified — CAPI routing logic unchanged, local routing now just appends to a pending file</p>
|
|
</div>
|
|
|
|
<div class="step">
|
|
<div class="step-header"><span class="step-num">3</span> Manual test — inject fake ban</div>
|
|
<pre>sudo bash -c 'echo '"'"'{"value":"1.2.3.4","scenario":"crowdsecurity/http-probing","duration":"4h","type":"ban","scope":"Ip","origin":"crowdsec","start_at":"2026-06-09T14:00:00Z"}'"'"' >> /var/log/crowdsec-notifier/local_pending.json'
|
|
sudo bash /usr/scripts/crowdsec/cs_local_digest.sh
|
|
sudo tail -20 /var/log/crowdsec-notifier/notifier.log</pre>
|
|
<p class="why">Verify digest builds, sends, and clears the pending log without waiting for a real attack</p>
|
|
<p class="result fail">First attempts: email format didn't match expectations, and the "Banned IPs — Past 4 Hours" table rendered with an empty body — IP rows were missing entirely</p>
|
|
</div>
|
|
|
|
<div class="step">
|
|
<div class="step-header"><span class="step-num">4</span> Isolated row-generation bug</div>
|
|
<pre>echo "$SNAPSHOT" | python3 - << 'PYEOF'
|
|
... (reads sys.stdin) ...
|
|
PYEOF</pre>
|
|
<p class="why">Table rows were empty even though local_pending.json had data — needed to isolate whether the bug was in JSON parsing or in HTML assembly</p>
|
|
<p class="result fail">Confirmed: this pattern produced zero output. A heredoc attached to <code>python3 -</code> becomes stdin itself, silently overriding the piped data — <code>sys.stdin</code> in Python was reading the heredoc body, not $SNAPSHOT</p>
|
|
</div>
|
|
|
|
<div class="step">
|
|
<div class="step-header"><span class="step-num">5</span> Fixed with inline variable substitution</div>
|
|
<pre>python3 << PYEOF
|
|
snapshot = """$SNAPSHOT"""
|
|
for line in snapshot.strip().splitlines():
|
|
...
|
|
PYEOF</pre>
|
|
<p class="why">Pass $SNAPSHOT directly into the heredoc as a shell-expanded Python string instead of piping</p>
|
|
<p class="result pass">Re-ran full test — table populated correctly with IP, scenario, type, duration, scope, detected time, and CTI/WHOIS/AbuseIPDB links; log confirmed send + local_pending.json cleared</p>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
<!-- =============================================
|
|
SECTION 3 — ROOT CAUSE
|
|
Use the coral callout for the cause statement.
|
|
Use the amber callout for any key clue or
|
|
missed signal.
|
|
Use the green callout for the resolution.
|
|
============================================= -->
|
|
<div class="section">
|
|
<div class="section-title" data-num="3">Root cause</div>
|
|
|
|
<div class="callout coral">
|
|
<p><strong>Notification volume:</strong> the original notifier sent one full HTML email per locally-detected ban with no batching, which scales linearly with attack frequency and quickly becomes unmanageable.</p>
|
|
</div>
|
|
|
|
<div class="callout amber">
|
|
<div class="clue-label">⚠ Key clue (table-rendering bug during testing)</div>
|
|
<p style="margin-bottom:6px;">The "Banned IPs — Past 4 Hours" table rendered with headers but an empty <tbody>. The script logged a non-zero pending count, so data existed — the failure was in row generation, not data collection.</p>
|
|
<pre>echo "$SNAPSHOT" | python3 - << 'PYEOF'
|
|
import sys, json
|
|
for line in sys.stdin: # <- reads the HEREDOC body, not $SNAPSHOT
|
|
...
|
|
PYEOF</pre>
|
|
<p style="margin-top:8px;">When a heredoc is attached directly to <code>python3 -</code>, the heredoc body becomes the process's stdin, completely overriding any piped input. <code>sys.stdin</code> received the (empty) heredoc content instead of $SNAPSHOT, so the loop never executed.</p>
|
|
</div>
|
|
|
|
<div class="callout green" style="margin-top:0.75rem;">
|
|
<p><strong>Resolution:</strong> Replaced immediate per-ban emails with a 5-minute polling script that appends local bans to <code>local_pending.json</code>, plus a new <code>cs_local_digest.sh</code> that runs every 4 hours, builds one batched email (stat cards + full IP table), sends it, and clears the log — and fixed the row-generation bug by passing <code>$SNAPSHOT</code> as an inline Python variable inside the heredoc instead of piping it.</p>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- Section 4 (Secondary issues) not applicable for this session -->
|
|
|
|
|
|
<!-- =============================================
|
|
SECTION 5 — SCRIPTS / FILES CREATED (OPTIONAL)
|
|
Use this section if any scripts, config files,
|
|
or tools were created during the session.
|
|
============================================= -->
|
|
<div class="section">
|
|
<div class="section-title" data-num="5">Scripts / files created</div>
|
|
<div class="script-box">
|
|
<div class="script-row">
|
|
<div class="script-label">Replace (updated)</div>
|
|
<pre>/usr/scripts/crowdsec/cs_notifier.sh</pre>
|
|
</div>
|
|
<div class="script-row">
|
|
<div class="script-label">Add (new)</div>
|
|
<pre>/usr/scripts/crowdsec/cs_local_digest.sh</pre>
|
|
</div>
|
|
<div class="script-row">
|
|
<div class="script-label">Setup</div>
|
|
<pre>chmod +x /usr/scripts/crowdsec/cs_notifier.sh
|
|
chmod +x /usr/scripts/crowdsec/cs_local_digest.sh
|
|
mkdir -p /var/log/crowdsec-notifier</pre>
|
|
</div>
|
|
<div class="script-row">
|
|
<div class="script-label">Run — cs_notifier.sh (existing OMV job, unchanged schedule)</div>
|
|
<pre>Every 5 minutes:
|
|
bash /usr/scripts/crowdsec/cs_notifier.sh</pre>
|
|
</div>
|
|
<div class="script-row">
|
|
<div class="script-label">Run — cs_local_digest.sh (new OMV scheduled job)</div>
|
|
<pre>Every 4 hours (cron: 0 0,4,8,12,16,20 * * *):
|
|
bash /usr/scripts/crowdsec/cs_local_digest.sh</pre>
|
|
</div>
|
|
<div class="script-row">
|
|
<div class="script-label">Manual test</div>
|
|
<pre>sudo bash -c 'echo '"'"'{"value":"1.2.3.4","scenario":"crowdsecurity/http-probing","duration":"4h","type":"ban","scope":"Ip","origin":"crowdsec","start_at":"2026-06-09T14:00:00Z"}'"'"' >> /var/log/crowdsec-notifier/local_pending.json'
|
|
sudo bash /usr/scripts/crowdsec/cs_local_digest.sh
|
|
sudo tail -20 /var/log/crowdsec-notifier/notifier.log</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- =============================================
|
|
SECTION 6 — LESSONS LEARNED
|
|
Keep entries short — one lesson per row.
|
|
============================================= -->
|
|
<div class="section">
|
|
<div class="section-title" data-num="6">Lessons learned</div>
|
|
<table>
|
|
<thead>
|
|
<tr><th>Lesson</th><th>Detail</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>Batch high-frequency alerts</td>
|
|
<td>Per-event email alerting doesn't scale with attack frequency. A 4-hour batched digest preserves full visibility (every IP, scenario, type, duration, scope, and threat-intel links) while cutting email volume from hundreds/week to a handful/day.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Skip-if-empty for digests</td>
|
|
<td>cs_local_digest.sh exits silently with no email if local_pending.json is empty — avoids inbox clutter during quiet periods.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Snapshot-before-clear pattern</td>
|
|
<td>The pending log is only cleared (<code>> local_pending.json</code>) after a confirmed successful SMTP send. If the send fails, the data stays intact and is retried on the next 4-hour run.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>python3 - with a heredoc ignores piped stdin</td>
|
|
<td><code>echo "$DATA" | python3 - << EOF ... EOF</code> does NOT work as expected — the heredoc body becomes stdin and the pipe is discarded. Pass shell data into Python via inline variable interpolation inside the heredoc instead (<code>var = """$DATA"""</code>).</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Iterating on email templates</td>
|
|
<td>Several format iterations were needed to match the desired layout exactly. Keeping the rendered HTML output from a real test send was the fastest way to confirm/correct structure.</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
|
|
<!-- =============================================
|
|
SECTION 7 — NETWORK MAP / DOCS TO UPDATE (OPTIONAL)
|
|
List anything that changed and needs to be
|
|
reflected in your architecture map or docs.
|
|
============================================= -->
|
|
<div class="section">
|
|
<div class="section-title" data-num="7">Network map — items to update</div>
|
|
<table>
|
|
<thead>
|
|
<tr><th>Item</th><th>Old value</th><th>New value</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>cs_notifier.sh behavior</td>
|
|
<td><code>Sends immediate per-IP email on each local ban</code></td>
|
|
<td><code>Appends local bans to local_pending.json (no email)</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td>New scheduled job</td>
|
|
<td><code>(none)</code></td>
|
|
<td><code>cs_local_digest.sh every 4 hours — cron: 0 0,4,8,12,16,20 * * *</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td>New log file</td>
|
|
<td><code>(none)</code></td>
|
|
<td><code>/var/log/crowdsec-notifier/local_pending.json</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td>server_nas16.md</td>
|
|
<td><code>2-script architecture (cs_notifier.sh, cs_digest.sh)</code></td>
|
|
<td><code>3-script architecture — updated (cs_notifier.sh, cs_local_digest.sh, cs_digest.sh)</code></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
|
|
<!-- FOOTER — date and network name auto-filled by Claude -->
|
|
<div class="footer">
|
|
<span>KingDezigns Home Network</span>
|
|
<span>Generated 2026-06-09</span>
|
|
</div>
|
|
|
|
</div>
|
|
</body>
|
|
</html>
|