Skip to content

Commit 4cf3fba

Browse files
itsDNNSclaude
andcommitted
Add LLM export feature for AI-powered connection analysis
New export button in sidebar opens a modal with a structured markdown report that users can paste into ChatGPT, Claude, Gemini etc. for DOCSIS insights. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ef2ce89 commit 4cf3fba

File tree

4 files changed

+209
-0
lines changed

4 files changed

+209
-0
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
44

55
Versioning: `YYYY-MM-DD.N` (date + sequential build number per day)
66

7+
## [2026-02-09.6]
8+
9+
### Added
10+
- **LLM Export**: Generate structured markdown report for AI analysis (ChatGPT, Claude, Gemini, etc.)
11+
- **Export modal**: Copy-to-clipboard dialog accessible from sidebar menu
12+
- **API endpoint**: `/api/export` returns full DOCSIS status report with context and reference values
13+
714
## [2026-02-09.5]
815

916
### Added

app/i18n.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,15 @@
118118

119119
# Language
120120
"language": "Language",
121+
122+
# Export
123+
"export_llm": "Export for LLM",
124+
"export_title": "LLM Analysis Export",
125+
"export_hint": "Copy this text and paste it into your favorite LLM (ChatGPT, Claude, Gemini, ...) for connection insights.",
126+
"copy_clipboard": "Copy to Clipboard",
127+
"copied": "Copied!",
128+
"close": "Close",
129+
"export_no_data": "No data available yet. Wait for the first poll.",
121130
}
122131

123132
DE = {
@@ -224,6 +233,14 @@
224233
"saved_ph": "Gespeichert",
225234

226235
"language": "Sprache",
236+
237+
"export_llm": "Export fuer LLM",
238+
"export_title": "LLM-Analyse Export",
239+
"export_hint": "Kopiere diesen Text und fuege ihn in dein bevorzugtes LLM (ChatGPT, Claude, Gemini, ...) ein, um Insights zu deiner Verbindung zu erhalten.",
240+
"copy_clipboard": "In Zwischenablage kopieren",
241+
"copied": "Kopiert!",
242+
"close": "Schliessen",
243+
"export_no_data": "Noch keine Daten vorhanden. Warte auf die erste Abfrage.",
227244
}
228245

229246
_TRANSLATIONS = {"en": EN, "de": DE}

app/templates/index.html

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,53 @@
261261
color: var(--muted); font-size: 0.95em;
262262
}
263263

