Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,8 @@
"signal_ds_channels": "DS Kanäle",
"signal_us_channels": "US Kanäle",
"signal_snapshot_time": "Snapshot",
"channel_onboarding_title": "Kanal-Verlauf",
"channel_onboarding_desc": "Waehle einen Kanal aus dem Dropdown um den Signalverlauf zu sehen.",
"channel_timeline": "Kanalverlauf",
"select_channel": "Kanal wählen",
"downstream_channels": "Downstream Kanäle",
Expand Down
2 changes: 2 additions & 0 deletions app/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,8 @@
"signal_ds_channels": "DS Channels",
"signal_us_channels": "US Channels",
"signal_snapshot_time": "Snapshot",
"channel_onboarding_title": "Channel Timeline",
"channel_onboarding_desc": "Select a channel from the dropdown to view its signal history over time.",
"channel_timeline": "Channel Timeline",
"select_channel": "Select Channel",
"downstream_channels": "Downstream Channels",
Expand Down
2 changes: 2 additions & 0 deletions app/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,8 @@
"signal_ds_channels": "Canales DS",
"signal_us_channels": "Canales US",
"signal_snapshot_time": "Snapshot",
"channel_onboarding_title": "Historial del canal",
"channel_onboarding_desc": "Selecciona un canal del menu para ver su historial de senal.",
"channel_timeline": "Historial de canal",
"select_channel": "Seleccionar canal",
"downstream_channels": "Canales descendentes",
Expand Down
2 changes: 2 additions & 0 deletions app/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,8 @@
"signal_ds_channels": "Canaux DS",
"signal_us_channels": "Canaux US",
"signal_snapshot_time": "Snapshot",
"channel_onboarding_title": "Historique du canal",
"channel_onboarding_desc": "Selectionnez un canal dans la liste pour voir son historique.",
"channel_timeline": "Historique du canal",
"select_channel": "Choisir un canal",
"downstream_channels": "Canaux descendants",
Expand Down
2 changes: 2 additions & 0 deletions app/i18n/template.json
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,8 @@
"signal_ds_channels": "",
"signal_us_channels": "",
"signal_snapshot_time": "",
"channel_onboarding_title": "",
"channel_onboarding_desc": "",
"channel_timeline": "",
"select_channel": "",
"downstream_channels": "",
Expand Down
82 changes: 82 additions & 0 deletions app/static/css/views.css
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,88 @@
.btn-ghost:hover { color: var(--text); }


/* ── Channel View Controls ── */
.ch-controls {
display: flex;
gap: 12px;
margin-bottom: var(--space-lg, 20px);
flex-wrap: wrap;
align-items: center;
}

/* Channel info bar */
.ch-info-bar {
display: flex;
gap: var(--space-md, 16px);
padding: var(--space-sm, 8px) var(--space-md, 16px);
margin-bottom: var(--space-md, 16px);
background: var(--tint-subtle, rgba(255,255,255,0.02));
border: 1px solid var(--glass-border, var(--card-border));
border-radius: var(--radius-sm, 8px);
font-size: 0.82em;
flex-wrap: wrap;
align-items: center;
}
.ch-info-item {
display: flex;
align-items: center;
gap: 6px;
color: var(--text-secondary);
}
.ch-info-item strong {
color: var(--text-primary);
}
.ch-info-sep {
width: 1px;
height: 16px;
background: var(--glass-border);
flex-shrink: 0;
}
.ch-info-health {
display: inline-flex;
align-items: center;
gap: 4px;
font-weight: 600;
}
.ch-info-health.good { color: var(--good); }
.ch-info-health.tolerated { color: var(--tolerated, var(--warn)); }
.ch-info-health.marginal, .ch-info-health.warn, .ch-info-health.warning { color: var(--warn); }
.ch-info-health.critical, .ch-info-health.crit { color: var(--crit); }

/* Channel onboarding (empty state) */
.ch-onboarding {
display: flex;
align-items: center;
gap: var(--space-md, 16px);
padding: var(--space-xl, 32px);
background: var(--tint-subtle, rgba(255,255,255,0.02));
border: 1px dashed var(--glass-border);
border-radius: var(--radius-md, 12px);
color: var(--text-secondary);
}
.ch-onboarding > i, .ch-onboarding > svg {
width: 32px; height: 32px;
color: var(--text-muted);
flex-shrink: 0;
}
.ch-onboarding-text {
display: flex;
flex-direction: column;
gap: 4px;
}
.ch-onboarding-text strong {
color: var(--text-primary);
font-size: 0.95em;
}
.ch-onboarding-text span {
font-size: 0.85em;
}

