Skip to content

Commit fa08e41

Browse files
authored
fix(health): address follow-up issues from health check system reviews (#3189)
* fix(health): use math.Inf(-1) for temperature max init Fixes Forgejo #739. maxTemp was initialized to 0, so environments with only negative sensor readings reported 0 instead of the actual maximum temperature. * fix(health): distinguish disabled health checks from no providers Fixes Forgejo #741. When notification providers are configured but health_check.enabled is false, the check now reports 'Notification health checks disabled' instead of the misleading 'No notification providers configured'. * feat(health): add EntriesSince to ErrorRingBuffer Returns cloned log entries with timestamps at or after a threshold. Preparation for enriching health check error reporting with grouped error details (Forgejo #736). * feat(health): enrich log checks with grouped error details When RecentErrorsCheck or ErrorTrendCheck reports warning/critical status, the Details map now includes a top_errors array grouping errors by (Component, Message), sorted by count, capped at 10. Addresses Forgejo #736. * i18n: add health check error detail labels for all 15 locales New keys under health.logs: topErrors, errorCount, errorComponent, errorLevel, errorMessage. Addresses Forgejo #736. * feat(health): add expandable error details to log checks Log-category health checks that report warning/critical status now show an expandable row with grouped error patterns including component, message, count, and level. Addresses Forgejo #736. * fix(health): address gate review findings - Replace inline SVG chevron with Lucide ChevronDown icon - Add aria-expanded attribute to expandable buttons - Wire up unused health.logs.topErrors i18n key as table heading - Use slices.SortFunc instead of sort.Slice for consistency - Preallocate EntriesSince result slice - Remove unnecessary order slice in groupErrors * fix(health): map error log level to error color, not warning Gemini review: error-level log entries should use var(--color-error) (red) for visual consistency. Also added explicit warn/warning cases.
1 parent 627e1b1 commit fa08e41

24 files changed

Lines changed: 514 additions & 51 deletions

frontend/src/lib/desktop/views/SystemHealth.svelte

Lines changed: 135 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
Clock,
1515
Loader2,
1616
Info,
17+
ChevronDown,
1718
} from '@lucide/svelte';
1819
import { onMount } from 'svelte';
1920
import { t } from '$lib/i18n';
@@ -71,6 +72,45 @@
7172
let error = $state<string | null>(null);
7273
let copied = $state(false);
7374
let copyTimer: ReturnType<typeof setTimeout> | null = null;
75+
let expandedChecks = $state(new Set<string>());
76+
77+
function toggleExpand(checkName: string) {
78+
const next = new Set(expandedChecks);
79+
if (next.has(checkName)) {
80+
next.delete(checkName);
81+
} else {
82+
next.add(checkName);
83+
}
84+
expandedChecks = next;
85+
}
86+
87+
interface ErrorGroup {
88+
component?: string;
89+
message: string;
90+
count: number;
91+
level: string;
92+
sample_fields?: Record<string, unknown>;
93+
}
94+
95+
function getTopErrors(result: DiagnosticsResult): ErrorGroup[] | null {
96+
const topErrors = result.details?.top_errors;
97+
if (!Array.isArray(topErrors) || topErrors.length === 0) return null;
98+
return topErrors as ErrorGroup[];
99+
}
100+
101+
function levelColor(level: string): string {
102+
switch (level) {
103+
case 'fatal':
104+
case 'panic':
105+
case 'error':
106+
return 'var(--color-error)';
107+
case 'warn':
108+
case 'warning':
109+
return 'var(--color-warning)';
110+
default:
111+
return 'var(--color-base-content)';
112+
}
113+
}
74114
75115
onMount(() => {
76116
return () => {
@@ -338,27 +378,102 @@
338378
>
339379
<div class="space-y-2">
340380
{#each results as result (result.name)}
341-
<div
342-
class="flex items-center gap-3 px-3 py-2.5 rounded-lg bg-[var(--color-base-200)]/50"
343-
>
344-
<StatusPill
345-
variant={statusToVariant(result.status)}
346-
label={t(`health.status.${result.status}`)}
347-
size="sm"
381+
{@const topErrors = getTopErrors(result)}
382+
{@const isExpandable =
383+
topErrors !== null && (result.status === 'warning' || result.status === 'critical')}
384+
{@const isExpanded = expandedChecks.has(result.name)}
385+
<div>
386+
<button
387+
type="button"
388+
class="flex items-center gap-3 w-full px-3 py-2.5 rounded-lg bg-[var(--color-base-200)]/50 text-left {isExpandable
389+
? 'cursor-pointer hover:bg-[var(--color-base-200)]'
390+
: 'cursor-default'}"
391+
onclick={() => isExpandable && toggleExpand(result.name)}
392+
disabled={!isExpandable}
393+
aria-expanded={isExpandable ? isExpanded : undefined}
348394
>
349-
{#snippet leadingIcon()}
350-
{@render statusIcon(result.status, 'size-3.5')}
351-
{/snippet}
352-
</StatusPill>
353-
<div class="flex-1 min-w-0">
354-
<span class="text-sm font-medium">{formatCheckName(result.name)}</span>
355-
<p class="text-xs text-[var(--color-base-content)] opacity-60 truncate">
356-
{result.message}
357-
</p>
358-
</div>
359-
<span class="text-xs text-[var(--color-base-content)] opacity-40 shrink-0">
360-
{result.duration_ms.toFixed(1)}ms
361-
</span>
395+
<StatusPill
396+
variant={statusToVariant(result.status)}
397+
label={t(`health.status.${result.status}`)}
398+
size="sm"
399+
>
400+
{#snippet leadingIcon()}
401+
{@render statusIcon(result.status, 'size-3.5')}
402+
{/snippet}
403+
</StatusPill>
404+
<div class="flex-1 min-w-0">
405+
<span class="text-sm font-medium">{formatCheckName(result.name)}</span>
406+
<p class="text-xs text-[var(--color-base-content)] opacity-60 truncate">
407+
{result.message}
408+
</p>
409+
</div>
410+
<span class="text-xs text-[var(--color-base-content)] opacity-40 shrink-0">
411+
{result.duration_ms.toFixed(1)}ms
412+
</span>
413+
{#if isExpandable}
414+
<ChevronDown
415+
class="size-4 shrink-0 opacity-40 transition-transform {isExpanded
416+
? 'rotate-180'
417+
: ''}"
418+
/>
419+
{/if}
420+
</button>
421+
422+
{#if isExpanded && topErrors}
423+
<div
424+
class="mt-1 ml-3 mr-3 mb-2 rounded-lg bg-[var(--color-base-200)]/30 overflow-hidden"
425+
>
426+
<p
427+
class="px-3 py-1.5 text-xs font-medium text-[var(--color-base-content)] opacity-60"
428+
>
429+
{t('health.logs.topErrors')}
430+
</p>
431+
<table class="w-full text-xs">
432+
<thead>
433+
<tr
434+
class="text-left text-[var(--color-base-content)] opacity-50 border-b border-[var(--color-base-200)]"
435+
>
436+
<th class="px-3 py-1.5 font-medium">{t('health.logs.errorComponent')}</th>
437+
<th class="px-3 py-1.5 font-medium">{t('health.logs.errorMessage')}</th>
438+
<th class="px-3 py-1.5 font-medium text-right"
439+
>{t('health.logs.errorCount')}</th
440+
>
441+
<th class="px-3 py-1.5 font-medium">{t('health.logs.errorLevel')}</th>
442+
</tr>
443+
</thead>
444+
<tbody>
445+
{#each topErrors as group (group.component + ':' + group.message)}
446+
<tr class="border-b border-[var(--color-base-200)]/50 last:border-0">
447+
<td class="px-3 py-1.5">
448+
{#if group.component}
449+
<span
450+
class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-[var(--color-base-300)] text-[var(--color-base-content)]"
451+
>
452+
{group.component}
453+
</span>
454+
{:else}
455+
<span class="opacity-30">-</span>
456+
{/if}
457+
</td>
458+
<td class="px-3 py-1.5 max-w-[300px] truncate" title={group.message}>
459+
{group.message}
460+
</td>
461+
<td class="px-3 py-1.5 text-right font-mono">{group.count}</td>
462+
<td class="px-3 py-1.5">
463+
<span
464+
class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium"
465+
style:color={levelColor(group.level)}
466+
style:background="color-mix(in srgb, {levelColor(group.level)} 10%, transparent)"
467+
>
468+
{group.level}
469+
</span>
470+
</td>
471+
</tr>
472+
{/each}
473+
</tbody>
474+
</table>
475+
</div>
476+
{/if}
362477
</div>
363478
{/each}
364479
</div>

frontend/static/messages/cs.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4818,6 +4818,13 @@
48184818
"timeLabel": "Time",
48194819
"durationLabel": "Duration",
48204820
"checksLabel": "Checks"
4821+
},
4822+
"logs": {
4823+
"topErrors": "Nejčastější chyby",
4824+
"errorCount": "Počet",
4825+
"errorComponent": "Komponenta",
4826+
"errorLevel": "Úroveň",
4827+
"errorMessage": "Zpráva"
48214828
}
48224829
}
48234830
}

frontend/static/messages/da.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4818,6 +4818,13 @@
48184818
"timeLabel": "Time",
48194819
"durationLabel": "Duration",
48204820
"checksLabel": "Checks"
4821+
},
4822+
"logs": {
4823+
"topErrors": "Hyppigste fejl",
4824+
"errorCount": "Antal",
4825+
"errorComponent": "Komponent",
4826+
"errorLevel": "Niveau",
4827+
"errorMessage": "Besked"
48214828
}
48224829
}
48234830
}

frontend/static/messages/de.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4818,6 +4818,13 @@
48184818
"timeLabel": "Time",
48194819
"durationLabel": "Duration",
48204820
"checksLabel": "Checks"
4821+
},
4822+
"logs": {
4823+
"topErrors": "Häufigste Fehler",
4824+
"errorCount": "Anzahl",
4825+
"errorComponent": "Komponente",
4826+
"errorLevel": "Stufe",
4827+
"errorMessage": "Nachricht"
48214828
}
48224829
}
48234830
}

