Und hier den HTML Code dazu ... <!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name([api.datev-status.de](https://api.datev-status.de/v1/incidents))tent="width=device-width, initial-scale=1.0" />
<title>DATEV Status – Incidents</title>
<style>
:root {
--bg: #f6f8fb;
--panel: #ffffff;
--text: #1f2937;
--muted: #6b7280;
--border: #e5e7eb;
--limited: #f59e0b;
--major: #ef4444;
--minor: #3b82f6;
--operational: #10b981;
--unknown: #6b7280;
--shadow: 0 10px 30px rgba(0,0,0,0.06);
--radius: 16px;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: linear-gradient(180deg, #f8fafc 0%, #eef2ff 100%);
color: var(--text);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 24px;
}
.header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 16px;
margin-bottom: 24px;
}
.title h1 {
margin: 0;
font-size: 2rem;
line-height: 1.1;
}
.title p {
margin: 8px 0 0;
color: var(--muted);
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.control, button {
border: 1px solid var(--border);
background: var(--panel);
color: var(--text);
border-radius: 12px;
padding: 10px 14px;
font-size: 0.95rem;
box-shadow: var(--shadow);
}
button {
cursor: pointer;
transition: transform 0.15s ease, opacity 0.15s ease;
}
button:hover { transform: translateY(-1px); }
button:disabled { opacity: 0.7; cursor: wait; }
.summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 18px;
box-shadow: var(--shadow);
}
.metric-label {
color: var(--muted);
font-size: 0.9rem;
margin-bottom: 8px;
}
.metric-value {
font-size: 1.8rem;
font-weight: 700;
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 20px;
align-items: center;
}
.toolbar input, .toolbar select {
flex: 1 1 220px;
min-width: 180px;
border: 1px solid var(--border);
border-radius: 12px;
padding: 12px 14px;
font-size: 0.95rem;
background: var(--panel);
box-shadow: var(--shadow);
}
.list {
display: grid;
gap: 16px;
}
.incident {
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
box-shadow: var(--shadow);
}
.incident-header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.incident-title {
font-size: 1.1rem;
font-weight: 700;
margin: 0;
}
.badges {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
border-radius: 999px;
padding: 6px 10px;
font-size: 0.82rem;
font-weight: 700;
color: white;
}
.LIMITED { background: var(--limited); }
.MAJOR { background: var(--major); }
.MINOR { background: var(--minor); }
.OPERATIONAL { background: var(--operational); }
.UNKNOWN { background: var(--unknown); }
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
margin-top: 14px;
}
.field {
background: #f9fafb;
border: 1px solid var(--border);
border-radius: 12px;
padding: 12px;
}
.field-label {
color: var(--muted);
font-size: 0.82rem;
margin-bottom: 6px;
}
.field-value {
font-size: 0.95rem;
word-break: break-word;
}
.external-info {
margin-top: 14px;
padding: 14px;
border-left: 4px solid var(--limited);
background: #fffaf0;
border-radius: 12px;
}
.statusline {
color: var(--muted);
font-size: 0.9rem;
margin-bottom: 16px;
}
.empty, .error {
text-align: center;
padding: 32px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.error {
border-color: #fecaca;
background: #fef2f2;
color: #991b1b;
}
.muted { color: var(--muted); }
@media (max-width: 640px) {
.container { padding: 16px; }
.title h1 { font-size: 1.5rem; }
.metric-value { font-size: 1.5rem; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="title">
<h1>DATEV Status – Incidents</h1>
<p>Übersicht der aktuellen Störungen und Einschränkungen aus der DATEV-Status-API</p>
</div>
<div class="controls">
<label class="control">
Auto-Refresh
<select id="refreshInterval" style="border:none; outline:none; background:transparent; margin-left:8px;">
<option value="0">aus</option>
<option value="30000">30 Sek.</option>
<option value="60000" selected>60 Sek.</option>
<option value="300000">5 Min.</option>
</select>
</label>
<button id="reloadBtn">Neu laden</button>
</div>
</div>
<div class="summary" id="summary"></div>
<div class="toolbar">
<input id="searchInput" type="text" placeholder="Nach Service, Status oder Hinweis suchen..." />
<select id="stateFilter">
<option value="ALL">Alle Stati</option>
<option value="LIMITED">LIMITED</option>
<option value="MAJOR">MAJOR</option>
<option value="MINOR">MINOR</option>
<option value="OPERATIONAL">OPERATIONAL</option>
</select>
<select id="sortBy">
<option value="startDesc">Startdatum ↓</option>
<option value="startAsc">Startdatum ↑</option>
<option value="serviceAsc">Service A–Z</option>
<option value="serviceDesc">Service Z–A</option>
<option value="createdDesc">Erstellt ↓</option>
</select>
</div>
<div class="statusline" id="statusLine">Lade Daten…</div>
<div id="content"></div>
</div>
<script>
const API_URL = 'https://api.datev-status.de/v1/incidents';
let incidents = [];
let timerId = null;
const summaryEl = document.getElementById('summary');
const contentEl = document.getElementById('content');
const statusLineEl = document.getElementById('statusLine');
const searchInputEl = document.getElementById('searchInput');
const stateFilterEl = document.getElementById('stateFilter');
const sortByEl = document.getElementById('sortBy');
const refreshIntervalEl = document.getElementById('refreshInterval');
const reloadBtnEl = document.getElementById('reloadBtn');
function formatDate(value) {
if (!value) return '—';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return new Intl.DateTimeFormat('de-DE', {
dateStyle: 'medium',
timeStyle: 'short'
}).format(date);
}
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function getSeverityOrder(state) {
const order = { MAJOR: 4, LIMITED: 3, MINOR: 2, OPERATIONAL: 1 };
return order[state] ?? 0;
}
function renderSummary(items) {
const byState = items.reduce((acc, item) => {
const state = item.currentState || 'UNKNOWN';
acc[state] = (acc[state] || 0) + 1;
return acc;
}, {});
const highestSeverity = items
.map(x => x.currentState || 'UNKNOWN')
.sort((a, b) => getSeverityOrder(b) - getSeverityOrder(a))[0] || '—';
const metrics = [
{ label: 'Incidents gesamt', value: items.length },
{ label: 'Höchster Status', value: highestSeverity },
{ label: 'LIMITED', value: byState.LIMITED || 0 },
{ label: 'MAJOR', value: byState.MAJOR || 0 }
];
summaryEl.innerHTML = metrics.map(metric => `
<div class="card">
<div class="metric-label">${escapeHtml(metric.label)}</div>
<div class="metric-value">${escapeHtml(metric.value)}</div>
</div>
`).join('');
}
function applyFilters(items) {
const search = searchInputEl.value.trim().toLowerCase();
const stateFilter = stateFilterEl.value;
const sortBy = sortByEl.value;
let result = [...items];
if (stateFilter !== 'ALL') {
result = result.filter(item => (item.currentState || 'UNKNOWN') === stateFilter);
}
if (search) {
result = result.filter(item => {
const haystack = [
item.serviceInfoDto?.name,
item.currentState,
item.worstState,
item.externalInfo,
item.id
].join(' ').toLowerCase();
return haystack.includes(search);
});
}
result.sort((a, b) => {
switch (sortBy) {
case 'startAsc':
return new Date(a.startDate) - new Date(b.startDate);
case 'serviceAsc':
return (a.serviceInfoDto?.name || '').localeCompare(b.serviceInfoDto?.name || '', 'de');
case 'serviceDesc':
return (b.serviceInfoDto?.name || '').localeCompare(a.serviceInfoDto?.name || '', 'de');
case 'createdDesc':
return new Date(b.createdAt) - new Date(a.createdAt);
case 'startDesc':
default:
return new Date(b.startDate) - new Date(a.startDate);
}
});
return result;
}
function renderIncidents(items) {
if (!items.length) {
contentEl.innerHTML = '<div class="empty">Keine passenden Incidents gefunden 🎉</div>';
return;
}
contentEl.innerHTML = `
<div class="list">
${items.map(item => {
const state = item.currentState || 'UNKNOWN';
const worstState = item.worstState || 'UNKNOWN';
const serviceName = item.serviceInfoDto?.name || 'Unbekannter Service';
const productCount = item.serviceInfoDto?.productIris?.length || 0;
return `
<article class="incident">
<div class="incident-header">
<div>
<h2 class="incident-title">${escapeHtml(serviceName)}</h2>
<div class="muted">Incident-ID: ${escapeHtml(item.id)}</div>
</div>
<div class="badges">
<span class="badge ${escapeHtml(state)}">Aktuell: ${escapeHtml(state)}</span>
<span class="badge ${escapeHtml(worstState)}">Schlimmster Status: ${escapeHtml(worstState)}</span>
</div>
</div>
<div class="grid">
<div class="field">
<div class="field-label">Start</div>
<div class="field-value">${escapeHtml(formatDate(item.startDate))}</div>
</div>
<div class="field">
<div class="field-label">Ende</div>
<div class="field-value">${escapeHtml(formatDate(item.endDate))}</div>
</div>
<div class="field">
<div class="field-label">Erstellt</div>
<div class="field-value">${escapeHtml(formatDate(item.createdAt))}</div>
</div>
<div class="field">
<div class="field-label">Produkte</div>
<div class="field-value">${escapeHtml(productCount)}</div>
</div>
</div>
${item.externalInfo ? `
<div class="external-info">
<strong>Hinweis:</strong><br />
${escapeHtml(item.externalInfo)}
</div>
` : ''}
</article>
`;
}).join('')}
</div>
`;
}
function render() {
const filtered = applyFilters(incidents);
renderSummary(incidents);
renderIncidents(filtered);
statusLineEl.textContent = `${filtered.length} von ${incidents.length} Incident(s) sichtbar · Letzte Aktualisierung: ${formatDate(new Date().toISOString())}`;
}
async function loadIncidents() {
reloadBtnEl.disabled = true;
statusLineEl.textContent = 'Lade Daten…';
try {
const response = await fetch(API_URL, {
headers: {
'Accept': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status} – ${response.statusText}`);
}
const data = await response.json();
if (!Array.isArray(data)) {
throw new Error('Unerwartetes API-Format: Es wurde kein Array zurückgegeben.');
}
incidents = data;
render();
} catch (error) {
console.error(error);
summaryEl.innerHTML = '';
contentEl.innerHTML = `
<div class="error">
<strong>Fehler beim Laden der DATEV-Statusdaten</strong><br /><br />
${escapeHtml(error.message)}<br /><br />
Prüfe ggf. CORS, Netzwerkzugriff oder einen vorgeschalteten Proxy.
</div>
`;
statusLineEl.textContent = 'Laden fehlgeschlagen';
} finally {
reloadBtnEl.disabled = false;
}
}
function updateAutoRefresh() {
const interval = Number(refreshIntervalEl.value);
if (timerId) {
clearInterval(timerId);
timerId = null;
}
if (interval > 0) {
timerId = setInterval(loadIncidents, interval);
}
}
searchInputEl.addEventListener('input', render);
stateFilterEl.addEventListener('change', render);
sortByEl.addEventListener('change', render);
refreshIntervalEl.addEventListener('change', updateAutoRefresh);
reloadBtnEl.addEventListener('click', loadIncidents);
updateAutoRefresh();
loadIncidents();
</script>
</body>
</html>
... Mehr anzeigen