espilon-source/tools/C3PO/templates/cameras.html
Eun0us 79c2a4d4bf c3po: full server rewrite with modular routes and honeypot dashboard
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.
2026-02-28 20:12:27 +01:00

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 %}