/* Full-width chart card (modulation) */
.ch-full-width {
grid-column: 1 / -1;
}

/* ── Channel Compare ── */
.compare-controls {
display: flex;
Expand Down
92 changes: 84 additions & 8 deletions app/static/js/channels.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,13 @@ function switchChannelMode() {
var comparePanel = document.getElementById('channel-panel-compare');
var timelineControls = document.getElementById('channel-timeline-controls');
var compareControls = document.getElementById('channel-compare-controls');
var infoBar = document.getElementById('channel-info-bar');
if (mode === 'compare') {
timelinePanel.style.display = 'none';
comparePanel.style.display = '';
if (timelineControls) timelineControls.style.display = 'none';
if (compareControls) compareControls.style.display = 'contents';
if (infoBar) infoBar.style.display = 'none';
loadCompareChannelList();
if (_compareChannels.length === 0) {
var emptyEl = document.getElementById('compare-empty');
Expand All @@ -168,9 +170,12 @@ function switchChannelMode() {
if (compareControls) compareControls.style.display = 'none';
var sel = document.getElementById('channel-select');
if (!sel || !sel.value) {
var chEmpty = document.getElementById('channel-empty');
chEmpty.textContent = T.channel_select_prompt || 'Select a channel above to view its signal history.';
chEmpty.style.display = '';
document.getElementById('channel-empty').style.display = '';
document.getElementById('channel-no-data').style.display = 'none';
} else {
// Restore info bar for already-selected channel
var cparts = sel.value.split('-');
_updateChannelInfoBar(cparts[0], cparts[1]);
}
}
writeChannelHash();
Expand Down Expand Up @@ -213,22 +218,91 @@ function loadChannelList(callback) {
});
sel.appendChild(grp2);
}
_cachedChannelData = data;
_channelsLoaded = true;
if (callback) callback();
})
.catch(function() { if (callback) callback(); });
}

function _makeInfoItem(text, bold) {
var el = document.createElement('span');
el.className = 'ch-info-item';
if (bold) {
var b = document.createElement('strong');
b.textContent = text;
el.appendChild(b);
} else {
el.textContent = text;
}
return el;
}
function _makeInfoItemWithLabel(label, value, unit) {
var el = document.createElement('span');
el.className = 'ch-info-item';
el.textContent = label + ' ';
var b = document.createElement('strong');
b.textContent = value;
el.appendChild(b);
if (unit) el.appendChild(document.createTextNode(' ' + unit));
return el;
}
function _makeInfoSep() {
var el = document.createElement('span');
el.className = 'ch-info-sep';
return el;
}

function _updateChannelInfoBar(direction, channelId) {
var bar = document.getElementById('channel-info-bar');
if (!bar) return;
if (!_cachedChannelData) { bar.style.display = 'none'; return; }
var channels = direction === 'ds'
? (_cachedChannelData.ds_channels || [])
: (_cachedChannelData.us_channels || []);
var ch = null;
for (var i = 0; i < channels.length; i++) {
if (String(channels[i].channel_id) === String(channelId)) { ch = channels[i]; break; }
}
if (!ch) { bar.style.display = 'none'; return; }
while (bar.firstChild) bar.removeChild(bar.firstChild);
var dir = direction.toUpperCase();
var health = ch.health || 'unknown';
var healthLabel = T['health_' + health] || health;

bar.appendChild(_makeInfoItem(dir + ' ' + channelId, true));
bar.appendChild(_makeInfoSep());
if (ch.frequency) {
var freqStr = String(ch.frequency);
bar.appendChild(_makeInfoItem(freqStr.indexOf('MHz') === -1 ? freqStr + ' MHz' : freqStr));
}
bar.appendChild(_makeInfoItem('DOCSIS ' + (ch.docsis_version || '3.0')));
bar.appendChild(_makeInfoSep());
if (ch.power != null) bar.appendChild(_makeInfoItemWithLabel('Power', ch.power, 'dBmV'));
if (ch.snr != null) bar.appendChild(_makeInfoItemWithLabel('SNR', ch.snr, 'dB'));
bar.appendChild(_makeInfoSep());
var healthEl = document.createElement('span');
healthEl.className = 'ch-info-health ' + health;
healthEl.textContent = healthLabel;
bar.appendChild(healthEl);
bar.style.display = '';
}

var _cachedChannelData = null;

