feat: add datapoint delete, show emoji icons in preview

- DELETE /api/datapoints/:id endpoint + frontend button
- Preview shows emoji symbols instead of [icon] text

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Mueller 2026-04-07 03:55:35 +02:00
parent 4964a2e8ab
commit 8a5031649a
4 changed files with 49 additions and 2 deletions

View File

@ -49,6 +49,21 @@ interface EpaperPreviewProps {
const SCALE = 0.6; const SCALE = 0.6;
const ICON_SYMBOLS: Record<string, string> = {
thermometer: '🌡',
droplet: '💧',
wind: '🌬',
pressure: '⏱',
sun: '☀',
bolt: '⚡',
gauge: '📊',
wifi: '📶',
battery: '🔋',
check: '✅',
warning: '⚠',
clock: '🕐',
};
export default function EpaperPreview({ layoutId, elements, layoutWidth = 800, layoutHeight = 480 }: EpaperPreviewProps) { export default function EpaperPreview({ layoutId, elements, layoutWidth = 800, layoutHeight = 480 }: EpaperPreviewProps) {
const [liveElements, setLiveElements] = useState<DisplayApiElement[] | null>(null); const [liveElements, setLiveElements] = useState<DisplayApiElement[] | null>(null);
const [displayId, setDisplayId] = useState<number | null>(null); const [displayId, setDisplayId] = useState<number | null>(null);
@ -170,7 +185,7 @@ export default function EpaperPreview({ layoutId, elements, layoutWidth = 800, l
display: 'flex', flexDirection: 'column', justifyContent: 'space-between', display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
}}> }}>
<div style={{ fontSize: fontSize * 0.45, color: '#555', textTransform: 'uppercase', letterSpacing: 0.5 }}> <div style={{ fontSize: fontSize * 0.45, color: '#555', textTransform: 'uppercase', letterSpacing: 0.5 }}>
{el.icon ? `[${el.icon}] ` : ''}{el.label || ''} {el.icon ? (ICON_SYMBOLS[el.icon] || el.icon) + ' ' : ''}{el.label || ''}
</div> </div>
<div style={{ fontSize, fontWeight: 'bold', textAlign: 'right' }}> <div style={{ fontSize, fontWeight: 'bold', textAlign: 'right' }}>
{el.value ?? '—'} <span style={{ fontSize: fontSize * 0.5, color: '#555' }}>{el.unit || ''}</span> {el.value ?? '—'} <span style={{ fontSize: fontSize * 0.5, color: '#555' }}>{el.unit || ''}</span>

View File

@ -40,6 +40,16 @@ export default function Datapoints() {
} }
} }
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 ( return (
<div> <div>
<h2 className="text-xl font-semibold text-gray-800 mb-6">Datenpunkte</h2> <h2 className="text-xl font-semibold text-gray-800 mb-6">Datenpunkte</h2>
@ -55,11 +65,12 @@ export default function Datapoints() {
<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">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">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">Status</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Aktion</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-100"> <tbody className="divide-y divide-gray-100">
{datapoints.length === 0 && ( {datapoints.length === 0 && (
<tr><td colSpan={6} className="px-4 py-6 text-center text-gray-400">Keine Datenpunkte vorhanden</td></tr> <tr><td colSpan={7} className="px-4 py-6 text-center text-gray-400">Keine Datenpunkte vorhanden</td></tr>
)} )}
{datapoints.map((dp) => ( {datapoints.map((dp) => (
<tr key={dp.id} className="hover:bg-gray-50"> <tr key={dp.id} className="hover:bg-gray-50">
@ -83,6 +94,9 @@ export default function Datapoints() {
</td> </td>
<td className="px-4 py-2 text-gray-500 text-xs">{dp.source_type}</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"><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> </tr>
))} ))}
</tbody> </tbody>

View File

@ -3,6 +3,7 @@ import {
getAllDatapoints, getAllDatapoints,
getDatapointById, getDatapointById,
updateDatapoint, updateDatapoint,
deleteDatapoint,
} from '../../modules/datapoints/datapoint-manager'; } from '../../modules/datapoints/datapoint-manager';
const router = Router(); const router = Router();
@ -38,4 +39,15 @@ router.put('/:id', (req, res): void => {
} }
}); });
// DELETE /api/datapoints/:id
router.delete('/:id', (req, res): void => {
try {
const deleted = deleteDatapoint(parseInt(req.params.id));
if (!deleted) { res.status(404).json({ error: 'Datapoint not found' }); return; }
res.status(204).send();
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
export default router; export default router;

View File

@ -118,6 +118,12 @@ export function createDatapointFromPg(
return db.prepare('SELECT * FROM datapoints WHERE id = ?').get(id) as Datapoint; return db.prepare('SELECT * FROM datapoints WHERE id = ?').get(id) as Datapoint;
} }
export function deleteDatapoint(id: number): boolean {
const db = getDb();
const result = db.prepare('DELETE FROM datapoints WHERE id = ?').run(id);
return (result.changes as number) > 0;
}
export function getDatapointByTopicId(topicId: number): Datapoint | undefined { export function getDatapointByTopicId(topicId: number): Datapoint | undefined {
const db = getDb(); const db = getDb();
return db return db