Christian Mueller 07bb4acb57 feat: auto-refresh dashboard and datapoints every 10 seconds
Values now update live without manual page reload.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:33:39 +02:00

113 lines
4.7 KiB
TypeScript

import { useEffect, useState } from 'react';
import { api } from '../api/client';
import StatusBadge from '../components/StatusBadge';
interface Datapoint {
id: number;
name: string;
label: string | null;
value: string | null;
unit: string | null;
source_type: string;
status: string;
updated_at: string | null;
}
export default function Datapoints() {
const [datapoints, setDatapoints] = useState<Datapoint[]>([]);
const [edits, setEdits] = useState<Record<number, { label: string; unit: string }>>({});
const [error, setError] = useState('');
async function load() {
try {
const data = await api.get<Datapoint[]>('/api/datapoints');
setDatapoints(data);
const e: Record<number, { label: string; unit: string }> = {};
data.forEach((dp) => { e[dp.id] = { label: dp.label ?? '', unit: dp.unit ?? '' }; });
setEdits(e);
} catch (e) {
setError((e as Error).message);
}
}
useEffect(() => {
load();
const interval = setInterval(load, 10000);
return () => clearInterval(interval);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
async function handleBlur(id: number, field: 'label' | 'unit') {
try {
await api.put(`/api/datapoints/${id}`, { [field]: edits[id]?.[field] || null });
} catch (e) {
setError((e as Error).message);
}
}
async function handleDelete(id: number) {
if (!confirm('Datenpunkt wirklich loeschen?')) return;
try {
await api.del(`/api/datapoints/${id}`);
load();
} catch (e) {
setError((e as Error).message);
}
}
return (
<div>
<h2 className="text-xl font-semibold text-gray-800 mb-6">Datenpunkte</h2>
{error && <div className="mb-4 p-3 bg-red-50 border border-red-200 rounded text-red-700 text-sm">{error}</div>}
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Interner Name</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Bezeichnung</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Wert</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Einheit</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Quelle</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Aktion</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{datapoints.length === 0 && (
<tr><td colSpan={7} className="px-4 py-6 text-center text-gray-400">Keine Datenpunkte vorhanden</td></tr>
)}
{datapoints.map((dp) => (
<tr key={dp.id} className="hover:bg-gray-50">
<td className="px-4 py-2 font-mono text-xs text-gray-600">{dp.name}</td>
<td className="px-4 py-2">
<input
className="border border-transparent hover:border-gray-300 focus:border-sky-400 rounded px-2 py-0.5 text-sm w-full focus:outline-none"
value={edits[dp.id]?.label ?? ''}
onChange={(e) => setEdits({ ...edits, [dp.id]: { ...edits[dp.id], label: e.target.value } })}
onBlur={() => handleBlur(dp.id, 'label')}
/>
</td>
<td className="px-4 py-2 font-semibold text-gray-900">{dp.value ?? '-'}</td>
<td className="px-4 py-2">
<input
className="border border-transparent hover:border-gray-300 focus:border-sky-400 rounded px-2 py-0.5 text-sm w-20 focus:outline-none"
value={edits[dp.id]?.unit ?? ''}
onChange={(e) => setEdits({ ...edits, [dp.id]: { ...edits[dp.id], unit: e.target.value } })}
onBlur={() => handleBlur(dp.id, 'unit')}
/>
</td>
<td className="px-4 py-2 text-gray-500 text-xs">{dp.source_type}</td>
<td className="px-4 py-2"><StatusBadge status={dp.status} /></td>
<td className="px-4 py-2">
<button onClick={() => handleDelete(dp.id)} className="text-red-400 hover:text-red-600 text-xs">Loeschen</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}