update main to v0.10.0 #48
+1
-1
@@ -126,7 +126,7 @@
|
|||||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
SFM Live Data
|
SFM Events
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="/sound-level-meters" class="flex items-center px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 {% if request.url.path == '/sound-level-meters' %}bg-gray-100 dark:bg-gray-700{% endif %}">
|
<a href="/sound-level-meters" class="flex items-center px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 {% if request.url.path == '/sound-level-meters' %}bg-gray-100 dark:bg-gray-700{% endif %}">
|
||||||
|
|||||||
+4
-647
@@ -6,7 +6,7 @@
|
|||||||
<div class="mb-6 flex items-center justify-between">
|
<div class="mb-6 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">SFM Event Data</h1>
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">SFM Event Data</h1>
|
||||||
<p class="text-gray-600 dark:text-gray-400 mt-1">Live device control and ACH event database</p>
|
<p class="text-gray-600 dark:text-gray-400 mt-1">Blastware ACH events forwarded by series3-watcher</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span id="sfm-status-badge" class="px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400">
|
<span id="sfm-status-badge" class="px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400">
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stats bar -->
|
<!-- Stats bar -->
|
||||||
<div id="sfm-stats" class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
<div id="sfm-stats" class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||||
<div class="bg-white dark:bg-slate-800 rounded-lg shadow p-4 flex flex-col">
|
<div class="bg-white dark:bg-slate-800 rounded-lg shadow p-4 flex flex-col">
|
||||||
<span class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">Known Units</span>
|
<span class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">Known Units</span>
|
||||||
<span id="stat-units" class="text-3xl font-bold text-gray-900 dark:text-white mt-1">—</span>
|
<span id="stat-units" class="text-3xl font-bold text-gray-900 dark:text-white mt-1">—</span>
|
||||||
@@ -28,14 +28,6 @@
|
|||||||
<span class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">Total Events</span>
|
<span class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">Total Events</span>
|
||||||
<span id="stat-events" class="text-3xl font-bold text-gray-900 dark:text-white mt-1">—</span>
|
<span id="stat-events" class="text-3xl font-bold text-gray-900 dark:text-white mt-1">—</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white dark:bg-slate-800 rounded-lg shadow p-4 flex flex-col">
|
|
||||||
<span class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">Monitor Intervals</span>
|
|
||||||
<span id="stat-monitor" class="text-3xl font-bold text-gray-900 dark:text-white mt-1">—</span>
|
|
||||||
</div>
|
|
||||||
<div class="bg-white dark:bg-slate-800 rounded-lg shadow p-4 flex flex-col">
|
|
||||||
<span class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">ACH Sessions</span>
|
|
||||||
<span id="stat-sessions" class="text-3xl font-bold text-gray-900 dark:text-white mt-1">—</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab navigation -->
|
<!-- Tab navigation -->
|
||||||
@@ -50,18 +42,6 @@
|
|||||||
class="sfm-tab shrink-0 px-6 py-4 text-sm font-medium border-b-2 transition-colors">
|
class="sfm-tab shrink-0 px-6 py-4 text-sm font-medium border-b-2 transition-colors">
|
||||||
Units
|
Units
|
||||||
</button>
|
</button>
|
||||||
<button onclick="switchTab('monitor')" id="tab-monitor"
|
|
||||||
class="sfm-tab shrink-0 px-6 py-4 text-sm font-medium border-b-2 transition-colors">
|
|
||||||
Monitor Log
|
|
||||||
</button>
|
|
||||||
<button onclick="switchTab('sessions')" id="tab-sessions"
|
|
||||||
class="sfm-tab shrink-0 px-6 py-4 text-sm font-medium border-b-2 transition-colors">
|
|
||||||
ACH Sessions
|
|
||||||
</button>
|
|
||||||
<button onclick="switchTab('live')" id="tab-live"
|
|
||||||
class="sfm-tab shrink-0 px-6 py-4 text-sm font-medium border-b-2 transition-colors">
|
|
||||||
Live Device
|
|
||||||
</button>
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -133,259 +113,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── Monitor Log Tab ───────────────────────────────────────────────── -->
|
|
||||||
<div id="panel-monitor" class="sfm-panel hidden p-6">
|
|
||||||
<div class="flex flex-wrap items-end gap-3 mb-4">
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<label class="text-xs text-gray-500 dark:text-gray-400">Unit Serial</label>
|
|
||||||
<select id="ml-serial" onchange="loadMonitorLog()"
|
|
||||||
class="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white">
|
|
||||||
<option value="">All Units</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button onclick="loadMonitorLog()"
|
|
||||||
class="ml-auto px-4 py-1.5 text-sm bg-seismo-orange text-white rounded-lg hover:bg-orange-600">
|
|
||||||
↻ Reload
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="monitor-container" class="overflow-x-auto">
|
|
||||||
<div class="text-center py-12 text-gray-500">Loading…</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ── Sessions Tab ──────────────────────────────────────────────────── -->
|
|
||||||
<div id="panel-sessions" class="sfm-panel hidden p-6">
|
|
||||||
<div class="flex flex-wrap items-end gap-3 mb-4">
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<label class="text-xs text-gray-500 dark:text-gray-400">Unit Serial</label>
|
|
||||||
<select id="sess-serial" onchange="loadSessions()"
|
|
||||||
class="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white">
|
|
||||||
<option value="">All Units</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button onclick="loadSessions()"
|
|
||||||
class="ml-auto px-4 py-1.5 text-sm bg-seismo-orange text-white rounded-lg hover:bg-orange-600">
|
|
||||||
↻ Reload
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="sessions-container" class="overflow-x-auto">
|
|
||||||
<div class="text-center py-12 text-gray-500">Loading…</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ── Live Device Tab ───────────────────────────────────────────────── -->
|
|
||||||
<div id="panel-live" class="sfm-panel hidden p-6">
|
|
||||||
|
|
||||||
<!-- Connection form -->
|
|
||||||
<div class="bg-gray-50 dark:bg-slate-900/50 rounded-lg p-4 mb-6">
|
|
||||||
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">Connect to Device</h3>
|
|
||||||
<div class="flex flex-wrap items-end gap-3">
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<label class="text-xs text-gray-500 dark:text-gray-400">Host / IP</label>
|
|
||||||
<input type="text" id="live-host" placeholder="e.g. 10.0.0.1"
|
|
||||||
class="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white w-40">
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<label class="text-xs text-gray-500 dark:text-gray-400">TCP Port</label>
|
|
||||||
<input type="number" id="live-port" placeholder="12345" value="12345"
|
|
||||||
class="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white w-28">
|
|
||||||
</div>
|
|
||||||
<button onclick="connectDevice()"
|
|
||||||
class="px-4 py-1.5 text-sm bg-seismo-orange text-white rounded-lg hover:bg-orange-600 font-medium">
|
|
||||||
Connect
|
|
||||||
</button>
|
|
||||||
<button onclick="disconnectDevice()" id="btn-disconnect"
|
|
||||||
class="hidden px-4 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300">
|
|
||||||
Disconnect
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Device panel (hidden until connected) -->
|
|
||||||
<div id="live-device-panel" class="hidden">
|
|
||||||
|
|
||||||
<!-- Header row: serial + status + controls -->
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-6">
|
|
||||||
<div>
|
|
||||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white" id="live-serial">—</h2>
|
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400" id="live-firmware">—</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<span id="live-monitor-badge"
|
|
||||||
class="px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300">
|
|
||||||
—
|
|
||||||
</span>
|
|
||||||
<button id="btn-start-monitor" onclick="startMonitoring()"
|
|
||||||
class="hidden px-4 py-1.5 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700 font-medium">
|
|
||||||
Start Monitoring
|
|
||||||
</button>
|
|
||||||
<button id="btn-stop-monitor" onclick="stopMonitoring()"
|
|
||||||
class="hidden px-4 py-1.5 text-sm bg-red-600 text-white rounded-lg hover:bg-red-700 font-medium">
|
|
||||||
Stop Monitoring
|
|
||||||
</button>
|
|
||||||
<button onclick="refreshDeviceStatus()"
|
|
||||||
class="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300">
|
|
||||||
↻ Status
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Info cards -->
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
|
||||||
<!-- Device info card -->
|
|
||||||
<div class="bg-gray-50 dark:bg-slate-900/50 rounded-lg p-4">
|
|
||||||
<h4 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">Device Info</h4>
|
|
||||||
<dl class="space-y-1.5 text-sm">
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<dt class="text-gray-500 dark:text-gray-400">Serial</dt>
|
|
||||||
<dd class="font-medium text-gray-900 dark:text-white" id="info-serial">—</dd>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<dt class="text-gray-500 dark:text-gray-400">Firmware</dt>
|
|
||||||
<dd class="font-medium text-gray-900 dark:text-white" id="info-firmware">—</dd>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<dt class="text-gray-500 dark:text-gray-400">Calibration</dt>
|
|
||||||
<dd class="font-medium text-gray-900 dark:text-white" id="info-cal">—</dd>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<dt class="text-gray-500 dark:text-gray-400">Sample Rate</dt>
|
|
||||||
<dd class="font-medium text-gray-900 dark:text-white" id="info-samplerate">—</dd>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<dt class="text-gray-500 dark:text-gray-400">Record Time</dt>
|
|
||||||
<dd class="font-medium text-gray-900 dark:text-white" id="info-recordtime">—</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
<!-- Monitor status card -->
|
|
||||||
<div class="bg-gray-50 dark:bg-slate-900/50 rounded-lg p-4">
|
|
||||||
<h4 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">Status</h4>
|
|
||||||
<dl class="space-y-1.5 text-sm">
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<dt class="text-gray-500 dark:text-gray-400">Mode</dt>
|
|
||||||
<dd class="font-medium text-gray-900 dark:text-white" id="stat-mode">—</dd>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<dt class="text-gray-500 dark:text-gray-400">Battery</dt>
|
|
||||||
<dd class="font-medium text-gray-900 dark:text-white" id="stat-battery">—</dd>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<dt class="text-gray-500 dark:text-gray-400">Memory Free</dt>
|
|
||||||
<dd class="font-medium text-gray-900 dark:text-white" id="stat-mem-free">—</dd>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<dt class="text-gray-500 dark:text-gray-400">Memory Total</dt>
|
|
||||||
<dd class="font-medium text-gray-900 dark:text-white" id="stat-mem-total">—</dd>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<dt class="text-gray-500 dark:text-gray-400">Stored Events</dt>
|
|
||||||
<dd class="font-medium text-gray-900 dark:text-white" id="stat-event-count">—</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
<!-- Compliance config card -->
|
|
||||||
<div class="bg-gray-50 dark:bg-slate-900/50 rounded-lg p-4">
|
|
||||||
<h4 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">Compliance Config</h4>
|
|
||||||
<dl class="space-y-1.5 text-sm">
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<dt class="text-gray-500 dark:text-gray-400">Trigger (Geo)</dt>
|
|
||||||
<dd class="font-medium text-gray-900 dark:text-white" id="info-trigger">—</dd>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<dt class="text-gray-500 dark:text-gray-400">Alarm (Geo)</dt>
|
|
||||||
<dd class="font-medium text-gray-900 dark:text-white" id="info-alarm">—</dd>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<dt class="text-gray-500 dark:text-gray-400">Max Range</dt>
|
|
||||||
<dd class="font-medium text-gray-900 dark:text-white" id="info-maxrange">—</dd>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<dt class="text-gray-500 dark:text-gray-400">Project</dt>
|
|
||||||
<dd class="font-medium text-gray-900 dark:text-white truncate max-w-[120px]" id="info-project">—</dd>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<dt class="text-gray-500 dark:text-gray-400">Operator</dt>
|
|
||||||
<dd class="font-medium text-gray-900 dark:text-white" id="info-operator">—</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Config editor -->
|
|
||||||
<div class="bg-gray-50 dark:bg-slate-900/50 rounded-lg p-4 mb-6">
|
|
||||||
<div class="flex items-center justify-between mb-3">
|
|
||||||
<h4 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Update Project Info</h4>
|
|
||||||
<span id="config-saved-msg" class="hidden text-xs text-green-600 dark:text-green-400">✓ Saved</span>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 mb-3">
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<label class="text-xs text-gray-500 dark:text-gray-400">Project</label>
|
|
||||||
<input type="text" id="cfg-project" placeholder="Project name"
|
|
||||||
class="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white">
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<label class="text-xs text-gray-500 dark:text-gray-400">Client</label>
|
|
||||||
<input type="text" id="cfg-client" placeholder="Client name"
|
|
||||||
class="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white">
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<label class="text-xs text-gray-500 dark:text-gray-400">Operator</label>
|
|
||||||
<input type="text" id="cfg-operator" placeholder="Operator name"
|
|
||||||
class="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white">
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<label class="text-xs text-gray-500 dark:text-gray-400">Sensor Location</label>
|
|
||||||
<input type="text" id="cfg-location" placeholder="Sensor location"
|
|
||||||
class="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white">
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<label class="text-xs text-gray-500 dark:text-gray-400">Trigger Level (in/s)</label>
|
|
||||||
<input type="number" id="cfg-trigger" placeholder="0.500" step="0.001" min="0"
|
|
||||||
class="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white">
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<label class="text-xs text-gray-500 dark:text-gray-400">Sample Rate</label>
|
|
||||||
<select id="cfg-samplerate"
|
|
||||||
class="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-slate-700 text-gray-900 dark:text-white">
|
|
||||||
<option value="1024">1024 sps (Standard)</option>
|
|
||||||
<option value="2048">2048 sps (Fast)</option>
|
|
||||||
<option value="4096">4096 sps (Faster)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button onclick="pushConfig()"
|
|
||||||
class="px-4 py-1.5 text-sm bg-seismo-orange text-white rounded-lg hover:bg-orange-600 font-medium">
|
|
||||||
Push Config to Device
|
|
||||||
</button>
|
|
||||||
<span id="config-error-msg" class="hidden ml-3 text-xs text-red-500"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Live events from device -->
|
|
||||||
<div>
|
|
||||||
<div class="flex items-center justify-between mb-3">
|
|
||||||
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Events on Device</h4>
|
|
||||||
<button onclick="loadLiveEvents()"
|
|
||||||
class="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300">
|
|
||||||
↻ Load Events
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="live-events-container">
|
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">Click "Load Events" to download events from the device. This may take 30–60 seconds.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Live operation log -->
|
|
||||||
<div id="live-log" class="mt-4 hidden">
|
|
||||||
<h4 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">Operation Log</h4>
|
|
||||||
<div id="live-log-content"
|
|
||||||
class="bg-gray-900 text-green-400 text-xs font-mono rounded-lg p-3 max-h-40 overflow-y-auto space-y-0.5">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div><!-- end live panel -->
|
|
||||||
|
|
||||||
</div><!-- end tab container -->
|
</div><!-- end tab container -->
|
||||||
|
|
||||||
<!-- Event detail modal -->
|
<!-- Event detail modal -->
|
||||||
@@ -439,10 +166,6 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
// ── State ───────────────────────────────────────────────────────────────────
|
// ── State ───────────────────────────────────────────────────────────────────
|
||||||
let _liveHost = null;
|
|
||||||
let _livePort = null;
|
|
||||||
let _liveConnected = false;
|
|
||||||
let _statusPollTimer = null;
|
|
||||||
let _knownSerials = [];
|
let _knownSerials = [];
|
||||||
|
|
||||||
// ── Tabs ────────────────────────────────────────────────────────────────────
|
// ── Tabs ────────────────────────────────────────────────────────────────────
|
||||||
@@ -454,8 +177,6 @@ function switchTab(name) {
|
|||||||
|
|
||||||
// Lazy-load tabs on first visit
|
// Lazy-load tabs on first visit
|
||||||
if (name === 'units' && document.getElementById('units-container').innerHTML.includes('Loading')) loadUnits();
|
if (name === 'units' && document.getElementById('units-container').innerHTML.includes('Loading')) loadUnits();
|
||||||
if (name === 'monitor' && document.getElementById('monitor-container').innerHTML.includes('Loading')) loadMonitorLog();
|
|
||||||
if (name === 'sessions' && document.getElementById('sessions-container').innerHTML.includes('Loading')) loadSessions();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── SFM health ───────────────────────────────────────────────────────────────
|
// ── SFM health ───────────────────────────────────────────────────────────────
|
||||||
@@ -489,16 +210,12 @@ async function loadStats() {
|
|||||||
_knownSerials = units.map(u => u.serial);
|
_knownSerials = units.map(u => u.serial);
|
||||||
|
|
||||||
const totalEvents = units.reduce((s, u) => s + (u.total_events || 0), 0);
|
const totalEvents = units.reduce((s, u) => s + (u.total_events || 0), 0);
|
||||||
const totalMonitor = units.reduce((s, u) => s + (u.total_monitor_entries || 0), 0);
|
|
||||||
const totalSessions = units.reduce((s, u) => s + (u.total_sessions || 0), 0);
|
|
||||||
|
|
||||||
document.getElementById('stat-units').textContent = units.length;
|
document.getElementById('stat-units').textContent = units.length;
|
||||||
document.getElementById('stat-events').textContent = totalEvents.toLocaleString();
|
document.getElementById('stat-events').textContent = totalEvents.toLocaleString();
|
||||||
document.getElementById('stat-monitor').textContent = totalMonitor.toLocaleString();
|
|
||||||
document.getElementById('stat-sessions').textContent = totalSessions.toLocaleString();
|
|
||||||
|
|
||||||
// Populate serial dropdowns
|
// Populate serial dropdowns
|
||||||
['ev-serial', 'ml-serial', 'sess-serial'].forEach(id => {
|
['ev-serial'].forEach(id => {
|
||||||
const sel = document.getElementById(id);
|
const sel = document.getElementById(id);
|
||||||
const cur = sel.value;
|
const cur = sel.value;
|
||||||
while (sel.options.length > 1) sel.remove(1);
|
while (sel.options.length > 1) sel.remove(1);
|
||||||
@@ -697,7 +414,7 @@ async function loadUnits() {
|
|||||||
const units = await r.json();
|
const units = await r.json();
|
||||||
|
|
||||||
if (units.length === 0) {
|
if (units.length === 0) {
|
||||||
container.innerHTML = '<div class="text-center py-12 text-gray-500 text-sm">No units in database. Start the ACH server and wait for a call-home.</div>';
|
container.innerHTML = '<div class="text-center py-12 text-gray-500 text-sm">No units in database. Waiting for series3-watcher to forward events from Blastware ACH.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -707,8 +424,6 @@ async function loadUnits() {
|
|||||||
<td class="px-4 py-3 text-sm font-mono font-semibold text-seismo-orange">${esc(u.serial)}</td>
|
<td class="px-4 py-3 text-sm font-mono font-semibold text-seismo-orange">${esc(u.serial)}</td>
|
||||||
<td class="px-4 py-3 text-sm text-gray-700 dark:text-gray-300">${lastSeen}</td>
|
<td class="px-4 py-3 text-sm text-gray-700 dark:text-gray-300">${lastSeen}</td>
|
||||||
<td class="px-4 py-3 text-sm text-gray-700 dark:text-gray-300">${(u.total_events || 0).toLocaleString()}</td>
|
<td class="px-4 py-3 text-sm text-gray-700 dark:text-gray-300">${(u.total_events || 0).toLocaleString()}</td>
|
||||||
<td class="px-4 py-3 text-sm text-gray-700 dark:text-gray-300">${(u.total_monitor_entries || 0).toLocaleString()}</td>
|
|
||||||
<td class="px-4 py-3 text-sm text-gray-700 dark:text-gray-300">${(u.total_sessions || 0).toLocaleString()}</td>
|
|
||||||
<td class="px-4 py-3 text-sm">
|
<td class="px-4 py-3 text-sm">
|
||||||
<button onclick="filterEventsBySerial('${esc(u.serial)}')"
|
<button onclick="filterEventsBySerial('${esc(u.serial)}')"
|
||||||
class="px-3 py-1 text-xs rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300">
|
class="px-3 py-1 text-xs rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300">
|
||||||
@@ -726,8 +441,6 @@ async function loadUnits() {
|
|||||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider text-left">Serial</th>
|
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider text-left">Serial</th>
|
||||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider text-left">Last Seen</th>
|
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider text-left">Last Seen</th>
|
||||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider text-left">Events</th>
|
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider text-left">Events</th>
|
||||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider text-left">Monitor Intervals</th>
|
|
||||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider text-left">Sessions</th>
|
|
||||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider text-left">Actions</th>
|
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider text-left">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -745,346 +458,6 @@ function filterEventsBySerial(serial) {
|
|||||||
loadEvents();
|
loadEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Monitor log tab ──────────────────────────────────────────────────────────
|
|
||||||
async function loadMonitorLog() {
|
|
||||||
const container = document.getElementById('monitor-container');
|
|
||||||
container.innerHTML = '<div class="text-center py-8 text-gray-500"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-seismo-orange mx-auto mb-3"></div>Loading…</div>';
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
const serial = document.getElementById('ml-serial').value;
|
|
||||||
if (serial) params.set('serial', serial);
|
|
||||||
params.set('limit', '500');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const r = await fetch('/api/sfm/db/monitor_log?' + params.toString());
|
|
||||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
||||||
const d = await r.json();
|
|
||||||
|
|
||||||
if (!d.entries || d.entries.length === 0) {
|
|
||||||
container.innerHTML = '<div class="text-center py-12 text-gray-500 text-sm">No monitor log entries found.</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = d.entries.map(e => {
|
|
||||||
const start = e.start_time ? e.start_time.slice(0, 19).replace('T', ' ') : '—';
|
|
||||||
const stop = e.stop_time ? e.stop_time.slice(0, 19).replace('T', ' ') : '—';
|
|
||||||
const dur = e.duration_seconds != null ? fmtDuration(e.duration_seconds) : '—';
|
|
||||||
const thr = e.geo_threshold_ips != null ? e.geo_threshold_ips.toFixed(3) + ' in/s' : '—';
|
|
||||||
return `<tr class="hover:bg-gray-50 dark:hover:bg-slate-700/50">
|
|
||||||
<td class="px-4 py-2.5 text-sm font-mono font-medium text-seismo-orange">${esc(e.serial)}</td>
|
|
||||||
<td class="px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 whitespace-nowrap">${start}</td>
|
|
||||||
<td class="px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 whitespace-nowrap">${stop}</td>
|
|
||||||
<td class="px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300">${dur}</td>
|
|
||||||
<td class="px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300">${thr}</td>
|
|
||||||
</tr>`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="text-xs text-gray-500 dark:text-gray-400 mb-2">${d.entries.length} entries</div>
|
|
||||||
<table class="w-full">
|
|
||||||
<thead class="bg-gray-50 dark:bg-slate-700 border-b border-gray-200 dark:border-gray-600">
|
|
||||||
<tr>
|
|
||||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider text-left">Serial</th>
|
|
||||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider text-left">Start</th>
|
|
||||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider text-left">Stop</th>
|
|
||||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider text-left">Duration</th>
|
|
||||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider text-left">Geo Threshold</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">${rows}</tbody>
|
|
||||||
</table>`;
|
|
||||||
} catch (e) {
|
|
||||||
container.innerHTML = `<div class="text-center py-8 text-red-500 text-sm">Failed to load monitor log: ${e.message}</div>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Sessions tab ─────────────────────────────────────────────────────────────
|
|
||||||
async function loadSessions() {
|
|
||||||
const container = document.getElementById('sessions-container');
|
|
||||||
container.innerHTML = '<div class="text-center py-8 text-gray-500"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-seismo-orange mx-auto mb-3"></div>Loading…</div>';
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
const serial = document.getElementById('sess-serial').value;
|
|
||||||
if (serial) params.set('serial', serial);
|
|
||||||
params.set('limit', '100');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const r = await fetch('/api/sfm/db/sessions?' + params.toString());
|
|
||||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
||||||
const d = await r.json();
|
|
||||||
|
|
||||||
if (!d.sessions || d.sessions.length === 0) {
|
|
||||||
container.innerHTML = '<div class="text-center py-12 text-gray-500 text-sm">No sessions found.</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = d.sessions.map(s => {
|
|
||||||
const ts = s.session_time ? s.session_time.slice(0, 19).replace('T', ' ') : '—';
|
|
||||||
const dur = s.duration_seconds != null ? fmtDuration(s.duration_seconds) : '—';
|
|
||||||
return `<tr class="hover:bg-gray-50 dark:hover:bg-slate-700/50">
|
|
||||||
<td class="px-4 py-2.5 text-sm font-mono font-medium text-seismo-orange">${esc(s.serial)}</td>
|
|
||||||
<td class="px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 whitespace-nowrap">${ts}</td>
|
|
||||||
<td class="px-4 py-2.5 text-sm text-gray-500 dark:text-gray-400">${esc(s.peer || '—')}</td>
|
|
||||||
<td class="px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300">${s.events_downloaded || 0}</td>
|
|
||||||
<td class="px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300">${s.monitor_entries || 0}</td>
|
|
||||||
<td class="px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300">${dur}</td>
|
|
||||||
</tr>`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="text-xs text-gray-500 dark:text-gray-400 mb-2">${d.sessions.length} sessions</div>
|
|
||||||
<table class="w-full">
|
|
||||||
<thead class="bg-gray-50 dark:bg-slate-700 border-b border-gray-200 dark:border-gray-600">
|
|
||||||
<tr>
|
|
||||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider text-left">Serial</th>
|
|
||||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider text-left">Time</th>
|
|
||||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider text-left">Peer IP</th>
|
|
||||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider text-left">Events DL</th>
|
|
||||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider text-left">Monitor</th>
|
|
||||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider text-left">Duration</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">${rows}</tbody>
|
|
||||||
</table>`;
|
|
||||||
} catch (e) {
|
|
||||||
container.innerHTML = `<div class="text-center py-8 text-red-500 text-sm">Failed to load sessions: ${e.message}</div>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Live Device tab ──────────────────────────────────────────────────────────
|
|
||||||
function liveLog(msg) {
|
|
||||||
const logDiv = document.getElementById('live-log');
|
|
||||||
const content = document.getElementById('live-log-content');
|
|
||||||
logDiv.classList.remove('hidden');
|
|
||||||
const ts = new Date().toLocaleTimeString();
|
|
||||||
const line = document.createElement('div');
|
|
||||||
line.textContent = `[${ts}] ${msg}`;
|
|
||||||
content.appendChild(line);
|
|
||||||
content.scrollTop = content.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function connectDevice() {
|
|
||||||
const host = document.getElementById('live-host').value.trim();
|
|
||||||
const port = parseInt(document.getElementById('live-port').value) || 12345;
|
|
||||||
|
|
||||||
if (!host) {
|
|
||||||
alert('Enter a host/IP address.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_liveHost = host;
|
|
||||||
_livePort = port;
|
|
||||||
liveLog(`Connecting to ${host}:${port}…`);
|
|
||||||
|
|
||||||
// Fetch device info
|
|
||||||
try {
|
|
||||||
const r = await fetch(`/api/sfm/device/info?host=${encodeURIComponent(host)}&tcp_port=${port}`);
|
|
||||||
if (!r.ok) {
|
|
||||||
const err = await r.json().catch(() => ({detail: 'HTTP ' + r.status}));
|
|
||||||
throw new Error(err.detail || 'HTTP ' + r.status);
|
|
||||||
}
|
|
||||||
const d = await r.json();
|
|
||||||
liveLog(`Connected — Serial: ${d.serial}, Firmware: ${d.firmware_version}`);
|
|
||||||
_liveConnected = true;
|
|
||||||
populateDeviceInfo(d);
|
|
||||||
document.getElementById('live-device-panel').classList.remove('hidden');
|
|
||||||
document.getElementById('btn-disconnect').classList.remove('hidden');
|
|
||||||
|
|
||||||
// Pre-fill config form
|
|
||||||
if (d.compliance_config) {
|
|
||||||
const cc = d.compliance_config;
|
|
||||||
if (cc.project) document.getElementById('cfg-project').value = cc.project;
|
|
||||||
if (cc.client) document.getElementById('cfg-client').value = cc.client;
|
|
||||||
if (cc.operator) document.getElementById('cfg-operator').value = cc.operator;
|
|
||||||
if (cc.sensor_location) document.getElementById('cfg-location').value = cc.sensor_location;
|
|
||||||
if (cc.trigger_level_geo) document.getElementById('cfg-trigger').value = cc.trigger_level_geo.toFixed(3);
|
|
||||||
if (cc.sample_rate) document.getElementById('cfg-samplerate').value = cc.sample_rate;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch monitor status
|
|
||||||
await refreshDeviceStatus();
|
|
||||||
|
|
||||||
// Start polling every 15s
|
|
||||||
if (_statusPollTimer) clearInterval(_statusPollTimer);
|
|
||||||
_statusPollTimer = setInterval(refreshDeviceStatus, 15000);
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
liveLog(`Connection failed: ${e.message}`);
|
|
||||||
_liveConnected = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function disconnectDevice() {
|
|
||||||
_liveConnected = false;
|
|
||||||
_liveHost = null;
|
|
||||||
_livePort = null;
|
|
||||||
if (_statusPollTimer) { clearInterval(_statusPollTimer); _statusPollTimer = null; }
|
|
||||||
document.getElementById('live-device-panel').classList.add('hidden');
|
|
||||||
document.getElementById('btn-disconnect').classList.add('hidden');
|
|
||||||
liveLog('Disconnected.');
|
|
||||||
}
|
|
||||||
|
|
||||||
function populateDeviceInfo(d) {
|
|
||||||
document.getElementById('live-serial').textContent = d.serial || '—';
|
|
||||||
document.getElementById('live-firmware').textContent = d.firmware_version || '—';
|
|
||||||
document.getElementById('info-serial').textContent = d.serial || '—';
|
|
||||||
document.getElementById('info-firmware').textContent = d.firmware_version || '—';
|
|
||||||
document.getElementById('info-cal').textContent = d.calibration_date || '—';
|
|
||||||
document.getElementById('stat-event-count').textContent = (d.event_count != null) ? d.event_count : '—';
|
|
||||||
|
|
||||||
const cc = d.compliance_config;
|
|
||||||
if (cc) {
|
|
||||||
document.getElementById('info-samplerate').textContent = (cc.sample_rate || '—') + ' sps';
|
|
||||||
document.getElementById('info-recordtime').textContent = cc.record_time != null ? cc.record_time.toFixed(1) + ' s' : '—';
|
|
||||||
document.getElementById('info-trigger').textContent = cc.trigger_level_geo != null ? cc.trigger_level_geo.toFixed(3) + ' in/s' : '—';
|
|
||||||
document.getElementById('info-alarm').textContent = cc.alarm_level_geo != null ? cc.alarm_level_geo.toFixed(3) + ' in/s' : '—';
|
|
||||||
document.getElementById('info-maxrange').textContent = cc.max_range_geo != null ? cc.max_range_geo.toFixed(2) + ' in/s' : '—';
|
|
||||||
document.getElementById('info-project').textContent = cc.project || '—';
|
|
||||||
document.getElementById('info-operator').textContent = cc.operator || '—';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshDeviceStatus() {
|
|
||||||
if (!_liveHost || !_livePort) return;
|
|
||||||
try {
|
|
||||||
const r = await fetch(`/api/sfm/device/monitor/status?host=${encodeURIComponent(_liveHost)}&tcp_port=${_livePort}`);
|
|
||||||
if (!r.ok) {
|
|
||||||
liveLog('Status poll failed: HTTP ' + r.status);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const d = await r.json();
|
|
||||||
const isMonitoring = d.is_monitoring;
|
|
||||||
const badge = document.getElementById('live-monitor-badge');
|
|
||||||
const btnStart = document.getElementById('btn-start-monitor');
|
|
||||||
const btnStop = document.getElementById('btn-stop-monitor');
|
|
||||||
|
|
||||||
if (isMonitoring) {
|
|
||||||
badge.textContent = '● Monitoring';
|
|
||||||
badge.className = 'px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
|
|
||||||
btnStart.classList.add('hidden');
|
|
||||||
btnStop.classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
badge.textContent = '○ Idle';
|
|
||||||
badge.className = 'px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300';
|
|
||||||
btnStart.classList.remove('hidden');
|
|
||||||
btnStop.classList.add('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('stat-mode').textContent = isMonitoring ? 'Monitoring' : 'Idle';
|
|
||||||
|
|
||||||
if (d.battery_voltage_v != null) {
|
|
||||||
document.getElementById('stat-battery').textContent = d.battery_voltage_v.toFixed(2) + ' V';
|
|
||||||
}
|
|
||||||
if (d.memory_free_bytes != null && d.memory_total_bytes != null) {
|
|
||||||
document.getElementById('stat-mem-free').textContent = fmtBytes(d.memory_free_bytes);
|
|
||||||
document.getElementById('stat-mem-total').textContent = fmtBytes(d.memory_total_bytes);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
liveLog(`Status error: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startMonitoring() {
|
|
||||||
if (!_liveHost) return;
|
|
||||||
liveLog('Starting monitoring…');
|
|
||||||
const btn = document.getElementById('btn-start-monitor');
|
|
||||||
btn.disabled = true; btn.textContent = 'Starting…';
|
|
||||||
try {
|
|
||||||
const r = await fetch(`/api/sfm/device/monitor/start?host=${encodeURIComponent(_liveHost)}&tcp_port=${_livePort}`, { method: 'POST' });
|
|
||||||
const d = await r.json();
|
|
||||||
if (r.ok) {
|
|
||||||
liveLog('Monitoring started.');
|
|
||||||
setTimeout(refreshDeviceStatus, 2000);
|
|
||||||
} else {
|
|
||||||
liveLog('Start failed: ' + (d.detail || r.status));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
liveLog('Start error: ' + e.message);
|
|
||||||
}
|
|
||||||
btn.disabled = false; btn.textContent = 'Start Monitoring';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function stopMonitoring() {
|
|
||||||
if (!_liveHost) return;
|
|
||||||
liveLog('Stopping monitoring…');
|
|
||||||
const btn = document.getElementById('btn-stop-monitor');
|
|
||||||
btn.disabled = true; btn.textContent = 'Stopping…';
|
|
||||||
try {
|
|
||||||
const r = await fetch(`/api/sfm/device/monitor/stop?host=${encodeURIComponent(_liveHost)}&tcp_port=${_livePort}`, { method: 'POST' });
|
|
||||||
const d = await r.json();
|
|
||||||
if (r.ok) {
|
|
||||||
liveLog('Monitoring stopped.');
|
|
||||||
setTimeout(refreshDeviceStatus, 2000);
|
|
||||||
} else {
|
|
||||||
liveLog('Stop failed: ' + (d.detail || r.status));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
liveLog('Stop error: ' + e.message);
|
|
||||||
}
|
|
||||||
btn.disabled = false; btn.textContent = 'Stop Monitoring';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pushConfig() {
|
|
||||||
if (!_liveHost) return;
|
|
||||||
const errEl = document.getElementById('config-error-msg');
|
|
||||||
const savedEl = document.getElementById('config-saved-msg');
|
|
||||||
errEl.classList.add('hidden');
|
|
||||||
savedEl.classList.add('hidden');
|
|
||||||
|
|
||||||
const body = {
|
|
||||||
project: document.getElementById('cfg-project').value.trim() || null,
|
|
||||||
client_name: document.getElementById('cfg-client').value.trim() || null,
|
|
||||||
operator: document.getElementById('cfg-operator').value.trim() || null,
|
|
||||||
sensor_location: document.getElementById('cfg-location').value.trim() || null,
|
|
||||||
trigger_level_geo: parseFloat(document.getElementById('cfg-trigger').value) || null,
|
|
||||||
sample_rate: parseInt(document.getElementById('cfg-samplerate').value) || null,
|
|
||||||
};
|
|
||||||
|
|
||||||
liveLog('Pushing config to device…');
|
|
||||||
try {
|
|
||||||
const r = await fetch(
|
|
||||||
`/api/sfm/device/config/project?host=${encodeURIComponent(_liveHost)}&tcp_port=${_livePort}`,
|
|
||||||
{ method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body) }
|
|
||||||
);
|
|
||||||
const d = await r.json();
|
|
||||||
if (r.ok) {
|
|
||||||
liveLog('Config pushed successfully.');
|
|
||||||
savedEl.classList.remove('hidden');
|
|
||||||
setTimeout(() => savedEl.classList.add('hidden'), 3000);
|
|
||||||
} else {
|
|
||||||
const msg = d.detail || ('HTTP ' + r.status);
|
|
||||||
liveLog('Config push failed: ' + msg);
|
|
||||||
errEl.textContent = msg;
|
|
||||||
errEl.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
liveLog('Config error: ' + e.message);
|
|
||||||
errEl.textContent = e.message;
|
|
||||||
errEl.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadLiveEvents() {
|
|
||||||
if (!_liveHost) return;
|
|
||||||
const container = document.getElementById('live-events-container');
|
|
||||||
container.innerHTML = '<div class="text-center py-8 text-gray-500"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-seismo-orange mx-auto mb-3"></div><p class="text-sm">Downloading events from device…<br><span class="text-xs">This may take 30–60 seconds.</span></p></div>';
|
|
||||||
liveLog('Downloading events from device…');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const r = await fetch(`/api/sfm/device/events?host=${encodeURIComponent(_liveHost)}&tcp_port=${_livePort}`, {signal: AbortSignal.timeout(120000)});
|
|
||||||
if (!r.ok) {
|
|
||||||
const err = await r.json().catch(() => ({detail: 'HTTP ' + r.status}));
|
|
||||||
throw new Error(err.detail || 'HTTP ' + r.status);
|
|
||||||
}
|
|
||||||
const d = await r.json();
|
|
||||||
liveLog(`Downloaded ${d.event_count} events.`);
|
|
||||||
if (d.cached) liveLog('(served from cache — no new events detected)');
|
|
||||||
renderEventsTable(d.events, d.event_count, container);
|
|
||||||
} catch (e) {
|
|
||||||
liveLog('Event download failed: ' + e.message);
|
|
||||||
container.innerHTML = `<div class="text-center py-8 text-red-500 text-sm">Failed: ${e.message}</div>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
function esc(s) {
|
function esc(s) {
|
||||||
@@ -1092,22 +465,6 @@ function esc(s) {
|
|||||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmtDuration(secs) {
|
|
||||||
if (secs == null) return '—';
|
|
||||||
if (secs < 60) return secs.toFixed(0) + 's';
|
|
||||||
const m = Math.floor(secs / 60), s = Math.floor(secs % 60);
|
|
||||||
if (m < 60) return `${m}m ${s}s`;
|
|
||||||
const h = Math.floor(m / 60), rm = m % 60;
|
|
||||||
return `${h}h ${rm}m`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fmtBytes(b) {
|
|
||||||
if (b == null) return '—';
|
|
||||||
if (b < 1024) return b + ' B';
|
|
||||||
if (b < 1048576) return (b / 1024).toFixed(0) + ' KB';
|
|
||||||
return (b / 1048576).toFixed(1) + ' MB';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Init ──────────────────────────────────────────────────────────────────────
|
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
checkSFMHealth();
|
checkSFMHealth();
|
||||||
|
|||||||
Reference in New Issue
Block a user