264+
/* ── Export Modal ── */
265+
.modal-overlay {
266+
position: fixed; inset: 0; background: var(--overlay);
267+
z-index: 200; display: none; justify-content: center; align-items: center;
268+
}
269+
.modal-overlay.open { display: flex; }
270+
.modal {
271+
background: var(--surface); border-radius: 10px;
272+
padding: 20px; max-width: 720px; width: 90%; max-height: 85vh;
273+
display: flex; flex-direction: column;
274+
border: 1px solid var(--input-border);
275+
box-shadow: 0 12px 40px rgba(0,0,0,0.4);
276+
}
277+
.modal-header {
278+
display: flex; justify-content: space-between; align-items: center;
279+
margin-bottom: 8px;
280+
}
281+
.modal-header h2 { font-size: 1.1em; color: var(--accent); margin: 0; }
282+
.modal-close {
283+
background: none; border: none; color: var(--muted);
284+
font-size: 1.4em; cursor: pointer; line-height: 1;
285+
}
286+
.modal-close:hover { color: var(--text); }
287+
.modal-hint { font-size: 0.85em; color: var(--muted); margin-bottom: 12px; }
288+
.modal-body {
289+
flex: 1; overflow-y: auto; margin-bottom: 12px;
290+
}
291+
.modal-body textarea {
292+
width: 100%; min-height: 300px; background: var(--bg);
293+
color: var(--text); border: 1px solid var(--input-border);
294+
border-radius: 6px; padding: 12px; font-size: 0.82em;
295+
font-family: 'Consolas', 'Monaco', monospace; resize: vertical;
296+
}
297+
.modal-footer { display: flex; gap: 10px; justify-content: flex-end; }
298+
.modal-footer .btn {
299+
display: inline-block; padding: 8px 20px; border: none;
300+
border-radius: 4px; cursor: pointer; font-size: 0.9em;
301+
font-family: inherit; font-weight: bold;
302+
}
303+
.btn-accent { background: var(--accent); color: #000; }
304+
.btn-accent:hover { filter: brightness(1.15); }
305+
.btn-muted {
306+
background: var(--card); color: var(--text);
307+
border: 1px solid var(--input-border);
308+
}
309+
.btn-muted:hover { background: var(--accent); color: #000; }
310+
264311
/* ── Responsive ── */
265312
@media (max-width: 900px) { .charts-grid { grid-template-columns: 1fr; } }
266313
@media (max-width: 600px) {
@@ -302,6 +349,9 @@
302349
<a class="sidebar-link" href="/settings">
303350
<span class="icon">&#9881;</span> {{ t.settings }}
304351
</a>
352+
<a class="sidebar-link" id="export-link" onclick="exportForLLM()">
353+
<span class="icon">&#128196;</span> {{ t.export_llm }}
354+
</a>
305355
<div class="sidebar-divider"></div>
306356
<details class="sidebar-ref">
307357
<summary>{{ t.reference_values }}</summary>
@@ -504,6 +554,24 @@ <h2 class="section-title">{{ t.upstream }} ({{ us|length }} {{ t.channels }})</h
504554
</div>
505555
</div><!-- /main-content -->
506556

557+
<!-- Export Modal -->
558+
<div class="modal-overlay" id="export-modal">
559+
<div class="modal">
560+
<div class="modal-header">
561+
<h2>{{ t.export_title }}</h2>
562+
<button class="modal-close" onclick="closeExportModal()">&times;</button>
563+
</div>
564+
<p class="modal-hint">{{ t.export_hint }}</p>
565+
<div class="modal-body">
566+
<textarea id="export-text" readonly></textarea>
567+
</div>
568+
<div class="modal-footer">
569+
<button class="btn btn-muted" onclick="closeExportModal()">{{ t.close }}</button>
570+
<button class="btn btn-accent" id="export-copy-btn" onclick="copyExport()">{{ t.copy_clipboard }}</button>
571+
</div>
572+
</div>
573+
</div>
574+
507575
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
508576
<script>
509577
(function() {
@@ -829,6 +897,44 @@ <h2 class="section-title">{{ t.upstream }} ({{ us|length }} {{ t.channels }})</h
829897
/* ── Init ── */
830898
updateDateLabel();
831899
})();
900+
901+
/* ── Export for LLM ── */
902+
function exportForLLM() {
903+
var T = {{ t|tojson }};
904+
var modal = document.getElementById('export-modal');
905+
var textarea = document.getElementById('export-text');
906+
var sidebar = document.getElementById('sidebar');
907+
var overlay = document.getElementById('sidebar-overlay');
908+
sidebar.classList.remove('open');
909+
overlay.classList.remove('open');
910+
textarea.value = T.export_no_data;
911+
modal.classList.add('open');
912+
fetch('/api/export')
913+
.then(function(r) { return r.json(); })
914+
.then(function(data) {
915+
if (data.text) {
916+
textarea.value = data.text;
917+
} else if (data.error) {
918+
textarea.value = data.error;
919+
}
920+
})
921+
.catch(function() {
922+
textarea.value = 'Error loading export data.';
923+
});
924+
}
925+
function closeExportModal() {
926+
document.getElementById('export-modal').classList.remove('open');
927+
}
928+
function copyExport() {
929+
var T = {{ t|tojson }};
930+
var textarea = document.getElementById('export-text');
931+
var btn = document.getElementById('export-copy-btn');
932+
textarea.select();
933+
navigator.clipboard.writeText(textarea.value).then(function() {
934+
btn.textContent = T.copied;
935+
setTimeout(function() { btn.textContent = T.copy_clipboard; }, 2000);
936+
});
937+
}
832938
</script>
833939

834940
</body>

app/web.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,85 @@ def api_trends():
254254
return jsonify({"error": "Invalid range (use day, week, month)"}), 400
255255

256256

257+
@app.route("/api/export")
258+
def api_export():
259+
"""Generate a structured markdown report for LLM analysis."""
260+
analysis = _state.get("analysis")
261+
if not analysis:
262+
return jsonify({"error": "No data available"}), 404
263+
264+
s = analysis["summary"]
265+
ds = analysis["ds_channels"]
266+
us = analysis["us_channels"]
267+
ts = _state.get("last_update", "unknown")
268+
269+
lines = [
270+
"# DOCSIS Cable Connection – Status Report",
271+
"",
272+
"## Context",
273+
"This is a status report from a DOCSIS cable modem (FritzBox Cable).",
274+
"DOCSIS (Data Over Cable Service Interface Specification) is the standard for internet over coaxial cable.",
275+
"Analyze this data and provide insights about connection health, problematic channels, and recommendations.",
276+
"",
277+
"## Overview",
278+
f"- **Health**: {s.get('health', 'Unknown')}",
279+
f"- **Details**: {s.get('health_details', '')}",
280+
f"- **Timestamp**: {ts}",
281+
"",
282+
"## Summary",
283+
"| Metric | Value |",
284+
"|--------|-------|",
285+
f"| Downstream Channels | {s.get('ds_total', 0)} |",
286+
f"| DS Power (Min/Avg/Max) | {s.get('ds_power_min')} / {s.get('ds_power_avg')} / {s.get('ds_power_max')} dBmV |",
287+
f"| DS SNR (Min/Avg) | {s.get('ds_snr_min')} / {s.get('ds_snr_avg')} dB |",
288+
f"| DS Correctable Errors | {s.get('ds_correctable_errors', 0):,} |",
289+
f"| DS Uncorrectable Errors | {s.get('ds_uncorrectable_errors', 0):,} |",
290+
f"| Upstream Channels | {s.get('us_total', 0)} |",
291+
f"| US Power (Min/Avg/Max) | {s.get('us_power_min')} / {s.get('us_power_avg')} / {s.get('us_power_max')} dBmV |",
292+
"",
293+
"## Downstream Channels",
294+
"| Ch | Frequency | Power (dBmV) | SNR (dB) | Modulation | Corr. Errors | Uncorr. Errors | DOCSIS | Health |",
295+
"|----|-----------|-------------|----------|------------|-------------|---------------|--------|--------|",
296+
]
297+
for ch in ds:
298+
lines.append(
299+
f"| {ch.get('channel_id','')} | {ch.get('frequency','')} | {ch.get('power','')} "
300+
f"| {ch.get('snr', '-')} | {ch.get('modulation','')} "
301+
f"| {ch.get('correctable_errors', 0):,} | {ch.get('uncorrectable_errors', 0):,} "
302+
f"| {ch.get('docsis_version','')} | {ch.get('health','')} |"
303+
)
304+
lines += [
305+
"",
306+
"## Upstream Channels",
307+
"| Ch | Frequency | Power (dBmV) | Modulation | Multiplex | DOCSIS | Health |",
308+
"|----|-----------|-------------|------------|-----------|--------|--------|",
309+
]
310+
for ch in us:
311+
lines.append(
312+
f"| {ch.get('channel_id','')} | {ch.get('frequency','')} | {ch.get('power','')} "
313+
f"| {ch.get('modulation','')} | {ch.get('multiplex','')} "
314+
f"| {ch.get('docsis_version','')} | {ch.get('health','')} |"
315+
)
316+
lines += [
317+
"",
318+
"## Reference Values",
319+
"| Metric | Good | Marginal | Poor |",
320+
"|--------|------|----------|------|",
321+
"| DS Power | -7 to +7 dBmV | +/-7 to +/-10 | > +/-10 dBmV |",
322+
"| US Power | 35 to 49 dBmV | 50 to 54 | > 54 dBmV |",
323+
"| SNR/MER | > 30 dB | 25 to 30 | < 25 dB |",
324+
"| Uncorr. Errors | low | - | > 10,000 |",
325+
"",
326+
"## Questions",
327+
"Please analyze this data and provide:",
328+
"1. Overall connection health assessment",
329+
"2. Channels that need attention (with reasons)",
330+
"3. Error rate analysis and whether it indicates a problem",
331+
"4. Specific recommendations to improve connection quality",
332+
]
333+
return jsonify({"text": "\n".join(lines)})
334+
335+
257336
@app.route("/api/snapshots")
258337
def api_snapshots():
259338
"""Return list of available snapshot timestamps."""

0 commit comments

Comments
 (0)