frontend/static/messages/en.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4818,6 +4818,13 @@
48184818
"timeLabel": "Time",
48194819
"durationLabel": "Duration",
48204820
"checksLabel": "Checks"
4821+
},
4822+
"logs": {
4823+
"topErrors": "Top error patterns",
4824+
"errorCount": "Count",
4825+
"errorComponent": "Component",
4826+
"errorLevel": "Level",
4827+
"errorMessage": "Message"
48214828
}
48224829
}
48234830
}

frontend/static/messages/es.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4818,6 +4818,13 @@
48184818
"timeLabel": "Time",
48194819
"durationLabel": "Duration",
48204820
"checksLabel": "Checks"
4821+
},
4822+
"logs": {
4823+
"topErrors": "Errores frecuentes",
4824+
"errorCount": "Cantidad",
4825+
"errorComponent": "Componente",
4826+
"errorLevel": "Nivel",
4827+
"errorMessage": "Mensaje"
48214828
}
48224829
}
48234830
}

frontend/static/messages/fi.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4818,6 +4818,13 @@
48184818
"timeLabel": "Time",
48194819
"durationLabel": "Duration",
48204820
"checksLabel": "Checks"
4821+
},
4822+
"logs": {
4823+
"topErrors": "Yleisimmät virheet",
4824+
"errorCount": "Lukumäärä",
4825+
"errorComponent": "Komponentti",
4826+
"errorLevel": "Taso",
4827+
"errorMessage": "Viesti"
48214828
}
48224829
}
48234830
}

