SQLite-Ereignisse und Personenprofile vorbereiten

This commit is contained in:
marcinator_2 committed 2026-10-01 03:50:26 +02:00
1 parent 643146599d
commit f15ab1d399
9 files changed
+637 -21

No files matched your search

+110 -4
View File
@@ -16,9 +16,10 @@
h2 { margin: 0 0 .7rem; font-size: 1.1rem; }
.camera { width: 100%; aspect-ratio: 4/3; object-fit: contain; background: #050808; border-radius: 8px; }
.row { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; }
button, select, a.download { border: 1px solid #5c6c67; border-radius: 7px; background: #293938; color: #f4f5ed; font: inherit; padding: .65rem .8rem; cursor: pointer; }
button, select, input, a.download { border: 1px solid #5c6c67; border-radius: 7px; background: #293938; color: #f4f5ed; font: inherit; padding: .65rem .8rem; }
button, select, a.download { cursor: pointer; }
button:hover, a.download:hover { background: #36504a; }
button:focus-visible, select:focus-visible, a:focus-visible { outline: 3px solid #ffb44f; outline-offset: 2px; }
button:focus-visible, select:focus-visible, input:focus-visible, a:focus-visible { outline: 3px solid #ffb44f; outline-offset: 2px; }
button.danger { background: #782c26; border-color: #c76652; font-weight: 700; }
button.danger:hover { background: #9c352c; }
button:disabled { opacity: .5; cursor: not-allowed; }
@@ -29,6 +30,11 @@
#message { min-height: 1.5rem; color: #ffca78; }
dl { display: grid; grid-template-columns: auto 1fr; gap: .4rem .8rem; margin: .5rem 0 1rem; }
dt { color: #adbfba; } dd { margin: 0; }
.events { grid-column: 1 / -1; }
#events-list { margin: .5rem 0 0; padding-left: 1.4rem; }
#events-list li { margin: .35rem 0; }
#profiles-list li { margin: .35rem 0; }
.event-tools { margin: .3rem 0 .8rem; }
@media (max-width: 760px) { main { grid-template-columns: 1fr; } }
</style>
</head>
@@ -74,13 +80,28 @@
</div>
<p id="message" role="status" aria-live="polite"></p>
</section>
<section class="events">
<h2>Gespeicherte Testereignisse</h2>
<p class="muted">Erfolgreiche Testauslösungen bleiben nach einem Neustart erhalten. Eine Profilzuordnung kann hier zum Testen oder Korrigieren gesetzt werden.</p>
<ul id="events-list"><li>Lädt …</li></ul>
</section>
<section class="events">
<h2>Personenprofile</h2>
<p class="notice">Die automatische Wiedererkennung per Kamera ist noch nicht aktiv. Diese Profile sind derzeit nur manuell angelegte Kennungen; Bilder oder Gesichtsmerkmale werden nicht gespeichert.</p>
<div class="row">
<input id="profile-label" type="text" maxlength="80" aria-label="Profilkennung" placeholder="Profilkennung oder Spitzname">
<button id="create-profile">Profil anlegen</button>
</div>
<ul id="profiles-list"><li>Lädt …</li></ul>
</section>
</main>
<script>
const $ = id => document.getElementById(id);
const names = { voll: 'Voller Schreck', sanft: 'Sanft', atmosphaere: 'Atmosphäre', aus: 'Aus' };
async function api(path, body = {}) {
let profiles = [];
async function api(path, body = {}, method = 'POST') {
const response = await fetch(path, {
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Halloween-UI': '1' },
method, headers: { 'Content-Type': 'application/json', 'X-Halloween-UI': '1' },
body: JSON.stringify(body)
});
const data = await response.json();
@@ -109,15 +130,100 @@
const data = await api(path, body);
$('message').textContent = data.reason || 'Einstellung übernommen';
await refresh();
await refreshDb();
} catch (error) { $('message').textContent = error.message; }
}
async function refreshProfiles() {
const response = await fetch('/api/profiles', { cache: 'no-store' });
const data = await response.json();
if (!response.ok) throw new Error(data.detail || 'Profile nicht verfügbar');
profiles = data.profiles;
const list = $('profiles-list');
list.replaceChildren();
if (!profiles.length) {
const item = document.createElement('li');
item.textContent = 'Noch keine Profile';
list.append(item);
}
for (const profile of profiles) {
const item = document.createElement('li');
const label = document.createElement('span');
label.textContent = '#' + profile.id + ' · ' + profile.label + ' · ' + profile.event_count + ' Ereignisse ';
const remove = document.createElement('button');
remove.textContent = 'Löschen';
remove.onclick = async () => {
if (!confirm('Profil und seine Zuordnungen entfernen? Die Ereignisse bleiben erhalten.')) return;
try {
await api('/api/profiles/' + profile.id, {}, 'DELETE');
await refreshDb();
} catch (error) { $('message').textContent = error.message; }
};
item.append(label, remove);
list.append(item);
}
}
async function refreshEvents() {
try {
const response = await fetch('/api/events', { cache: 'no-store' });
const data = await response.json();
if (!response.ok) throw new Error(data.detail || 'Ereignisse nicht verfügbar');
const list = $('events-list');
list.replaceChildren();
if (!data.events.length) {
const item = document.createElement('li');
item.textContent = 'Noch keine Testauslösung gespeichert';
list.append(item);
}
for (const event of data.events) {
const item = document.createElement('li');
const summary = document.createElement('span');
summary.textContent = '#' + event.id + ' · ' + new Date(event.occurred_at).toLocaleString('de-DE') + ' · ' + (names[event.mode] || event.mode) + (event.profile_label ? ' · ' + event.profile_label : '');
const tools = document.createElement('div');
tools.className = 'row event-tools';
const select = document.createElement('select');
select.setAttribute('aria-label', 'Profil für Ereignis ' + event.id);
select.add(new Option('Ohne Profil', ''));
for (const profile of profiles) select.add(new Option('#' + profile.id + ' · ' + profile.label, String(profile.id)));
select.value = event.profile_id === null ? '' : String(event.profile_id);
const assign = document.createElement('button');
assign.textContent = 'Zuordnen';
assign.onclick = async () => {
try {
await api('/api/events/' + event.id + '/profile', { profile_id: select.value ? Number(select.value) : null }, 'PATCH');
await refreshDb();
} catch (error) { $('message').textContent = error.message; }
};
tools.append(select, assign);
item.append(summary, tools);
list.append(item);
}
} catch (error) { $('events-list').textContent = error.message; }
}
async function refreshDb() {
try {
await refreshProfiles();
await refreshEvents();
} catch (error) {
$('profiles-list').textContent = error.message;
$('events-list').textContent = error.message;
}
}
$('save-mode').onclick = () => action('/api/mode', { mode: $('mode').value });
$('zone-clear').onclick = () => action('/api/zone-clear');
$('test-trigger').onclick = () => action('/api/test-trigger');
$('stop').onclick = () => action('/api/stop');
$('reset').onclick = () => action('/api/reset');
$('create-profile').onclick = async () => {
try {
await api('/api/profiles', { label: $('profile-label').value });
$('profile-label').value = '';
await refreshDb();
} catch (error) { $('message').textContent = error.message; }
};
refresh();
refreshDb();
setInterval(refresh, 1000);
setInterval(refreshDb, 15000);
</script>
</body>
</html>