diff --git a/app/modules/speedtest/i18n/de.json b/app/modules/speedtest/i18n/de.json index 573d4d65..1a98cf2b 100644 --- a/app/modules/speedtest/i18n/de.json +++ b/app/modules/speedtest/i18n/de.json @@ -23,5 +23,7 @@ "run_speedtest": "Speedtest starten", "speedtest_running": "Läuft...", "speedtest_complete": "Speedtest abgeschlossen", - "speedtest_timeout": "Speedtest dauert länger als erwartet. Aktualisiere die Seite." + "speedtest_timeout": "Speedtest dauert länger als erwartet. Aktualisiere die Seite.", + "speedtest_empty_title": "Noch keine Speedtest-Ergebnisse", + "speedtest_empty_desc": "Automatische Tests erscheinen hier, sobald der Zeitplan läuft. Du kannst auch einen manuellen Test starten." } diff --git a/app/modules/speedtest/i18n/en.json b/app/modules/speedtest/i18n/en.json index 6496c928..258f14fb 100644 --- a/app/modules/speedtest/i18n/en.json +++ b/app/modules/speedtest/i18n/en.json @@ -23,5 +23,7 @@ "run_speedtest": "Run Speedtest", "speedtest_running": "Running...", "speedtest_complete": "Speedtest complete", - "speedtest_timeout": "Speedtest is taking longer than expected. Refresh to check." + "speedtest_timeout": "Speedtest is taking longer than expected. Refresh to check.", + "speedtest_empty_title": "No speedtest results yet", + "speedtest_empty_desc": "Automated tests will appear here once the schedule runs. You can also start a manual test." } diff --git a/app/modules/speedtest/i18n/es.json b/app/modules/speedtest/i18n/es.json index 03b59513..bc633df9 100644 --- a/app/modules/speedtest/i18n/es.json +++ b/app/modules/speedtest/i18n/es.json @@ -23,5 +23,7 @@ "run_speedtest": "Ejecutar Speedtest", "speedtest_running": "Ejecutando...", "speedtest_complete": "Speedtest completado", - "speedtest_timeout": "El speedtest está tardando más de lo esperado. Actualiza para comprobar." + "speedtest_timeout": "El speedtest está tardando más de lo esperado. Actualiza para comprobar.", + "speedtest_empty_title": "Aún no hay resultados de speedtest", + "speedtest_empty_desc": "Los tests automáticos aparecerán aquí cuando se ejecute la programación. También puedes iniciar un test manual." } diff --git a/app/modules/speedtest/i18n/fr.json b/app/modules/speedtest/i18n/fr.json index 8cfeba38..d370dad0 100644 --- a/app/modules/speedtest/i18n/fr.json +++ b/app/modules/speedtest/i18n/fr.json @@ -23,5 +23,7 @@ "run_speedtest": "Lancer le Speedtest", "speedtest_running": "En cours...", "speedtest_complete": "Speedtest terminé", - "speedtest_timeout": "Le speedtest prend plus de temps que prévu. Actualisez pour vérifier." + "speedtest_timeout": "Le speedtest prend plus de temps que prévu. Actualisez pour vérifier.", + "speedtest_empty_title": "Pas encore de résultats de speedtest", + "speedtest_empty_desc": "Les tests automatiques apparaîtront ici une fois le planning actif. Vous pouvez aussi lancer un test manuel." } diff --git a/app/modules/speedtest/i18n/template.json b/app/modules/speedtest/i18n/template.json index 7680676e..bd4a52cd 100644 --- a/app/modules/speedtest/i18n/template.json +++ b/app/modules/speedtest/i18n/template.json @@ -23,5 +23,7 @@ "run_speedtest": "", "speedtest_running": "", "speedtest_complete": "", - "speedtest_timeout": "" + "speedtest_timeout": "", + "speedtest_empty_title": "", + "speedtest_empty_desc": "" } diff --git a/app/modules/speedtest/static/style.css b/app/modules/speedtest/static/style.css index 83effba3..2f1957ec 100644 --- a/app/modules/speedtest/static/style.css +++ b/app/modules/speedtest/static/style.css @@ -43,11 +43,6 @@ margin-bottom: var(--space-xl, 24px); transition: all 0.2s ease; } -.speedtest-chart-card:hover { - border-color: var(--accent-purple, #a855f7); - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(168,85,247,0.15); -} .speedtest-chart-wrap { position: relative; width: 100%; @@ -133,6 +128,37 @@ align-items: center; } +/* Empty state */ +.speedtest-empty-state { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + padding: 48px 24px; +} +.speedtest-empty-title { + font-size: 1.1em; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 6px; +} +.speedtest-empty-desc { + font-size: 0.85em; + color: var(--text-secondary); + max-width: 360px; + line-height: 1.5; +} +.speedtest-empty-state.speedtest-empty-error .speedtest-empty-title, +.speedtest-empty-state.speedtest-empty-error .speedtest-empty-desc, +.speedtest-empty-state.speedtest-empty-error .btn, +.speedtest-empty-state.speedtest-empty-error [data-lucide] { + display: none; +} +.speedtest-empty-state.speedtest-empty-error::after { + content: attr(data-error); + color: var(--text-muted); +} + /* Responsive */ @media (max-width: 768px) { .speedtest-chart-card { diff --git a/app/static/css/main.css b/app/static/css/main.css index 6e539816..fe16dff2 100644 --- a/app/static/css/main.css +++ b/app/static/css/main.css @@ -742,6 +742,7 @@ body.is-offline .offline-banner { display: flex; } /* Value color utilities (used across channel, BNetzA, speedtest tables) */ .val-good { color: var(--good); } .val-tolerated { color: var(--tolerated); } +.val-bad { color: var(--crit); } .val-warn { color: var(--warn); } .val-crit { color: var(--crit); } /* Per-metric health coloring via data attributes (channel tables) */ diff --git a/app/static/js/speedtest.js b/app/static/js/speedtest.js index 6604d61e..a7b72236 100644 --- a/app/static/js/speedtest.js +++ b/app/static/js/speedtest.js @@ -5,6 +5,7 @@ var _speedtestAllData = []; var _speedtestVisible = 50; var _speedtestSortCol = 'timestamp'; var _speedtestSortDir = 'desc'; +var _signalCache = {}; function formatSpeedtestTimestamp(ts) { if (!ts) return ''; @@ -32,14 +33,15 @@ function loadSpeedtestHistory() { if (moreWrap) moreWrap.style.display = 'none'; _speedtestRawData = []; _speedtestAllData = []; + _signalCache = {}; _speedtestVisible = 50; fetch('/api/speedtest?count=2000') .then(function(r) { return r.json(); }) .then(function(data) { if (loading) loading.style.display = 'none'; if (!data || data.length === 0) { - noData.textContent = T.speedtest_no_data || 'No speedtest data.'; - noData.style.display = 'block'; + noData.classList.remove('speedtest-empty-error'); + noData.style.display = ''; return; } _speedtestRawData = data; @@ -47,8 +49,9 @@ function loadSpeedtestHistory() { }) .catch(function() { if (loading) loading.style.display = 'none'; - noData.textContent = T.network_error || 'Error'; - noData.style.display = 'block'; + noData.classList.add('speedtest-empty-error'); + noData.setAttribute('data-error', T.network_error || 'Error'); + noData.style.display = ''; }); } @@ -69,8 +72,8 @@ function filterSpeedtestData() { if (_speedtestAllData.length === 0) { if (table) table.style.display = 'none'; if (noData) { - noData.textContent = T.speedtest_no_data || 'No speedtest data.'; - noData.style.display = 'block'; + noData.classList.remove('speedtest-empty-error'); + noData.style.display = ''; } var cc = document.getElementById('speedtest-chart-container'); if (cc) cc.style.display = 'none'; @@ -186,6 +189,76 @@ function renderSpeedtestRows() { if (typeof lucide !== 'undefined') lucide.createIcons(); } +function _renderSignalDetail(data, container) { + container.textContent = ''; + if (!data.found) { + var noDataSpan = document.createElement('span'); + noDataSpan.className = 'st-sig-no-data'; + noDataSpan.textContent = data.message || T.signal_no_snapshot; + container.appendChild(noDataSpan); + return; + } + var healthClass = 'health-' + (data.health || 'unknown'); + var healthLabels = {good: T.health_good || 'Good', tolerated: T.health_tolerated || 'Tolerated', marginal: T.health_marginal || 'Marginal', critical: T.health_critical || 'Critical'}; + var healthLabel = healthLabels[data.health] || data.health; + var items = [ + {label: T.signal_health || 'Health', value: healthLabel, badge: healthClass}, + {label: T.signal_ds_power || 'DS Power', value: data.ds_power_min + ' / ' + data.ds_power_avg + ' / ' + data.ds_power_max + ' dBmV'}, + {label: T.signal_ds_snr || 'DS SNR', value: data.ds_snr_min + ' / ' + data.ds_snr_avg + ' dB'}, + {label: T.signal_us_power || 'US Power', value: data.us_power_min + ' / ' + data.us_power_avg + ' / ' + data.us_power_max + ' dBmV'}, + {label: T.signal_errors || 'Errors', value: (data.ds_correctable_errors || 0).toLocaleString() + ' ' + (T.signal_corr || 'corr.') + ' / ' + (data.ds_uncorrectable_errors || 0).toLocaleString() + ' ' + (T.signal_uncorr || 'uncorr.')}, + {label: (T.signal_ds_channels || 'DS') + ' / ' + (T.signal_us_channels || 'US'), value: (data.ds_total || 0) + ' / ' + (data.us_total || 0)} + ]; + items.forEach(function(item) { + var div = document.createElement('div'); + div.className = 'st-sig-item'; + var lbl = document.createElement('span'); + lbl.className = 'st-sig-label'; + lbl.textContent = item.label; + div.appendChild(lbl); + if (item.badge) { + var badge = document.createElement('span'); + badge.className = 'st-health-badge ' + item.badge; + badge.textContent = item.value; + div.appendChild(badge); + } else { + var val = document.createElement('span'); + val.className = 'st-sig-value'; + val.textContent = item.value; + div.appendChild(val); + } + container.appendChild(div); + }); + if (data.us_channels && data.us_channels.length > 0) { + var modsDiv = document.createElement('div'); + modsDiv.className = 'st-us-mods'; + var modsLabel = document.createElement('span'); + modsLabel.className = 'st-sig-label'; + modsLabel.textContent = (T.signal_us_modulation || 'US Modulation') + ': '; + modsDiv.appendChild(modsLabel); + for (var c = 0; c < data.us_channels.length; c++) { + var ch = data.us_channels[c]; + var chSpan = document.createElement('span'); + chSpan.textContent = 'Ch' + (ch.channel_id || c) + ': ' + (ch.modulation || '?'); + modsDiv.appendChild(chSpan); + } + container.appendChild(modsDiv); + } + var snapDiv = document.createElement('div'); + snapDiv.className = 'st-sig-item'; + var snapLabel = document.createElement('span'); + snapLabel.className = 'st-sig-label'; + snapLabel.textContent = T.signal_snapshot_time || 'Snapshot'; + snapDiv.appendChild(snapLabel); + var snapVal = document.createElement('span'); + snapVal.className = 'st-sig-value'; + snapVal.style.fontSize = '0.85em'; + snapVal.style.color = 'var(--muted)'; + snapVal.textContent = data.snapshot_timestamp || ''; + snapDiv.appendChild(snapVal); + container.appendChild(snapDiv); +} + function toggleSpeedtestSignal(btn) { var id = btn.getAttribute('data-id'); var parentRow = btn.closest('tr'); @@ -196,55 +269,42 @@ function toggleSpeedtestSignal(btn) { btn.classList.remove('open'); return; } - // Create detail row and fetch data + // Create detail row and populate (from cache or fetch) btn.classList.add('open'); var newRow = document.createElement('tr'); newRow.className = 'st-signal-row'; var cols = parentRow.children.length; var td = document.createElement('td'); td.colSpan = cols; - td.innerHTML = '