Admin-Bereich, PWA-Manifest und Deploy-Setup

- Admin: Login, Dashboard, Protokoll, Stationen mit QR-Links
- PWA: vite-plugin-pwa mit Workbox Offline-Caching
- SvelteKit adapter-node + Dockerfile für DS-Deployment
- docker-compose.yml mit app + pocketbase Services
- Makefile: make deploy Befehl
This commit is contained in:
rene 2026-05-17 11:37:42 +02:00
parent f2615c9e07
commit 18570a42f0
15 changed files with 6042 additions and 12 deletions

View file

@ -0,0 +1,148 @@
<script lang="ts">
import { pb } from '$lib/pb';
import { onMount } from 'svelte';
const STATUS_LABEL: Record<string, string> = {
ok: 'OK', abweichung: 'Abweichung', kritisch: 'Kritisch'
};
const STATUS_COLOR: Record<string, string> = {
ok: '#16a34a', abweichung: '#ea580c', kritisch: '#dc2626'
};
let logs = $state<any[]>([]);
let stats = $state({ total: 0, ok: 0, abweichung: 0, kritisch: 0 });
let loading = $state(true);
onMount(async () => {
const today = new Date();
today.setHours(0, 0, 0, 0);
const tenantId = (pb.authStore.record as any)?.tenant;
const result = await pb.collection('check_logs').getList(1, 50, {
filter: `tenant = "${tenantId}" && created >= "${today.toISOString()}"`,
expand: 'station',
sort: '-created'
});
logs = result.items;
stats.total = result.totalItems;
stats.ok = logs.filter(l => l.status === 'ok').length;
stats.abweichung = logs.filter(l => l.status === 'abweichung').length;
stats.kritisch = logs.filter(l => l.status === 'kritisch').length;
loading = false;
});
function formatTime(iso: string) {
return new Date(iso).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
}
</script>
<svelte:head><title>Dashboard — checkflo</title></svelte:head>
<div class="page">
<h1>Dashboard</h1>
<p class="date">{new Date().toLocaleDateString('de-DE', { weekday: 'long', day: 'numeric', month: 'long' })}</p>
<div class="stats">
<div class="stat">
<span class="stat-num">{stats.total}</span>
<span class="stat-label">Checks heute</span>
</div>
<div class="stat stat-ok">
<span class="stat-num">{stats.ok}</span>
<span class="stat-label">In Ordnung</span>
</div>
<div class="stat stat-warn">
<span class="stat-num">{stats.abweichung}</span>
<span class="stat-label">Abweichungen</span>
</div>
<div class="stat stat-crit">
<span class="stat-num">{stats.kritisch}</span>
<span class="stat-label">Kritisch</span>
</div>
</div>
<h2>Heutige Einträge</h2>
{#if loading}
<p class="hint">Lädt…</p>
{:else if logs.length === 0}
<p class="hint">Noch keine Einträge heute.</p>
{:else}
<div class="log-list">
{#each logs as log}
<div class="log-item">
<div class="log-status" style="background: {STATUS_COLOR[log.status]}20; color: {STATUS_COLOR[log.status]}">
{STATUS_LABEL[log.status]}
</div>
<div class="log-info">
<span class="log-station">{log.expand?.station?.name ?? '—'}</span>
{#if log.temperature}
<span class="log-temp">{log.temperature} °C</span>
{/if}
</div>
<div class="log-meta">
<span>{log.checked_by}</span>
<span class="log-time">{formatTime(log.created)}</span>
</div>
</div>
{/each}
</div>
{/if}
</div>
<style>
.page { max-width: 800px; }
h1 { font-size: 1.6rem; font-weight: 800; color: #0B1023; }
h2 { font-size: 1.1rem; font-weight: 700; color: #0B1023; margin: 2rem 0 1rem; }
.date { color: #888; margin-top: 0.25rem; margin-bottom: 2rem; }
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
margin-bottom: 2rem;
}
.stat {
background: #fff;
border-radius: 12px;
padding: 1.25rem;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
border-left: 3px solid #e2e8f0;
}
.stat-ok { border-color: #16a34a; }
.stat-warn { border-color: #ea580c; }
.stat-crit { border-color: #dc2626; }
.stat-num { display: block; font-size: 2rem; font-weight: 800; color: #0B1023; line-height: 1; }
.stat-label { font-size: 0.8rem; color: #888; margin-top: 0.25rem; display: block; }
.hint { color: #aaa; font-size: 0.95rem; }
.log-list { display: flex; flex-direction: column; gap: 0.6rem; }
.log-item {
background: #fff;
border-radius: 10px;
padding: 0.9rem 1.1rem;
display: flex;
align-items: center;
gap: 1rem;
box-shadow: 0 1px 4px rgba(0,0,0,0.05);
}
.log-status {
padding: 0.25rem 0.7rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 700;
white-space: nowrap;
}
.log-info { flex: 1; display: flex; flex-direction: column; gap: 0.1rem; }
.log-station { font-weight: 600; font-size: 0.95rem; }
.log-temp { font-size: 0.85rem; color: #555; }
.log-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 0.1rem; font-size: 0.85rem; color: #888; }
.log-time { font-size: 0.8rem; }
@media (max-width: 600px) {
.stats { grid-template-columns: repeat(2, 1fr); }
}
</style>