feat(monitoring): add priority screenshot pipeline with screenshot_type + docs cleanup

Implement end-to-end support for typed screenshots and priority rendering in monitoring.

Added
- Accept and forward screenshot_type from MQTT screenshot/dashboard payloads
  (periodic, event_start, event_stop)
- Extend screenshot upload handling to persist typed screenshots and metadata
- Add dedicated priority screenshot serving endpoint with fallback behavior
- Extend monitoring overview with priority screenshot fields and summary count
- Add configurable PRIORITY_SCREENSHOT_TTL_SECONDS window for active priority state

Fixed
- Ensure screenshot cache-busting updates reliably via screenshot hash updates
- Preserve normal periodic screenshot flow while introducing event_start/event_stop priority path

Improved
- Monitoring dashboard now displays screenshot type badges
- Adaptive polling: faster refresh while priority screenshots are active
- Priority screenshot presentation is surfaced immediately to operators

Docs
- Update README and copilot-instructions to match new screenshot_type behavior,
  priority endpoint, TTL config, monitoring fields, and retention model
- Remove redundant/duplicate documentation blocks and improve troubleshooting section clarity
This commit is contained in:
2026-03-29 13:13:13 +00:00
parent 9c330f984f
commit 24cdf07279
10 changed files with 258 additions and 57 deletions

View File

@@ -26,6 +26,10 @@ export interface MonitoringClient {
screenHealthStatus?: string | null;
lastScreenshotAnalyzed?: string | null;
lastScreenshotHash?: string | null;
latestScreenshotType?: 'periodic' | 'event_start' | 'event_stop' | null;
priorityScreenshotType?: 'event_start' | 'event_stop' | null;
priorityScreenshotReceivedAt?: string | null;
hasActivePriorityScreenshot?: boolean;
screenshotUrl: string;
logCounts24h: {
error: number;
@@ -47,6 +51,7 @@ export interface MonitoringOverview {
criticalClients: number;
errorLogs: number;
warnLogs: number;
activePriorityScreenshots: number;
};
periodHours: number;
gracePeriodSeconds: number;

View File

@@ -194,6 +194,32 @@
margin-top: 0.55rem;
font-size: 0.88rem;
color: #64748b;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.monitoring-shot-type {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 0.15rem 0.55rem;
font-size: 0.78rem;
font-weight: 700;
}
.monitoring-shot-type-periodic {
background: #e2e8f0;
color: #334155;
}
.monitoring-shot-type-event {
background: #ffedd5;
color: #9a3412;
}
.monitoring-shot-type-active {
box-shadow: 0 0 0 2px #fdba74;
}
.monitoring-error-box {

View File

@@ -25,6 +25,7 @@ import { DialogComponent } from '@syncfusion/ej2-react-popups';
import './monitoring.css';
const REFRESH_INTERVAL_MS = 15000;
const PRIORITY_REFRESH_INTERVAL_MS = 3000;
const hourOptions = [
{ text: 'Letzte 6 Stunden', value: 6 },
@@ -95,6 +96,19 @@ function statusBadge(status: string) {
);
}
function screenshotTypeBadge(type?: string | null, hasPriority = false) {
const normalized = (type || 'periodic').toLowerCase();
const map: Record<string, { label: string; className: string }> = {
periodic: { label: 'Periodisch', className: 'monitoring-shot-type-periodic' },
event_start: { label: 'Event-Start', className: 'monitoring-shot-type-event' },
event_stop: { label: 'Event-Stopp', className: 'monitoring-shot-type-event' },
};
const info = map[normalized] || map.periodic;
const classes = `monitoring-shot-type ${info.className}${hasPriority ? ' monitoring-shot-type-active' : ''}`;
return <span className={classes}>{info.label}</span>;
}
function renderMetricCard(title: string, value: number, subtitle: string, accent: string) {
return (
<div className="e-card monitoring-metric-card" style={{ borderTop: `4px solid ${accent}` }}>
@@ -188,12 +202,14 @@ const MonitoringDashboard: React.FC = () => {
}, [hours, loadOverview]);
React.useEffect(() => {
const hasActivePriorityScreenshots = (overview?.summary.activePriorityScreenshots || 0) > 0;
const intervalMs = hasActivePriorityScreenshots ? PRIORITY_REFRESH_INTERVAL_MS : REFRESH_INTERVAL_MS;
const intervalId = window.setInterval(() => {
loadOverview(hours);
}, REFRESH_INTERVAL_MS);
}, intervalMs);
return () => window.clearInterval(intervalId);
}, [hours, loadOverview]);
}, [hours, loadOverview, overview?.summary.activePriorityScreenshots]);
React.useEffect(() => {
if (!selectedClientUuid) {
@@ -288,6 +304,7 @@ const MonitoringDashboard: React.FC = () => {
{renderMetricCard('Warnungen', overview?.summary.warningClients || 0, 'Warn-Logs oder Übergangszustände', '#d97706')}
{renderMetricCard('Kritisch', overview?.summary.criticalClients || 0, 'Crashs oder Fehler-Logs', '#dc2626')}
{renderMetricCard('Offline', overview?.summary.offlineClients || 0, 'Keine frischen Signale', '#475569')}
{renderMetricCard('Prioritäts-Screens', overview?.summary.activePriorityScreenshots || 0, 'Event-Start/Stop aktiv', '#ea580c')}
{renderMetricCard('Fehler-Logs', overview?.summary.errorLogs || 0, 'Im gewählten Zeitraum', '#b91c1c')}
</div>
@@ -380,6 +397,21 @@ const MonitoringDashboard: React.FC = () => {
<span>Letzte Analyse</span>
<strong>{formatTimestamp(selectedClient.lastScreenshotAnalyzed)}</strong>
</div>
<div className="monitoring-detail-row">
<span>Screenshot-Typ</span>
<strong>
{screenshotTypeBadge(
selectedClient.latestScreenshotType,
!!selectedClient.hasActivePriorityScreenshot
)}
</strong>
</div>
{selectedClient.priorityScreenshotReceivedAt && (
<div className="monitoring-detail-row">
<span>Priorität empfangen</span>
<strong>{formatTimestamp(selectedClient.priorityScreenshotReceivedAt)}</strong>
</div>
)}
</div>
) : (
<MessageComponent severity="Info" content="Wählen Sie links einen Client aus." />
@@ -407,7 +439,14 @@ const MonitoringDashboard: React.FC = () => {
/>
)}
<div className="monitoring-screenshot-meta">
Empfangen: {formatTimestamp(selectedClient.lastScreenshotAnalyzed)}
<span>Empfangen: {formatTimestamp(selectedClient.lastScreenshotAnalyzed)}</span>
<span>
Typ:{' '}
{screenshotTypeBadge(
selectedClient.latestScreenshotType,
!!selectedClient.hasActivePriorityScreenshot
)}
</span>
</div>
</>
) : (