function loadChannelTimeline() {
var sel = document.getElementById('channel-select');
var val = sel.value;
var chartsEl = document.getElementById('channel-charts');
var emptyEl = document.getElementById('channel-empty');
var noDataEl = document.getElementById('channel-no-data');
var loadingEl = document.getElementById('channel-loading');
var infoBar = document.getElementById('channel-info-bar');
if (!val) {
chartsEl.style.display = 'none';
noDataEl.style.display = 'none';
loadingEl.style.display = 'none';
emptyEl.textContent = T.channel_select_prompt || 'Select a channel above to view its signal history.';
if (infoBar) infoBar.style.display = 'none';
emptyEl.style.display = '';
writeChannelHash();
return;
Expand All @@ -244,14 +318,16 @@ function loadChannelTimeline() {
loadingEl.style.display = '';
chartsEl.style.display = 'none';
emptyEl.style.display = 'none';
noDataEl.style.display = 'none';
_updateChannelInfoBar(direction, channelId);

fetch('/api/channel-history?channel_id=' + channelId + '&direction=' + direction + '&days=' + days)
.then(function(r) { return r.json(); })
.then(function(data) {
loadingEl.style.display = 'none';
if (!data || data.length === 0) {
emptyEl.textContent = T.no_channel_data || 'No data available for this channel.';
emptyEl.style.display = '';
noDataEl.textContent = T.no_channel_data || 'No data available for this channel.';
noDataEl.style.display = '';
return;
}
chartsEl.style.display = '';
Expand Down Expand Up @@ -321,8 +397,8 @@ function loadChannelTimeline() {
})
.catch(function() {
loadingEl.style.display = 'none';
emptyEl.textContent = T.trend_error || 'Error loading data.';
emptyEl.style.display = '';
noDataEl.textContent = T.trend_error || 'Error loading data.';
noDataEl.style.display = '';
});
}
window.loadChannelTimeline = loadChannelTimeline;
Expand Down
19 changes: 13 additions & 6 deletions app/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1338,10 +1338,7 @@ <h2 id="bqm-import-modal-title">{{ t.bqm_import_title }}</h2>

<!-- ═══ View: Channel Timeline ═══ -->
<div id="view-channels" class="view">
<div class="trend-header" style="margin-bottom:12px;">
<span class="trend-title">{{ t.channels }}</span>
</div>
<div style="display:flex; gap:12px; margin-bottom:20px; flex-wrap:wrap; align-items:center;">
<div class="ch-controls">
<div class="trend-tabs" id="channel-mode-tabs">
<button class="trend-tab active" data-value="timeline" onclick="selectPill(this, switchChannelMode)">{{ t.channel_timeline }}</button>
<button class="trend-tab" data-value="compare" onclick="selectPill(this, switchChannelMode)">{{ t.channel_compare }}</button>
Expand Down Expand Up @@ -1375,12 +1372,22 @@ <h2 id="bqm-import-modal-title">{{ t.bqm_import_title }}</h2>
</span>
</div>

<!-- Channel info bar (populated by JS after channel selection) -->
<div id="channel-info-bar" class="ch-info-bar" style="display:none;"></div>

<!-- ── Timeline Sub-View ── -->
<div id="channel-panel-timeline">
<div id="channel-loading" class="waiting" style="display:none;">
<div class="skeleton skel-chart"></div>
</div>
<div id="channel-empty" class="events-empty" style="display:none;"></div>
<div id="channel-empty" class="ch-onboarding" style="display:none;">
<i data-lucide="layers"></i>
<div class="ch-onboarding-text">
<strong>{{ t.get('channel_onboarding_title', 'Channel Timeline') }}</strong>
<span>{{ t.get('channel_onboarding_desc', 'Select a channel from the dropdown to view its signal history over time.') }}</span>
</div>
</div>
<div id="channel-no-data" class="no-data-msg" style="display:none;"></div>
<div id="channel-charts" class="charts-grid" style="display:none;">
<div class="chart-card">
<div class="chart-card-header">
Expand Down Expand Up @@ -1408,7 +1415,7 @@ <h2 id="bqm-import-modal-title">{{ t.bqm_import_title }}</h2>
</div>
<canvas id="chart-ch-errors"></canvas>
</div>
<div class="chart-card" id="channel-modulation-card">
<div class="chart-card ch-full-width" id="channel-modulation-card">
<div class="chart-card-header">
<div class="chart-header-content">
<svg class="chart-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
Expand Down