feat(nextcloud-status): erneute Prüfung nach Ausfall und Hinweis auf der Kachel
- Wiederholungsauftrag jede Minute für Clouds mit einem Fehlschlag älter als fünf Minuten - Neue Adresse setzt den Prüfstand zurück, der gemeldete Zustand bleibt - Kachel: Hinweis Prüfung fehlgeschlagen, Fehlercodes als lesbarer Text mit Tooltip Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useLocale, useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import { errorHint } from '@/components/nextcloud-status/error-hint';
|
||||
import { RATING_STYLE, ratingReasonText } from '@/components/nextcloud-status/rating-display';
|
||||
import { formatAge } from '@/components/proxmox/proxmox-status';
|
||||
import { logoSrc, type NextcloudInstance } from '@/lib/nextcloud-status-api';
|
||||
@@ -55,10 +56,15 @@ export function CloudTile({
|
||||
const reason = ratingReasonText(t, instance.rating, locale);
|
||||
const src = logoFailed ? null : logoSrc(instance);
|
||||
const checkedAge = formatAge(instance.status.checkedAt, now, locale);
|
||||
const showErrorDetail =
|
||||
// Fehlerhinweis in Klartext; die Rohkennung bleibt als Tooltip erhalten.
|
||||
const hint =
|
||||
instance.rating.level === 'red' &&
|
||||
instance.rating.reason === 'unreachable' &&
|
||||
instance.status.errorDetail;
|
||||
(instance.status.errorKind || instance.status.errorDetail)
|
||||
? errorHint(instance.status.errorKind, instance.status.errorDetail)
|
||||
: null;
|
||||
const hintText = hint ? t(`errorHint.${hint.key}`, { code: hint.code ?? '' }) : null;
|
||||
const hintRaw = instance.status.errorDetail ?? instance.status.errorKind ?? undefined;
|
||||
|
||||
return (
|
||||
<article
|
||||
@@ -198,8 +204,24 @@ export function CloudTile({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{showErrorDetail && (
|
||||
<p className="break-words text-xs text-muted-foreground">{instance.status.errorDetail}</p>
|
||||
{hintText && (
|
||||
<p
|
||||
data-testid="error-hint"
|
||||
className="break-words text-xs text-muted-foreground"
|
||||
title={hintRaw}
|
||||
>
|
||||
{hintText}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{instance.status.pendingRetry === true && (
|
||||
<p
|
||||
data-testid="pending-retry"
|
||||
className="flex items-center gap-1.5 text-xs text-muted-foreground"
|
||||
>
|
||||
<span aria-hidden="true" className="h-1.5 w-1.5 shrink-0 rounded-full bg-status-warn" />
|
||||
{t('card.pendingRetry')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div
|
||||
|
||||
@@ -378,4 +378,62 @@ describe('NextcloudStatusPage', () => {
|
||||
).toHaveAttribute('aria-pressed', 'true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Hinweise auf der Kachel (quick-261002-kxc)', () => {
|
||||
const redWith = (errorKind: string | null, errorDetail: string | null): NextcloudInstance =>
|
||||
makeInstance({
|
||||
id: 'r',
|
||||
customerName: 'Kunde R',
|
||||
status: {
|
||||
checkedAt: new Date().toISOString(),
|
||||
reachable: false,
|
||||
maintenance: null,
|
||||
needsDbUpgrade: null,
|
||||
versionString: null,
|
||||
edition: null,
|
||||
errorKind,
|
||||
errorDetail,
|
||||
},
|
||||
rating: { level: 'red', reason: 'unreachable', updateTo: null, eolDate: null, cycle: null },
|
||||
});
|
||||
|
||||
const renderOne = async (instance: NextcloudInstance) => {
|
||||
mockListInstances.mockResolvedValue({ ...LIST, instances: [instance] });
|
||||
render(<NextcloudStatusPage />);
|
||||
return screen.findByTestId('cloud-tile');
|
||||
};
|
||||
|
||||
it('zeigt "Prüfung fehlgeschlagen" bei pendingRetry, Pille und Version behalten den guten Stand', async () => {
|
||||
const base = makeInstance({ id: 'p', customerName: 'Kunde P' });
|
||||
const tile = await renderOne({ ...base, status: { ...base.status, pendingRetry: true } });
|
||||
expect(within(tile).getByTestId('pending-retry')).toHaveTextContent(
|
||||
'Prüfung fehlgeschlagen, wird in wenigen Minuten wiederholt',
|
||||
);
|
||||
expect(tile).toHaveAttribute('data-level', 'green');
|
||||
expect(within(tile).getByText('Aktuell')).toBeInTheDocument();
|
||||
expect(within(tile).getByText('35.0.1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('zeigt den Hinweis nicht ohne pendingRetry', async () => {
|
||||
const tile = await renderOne(makeInstance({ id: 'q', customerName: 'Kunde Q' }));
|
||||
expect(within(tile).queryByTestId('pending-retry')).toBeNull();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['tls', 'ERR_TLS_CERT_ALTNAME_INVALID', 'Zertifikat passt nicht zur Adresse'],
|
||||
['tls', 'CERT_HAS_EXPIRED', 'Zertifikat abgelaufen'],
|
||||
['tls', 'DEPTH_ZERO_SELF_SIGNED_CERT', 'Zertifikat nicht vertrauenswürdig'],
|
||||
['network', 'ENOTFOUND', 'Adresse nicht gefunden'],
|
||||
['network', 'ECONNREFUSED', 'Verbindung abgelehnt'],
|
||||
['timeout', null, 'Zeitüberschreitung'],
|
||||
['http-status', 'HTTP 502', 'Server antwortet mit Fehler 502'],
|
||||
['network', 'ECONNRESET', 'Verbindungsfehler'],
|
||||
])('„Nicht erreichbar“ zeigt %s / %s als „%s“, die Rohkennung als Tooltip', async (kind, detail, text) => {
|
||||
const tile = await renderOne(redWith(kind, detail));
|
||||
const hint = within(tile).getByTestId('error-hint');
|
||||
expect(hint).toHaveTextContent(text);
|
||||
expect(hint).toHaveAttribute('title', detail ?? kind);
|
||||
expect(within(tile).queryByText(detail ?? '__nichts__')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user