Private
Public Access
SQLite-Ereignisse und Personenprofile vorbereiten
This commit is contained in:
1 parent
643146599d
commit
f15ab1d399
9 files changed
+637
-21
No files matched your search
+110
-4
@@ -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>
|
||||
Reference in new issue
Block a user