Feat: add SLM live monitoring improvements #60
@@ -94,6 +94,7 @@ async def get_slm_units(
|
|||||||
# Legacy default from the roster field; refined from SLMM's cached status below.
|
# Legacy default from the roster field; refined from SLMM's cached status below.
|
||||||
unit.is_recent = bool(unit.slm_last_check and unit.slm_last_check > one_hour_ago)
|
unit.is_recent = bool(unit.slm_last_check and unit.slm_last_check > one_hour_ago)
|
||||||
unit.measurement_state = None
|
unit.measurement_state = None
|
||||||
|
unit.cache_last_seen = None # SLMM cache last_seen (real monitoring freshness)
|
||||||
|
|
||||||
if include_measurement:
|
if include_measurement:
|
||||||
# SLMM's /roster carries each unit's CACHED status (last_seen,
|
# SLMM's /roster carries each unit's CACHED status (last_seen,
|
||||||
@@ -124,6 +125,7 @@ async def get_slm_units(
|
|||||||
try:
|
try:
|
||||||
ls = datetime.fromisoformat(last_seen.replace("Z", ""))
|
ls = datetime.fromisoformat(last_seen.replace("Z", ""))
|
||||||
unit.is_recent = ls > recent_cutoff
|
unit.is_recent = ls > recent_cutoff
|
||||||
|
unit.cache_last_seen = ls # the real freshness the monitor updates
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
<span class="px-2 py-0.5 text-xs font-medium bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300 rounded-full">Retired</span>
|
<span class="px-2 py-0.5 text-xs font-medium bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300 rounded-full">Retired</span>
|
||||||
{% elif not unit.deployed %}
|
{% elif not unit.deployed %}
|
||||||
<span class="px-2 py-0.5 text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300 rounded-full">Benched</span>
|
<span class="px-2 py-0.5 text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300 rounded-full">Benched</span>
|
||||||
{% elif unit.measurement_state == "Start" %}
|
{% elif unit.measurement_state in ["Start", "Measure"] %}
|
||||||
<span class="px-2 py-0.5 text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 rounded-full">Measuring</span>
|
<span class="px-2 py-0.5 text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 rounded-full">Measuring</span>
|
||||||
{% elif unit.is_recent %}
|
{% elif unit.is_recent %}
|
||||||
<span class="px-2 py-0.5 text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 rounded-full">Active</span>
|
<span class="px-2 py-0.5 text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 rounded-full">Active</span>
|
||||||
@@ -57,7 +57,9 @@
|
|||||||
<span class="px-2 py-0.5 text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300 rounded-full">Idle</span>
|
<span class="px-2 py-0.5 text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300 rounded-full">Idle</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
{% if unit.slm_last_check %}
|
{% if unit.cache_last_seen %}
|
||||||
|
Last check: {{ unit.cache_last_seen|local_datetime }}
|
||||||
|
{% elif unit.slm_last_check %}
|
||||||
Last check: {{ unit.slm_last_check|local_datetime }}
|
Last check: {{ unit.slm_last_check|local_datetime }}
|
||||||
{% else %}
|
{% else %}
|
||||||
No recent check-in
|
No recent check-in
|
||||||
|
|||||||
@@ -168,9 +168,18 @@ window.selectedUnitId = null;
|
|||||||
window.dashboardChartData = {
|
window.dashboardChartData = {
|
||||||
timestamps: [],
|
timestamps: [],
|
||||||
lp: [],
|
lp: [],
|
||||||
leq: []
|
leq: [],
|
||||||
|
ln1: [],
|
||||||
|
ln2: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Parse a metric to a number, or null (so a missing/"-.-" percentile leaves a gap
|
||||||
|
// in the line instead of dropping it to 0).
|
||||||
|
function numOrNull(v) {
|
||||||
|
const f = parseFloat(v);
|
||||||
|
return isNaN(f) ? null : f;
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize Chart.js
|
// Initialize Chart.js
|
||||||
function initializeDashboardChart() {
|
function initializeDashboardChart() {
|
||||||
if (typeof Chart === 'undefined') {
|
if (typeof Chart === 'undefined') {
|
||||||
@@ -212,6 +221,26 @@ function initializeDashboardChart() {
|
|||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
pointRadius: 0
|
pointRadius: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'L1',
|
||||||
|
data: [],
|
||||||
|
borderColor: 'rgb(168, 85, 247)',
|
||||||
|
backgroundColor: 'rgba(168, 85, 247, 0.1)',
|
||||||
|
tension: 0.3,
|
||||||
|
borderWidth: 2,
|
||||||
|
pointRadius: 0,
|
||||||
|
spanGaps: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'L10',
|
||||||
|
data: [],
|
||||||
|
borderColor: 'rgb(249, 115, 22)',
|
||||||
|
backgroundColor: 'rgba(249, 115, 22, 0.1)',
|
||||||
|
tension: 0.3,
|
||||||
|
borderWidth: 2,
|
||||||
|
pointRadius: 0,
|
||||||
|
spanGaps: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -263,11 +292,10 @@ function showLiveChart(unitId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reset data for the newly-selected unit (clears any prior unit's line)
|
// Reset data for the newly-selected unit (clears any prior unit's line)
|
||||||
window.dashboardChartData = { timestamps: [], lp: [], leq: [] };
|
window.dashboardChartData = { timestamps: [], lp: [], leq: [], ln1: [], ln2: [] };
|
||||||
if (window.dashboardChart) {
|
if (window.dashboardChart) {
|
||||||
window.dashboardChart.data.labels = [];
|
window.dashboardChart.data.labels = [];
|
||||||
window.dashboardChart.data.datasets[0].data = [];
|
window.dashboardChart.data.datasets.forEach(ds => ds.data = []);
|
||||||
window.dashboardChart.data.datasets[1].data = [];
|
|
||||||
window.dashboardChart.update('none');
|
window.dashboardChart.update('none');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,22 +394,30 @@ function updateDashboardMetrics(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateDashboardChart(data) {
|
function updateDashboardChart(data) {
|
||||||
|
const cd = window.dashboardChartData;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
window.dashboardChartData.timestamps.push(now.toLocaleTimeString());
|
cd.timestamps.push(now.toLocaleTimeString());
|
||||||
window.dashboardChartData.lp.push(parseFloat(data.lp || 0));
|
cd.lp.push(numOrNull(data.lp));
|
||||||
window.dashboardChartData.leq.push(parseFloat(data.leq || 0));
|
cd.leq.push(numOrNull(data.leq));
|
||||||
|
// /monitor (DOD) frames carry ln1/ln2; a DRD frame would omit them -> null gap.
|
||||||
|
cd.ln1.push(numOrNull(data.ln1));
|
||||||
|
cd.ln2.push(numOrNull(data.ln2));
|
||||||
|
|
||||||
// Keep a generous window (backfill seeds up to ~120 points from the 2h trail).
|
// Keep a generous window (backfill seeds up to ~120 points from the 2h trail).
|
||||||
if (window.dashboardChartData.timestamps.length > 600) {
|
if (cd.timestamps.length > 600) {
|
||||||
window.dashboardChartData.timestamps.shift();
|
cd.timestamps.shift();
|
||||||
window.dashboardChartData.lp.shift();
|
cd.lp.shift();
|
||||||
window.dashboardChartData.leq.shift();
|
cd.leq.shift();
|
||||||
|
cd.ln1.shift();
|
||||||
|
cd.ln2.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.dashboardChart) {
|
if (window.dashboardChart) {
|
||||||
window.dashboardChart.data.labels = window.dashboardChartData.timestamps;
|
window.dashboardChart.data.labels = cd.timestamps;
|
||||||
window.dashboardChart.data.datasets[0].data = window.dashboardChartData.lp;
|
window.dashboardChart.data.datasets[0].data = cd.lp;
|
||||||
window.dashboardChart.data.datasets[1].data = window.dashboardChartData.leq;
|
window.dashboardChart.data.datasets[1].data = cd.leq;
|
||||||
|
window.dashboardChart.data.datasets[2].data = cd.ln1;
|
||||||
|
window.dashboardChart.data.datasets[3].data = cd.ln2;
|
||||||
window.dashboardChart.update('none');
|
window.dashboardChart.update('none');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -417,13 +453,17 @@ async function backfillDashboardChart(unitId) {
|
|||||||
// Trail timestamps are naive UTC; append 'Z' to render in local time
|
// Trail timestamps are naive UTC; append 'Z' to render in local time
|
||||||
// consistently with the live frames (which use local Date.now()).
|
// consistently with the live frames (which use local Date.now()).
|
||||||
cd.timestamps.push(row.timestamp ? new Date(row.timestamp + 'Z').toLocaleTimeString() : '');
|
cd.timestamps.push(row.timestamp ? new Date(row.timestamp + 'Z').toLocaleTimeString() : '');
|
||||||
cd.lp.push(parseFloat(row.lp || 0));
|
cd.lp.push(numOrNull(row.lp));
|
||||||
cd.leq.push(parseFloat(row.leq || 0));
|
cd.leq.push(numOrNull(row.leq));
|
||||||
|
cd.ln1.push(numOrNull(row.ln1));
|
||||||
|
cd.ln2.push(numOrNull(row.ln2));
|
||||||
}
|
}
|
||||||
if (window.dashboardChart) {
|
if (window.dashboardChart) {
|
||||||
window.dashboardChart.data.labels = cd.timestamps;
|
window.dashboardChart.data.labels = cd.timestamps;
|
||||||
window.dashboardChart.data.datasets[0].data = cd.lp;
|
window.dashboardChart.data.datasets[0].data = cd.lp;
|
||||||
window.dashboardChart.data.datasets[1].data = cd.leq;
|
window.dashboardChart.data.datasets[1].data = cd.leq;
|
||||||
|
window.dashboardChart.data.datasets[2].data = cd.ln1;
|
||||||
|
window.dashboardChart.data.datasets[3].data = cd.ln2;
|
||||||
window.dashboardChart.update('none');
|
window.dashboardChart.update('none');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user