Replace monolithic CLI and web server with route-based Flask API. New routes: api_commands, api_build, api_can, api_monitor, api_ota, api_tunnel. Add honeypot security dashboard with real-time SSE, MITRE ATT&CK mapping, kill chain analysis. New TUI with commander/help modules. Add session management, tunnel proxy core, CAN bus data store. Docker support.
137 lines
5.1 KiB
HTML
137 lines
5.1 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Cameras - ESPILON{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="page" x-data="camerasApp()" x-init="init()">
|
|
<div class="split-h" style="flex:1;">
|
|
|
|
<!-- Left: Camera list -->
|
|
<div class="panel" style="width:320px;min-width:220px;">
|
|
<div class="panel-header">
|
|
<span>Cameras (<span x-text="cameras.length"></span>)</span>
|
|
</div>
|
|
<div class="panel-body">
|
|
<table class="dt">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th class="col-shrink">Rec</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template x-for="cam in cameras" :key="cam.id">
|
|
<tr class="clickable" :class="selectedCam === cam.id ? 'selected' : ''" @click="selectedCam = cam.id">
|
|
<td x-text="cam.id.replace(/_/g, ':')"></td>
|
|
<td class="col-center">
|
|
<span x-show="cam.recording" class="rec-dot active"></span>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
<template x-if="cameras.length === 0">
|
|
<div class="dt-empty">No cameras</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="resizer"></div>
|
|
|
|
<!-- Right: Preview + controls -->
|
|
<div class="panel flex-1">
|
|
<div class="panel-header">
|
|
<span x-text="selectedCam ? selectedCam.replace(/_/g, ':') : 'Select a camera'"></span>
|
|
<div class="panel-header-actions" x-show="selectedCam">
|
|
<button class="btn btn-sm" :class="isRecording(selectedCam) ? 'btn-danger' : ''"
|
|
@click="toggleRecording(selectedCam)"
|
|
x-text="isRecording(selectedCam) ? 'Stop Rec' : 'Record'"></button>
|
|
<span x-show="isRecording(selectedCam)" class="text-xs text-mono" style="color:var(--err);"
|
|
x-text="recTimer(selectedCam)"></span>
|
|
</div>
|
|
</div>
|
|
<div class="panel-body">
|
|
<template x-if="selectedCam">
|
|
<div class="cam-preview">
|
|
<img :src="'/streams/' + selectedCam + '.jpg?t=' + refreshTs"
|
|
:alt="selectedCam"
|
|
onerror="this.style.display='none'">
|
|
</div>
|
|
</template>
|
|
<template x-if="!selectedCam">
|
|
<div class="cam-preview">
|
|
<span class="no-signal">Select a camera from the list</span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
function camerasApp() {
|
|
return {
|
|
cameras: [],
|
|
selectedCam: null,
|
|
refreshTs: Date.now(),
|
|
recState: {},
|
|
|
|
init() {
|
|
this.fetchCameras();
|
|
setInterval(() => this.fetchCameras(), 5000);
|
|
setInterval(() => { this.refreshTs = Date.now(); }, 150);
|
|
setInterval(() => this.$nextTick(() => {}), 1000);
|
|
},
|
|
|
|
async fetchCameras() {
|
|
try {
|
|
const res = await fetch('/api/cameras');
|
|
const data = await res.json();
|
|
this.cameras = data.cameras || [];
|
|
for (const cam of this.cameras) {
|
|
if (cam.recording && !this.recState[cam.id]) {
|
|
this.recState[cam.id] = Date.now();
|
|
} else if (!cam.recording && this.recState[cam.id]) {
|
|
delete this.recState[cam.id];
|
|
}
|
|
}
|
|
if (!this.selectedCam && this.cameras.length > 0) {
|
|
this.selectedCam = this.cameras[0].id;
|
|
}
|
|
} catch (e) {}
|
|
},
|
|
|
|
isRecording(camId) {
|
|
return !!this.recState[camId];
|
|
},
|
|
|
|
recTimer(camId) {
|
|
if (!this.recState[camId]) return '';
|
|
const elapsed = Math.floor((Date.now() - this.recState[camId]) / 1000);
|
|
const m = String(Math.floor(elapsed / 60)).padStart(2, '0');
|
|
const s = String(elapsed % 60).padStart(2, '0');
|
|
return m + ':' + s;
|
|
},
|
|
|
|
async toggleRecording(camId) {
|
|
if (!camId) return;
|
|
const isRec = this.isRecording(camId);
|
|
const endpoint = isRec ? 'stop' : 'start';
|
|
try {
|
|
const res = await fetch('/api/recording/' + endpoint + '/' + camId, { method: 'POST' });
|
|
const data = await res.json();
|
|
if (!data.error) {
|
|
if (isRec) {
|
|
delete this.recState[camId];
|
|
} else {
|
|
this.recState[camId] = Date.now();
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
};
|
|
}
|
|
</script>
|
|
{% endblock %}
|