frontend/static/messages/fr.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4818,6 +4818,13 @@
48184818
"timeLabel": "Time",
48194819
"durationLabel": "Duration",
48204820
"checksLabel": "Checks"
4821+
},
4822+
"logs": {
4823+
"topErrors": "Erreurs fréquentes",
4824+
"errorCount": "Nombre",
4825+
"errorComponent": "Composant",
4826+
"errorLevel": "Niveau",
4827+
"errorMessage": "Message"
48214828
}
48224829
}
48234830
}

frontend/static/messages/hu.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4818,6 +4818,13 @@
48184818
"timeLabel": "Time",
48194819
"durationLabel": "Duration",
48204820
"checksLabel": "Checks"
4821+
},
4822+
"logs": {
4823+
"topErrors": "Leggyakoribb hibák",
4824+
"errorCount": "Darab",
4825+
"errorComponent": "Komponens",
4826+
"errorLevel": "Szint",
4827+
"errorMessage": "Üzenet"
48214828
}
48224829
}
48234830
}

frontend/static/messages/it.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4818,6 +4818,13 @@
48184818
"timeLabel": "Time",
48194819
"durationLabel": "Duration",
48204820
"checksLabel": "Checks"
4821+
},
4822+
"logs": {
4823+
"topErrors": "Errori frequenti",
4824+
"errorCount": "Conteggio",
4825+
"errorComponent": "Componente",
4826+
"errorLevel": "Livello",
4827+
"errorMessage": "Messaggio"
48214828
}
48224829
}
48234830
}

0 commit comments

Comments
 (0)