feat(nextcloud-status): Modul mit Statusabruf, Versionsbewertung und Kachelansicht

- NextcloudInstance mit Zeilenschutz, Migration 20261002150000
- Bewertung (Ampel) als reine Funktion, status.php-Abruf mit Grenzen, endoflife.date-Zwischenspeicher
- API: Liste mit Bewertung, Anlegen und Einzelprüfung nur mit Verwalten
- Modulseite mit Kacheln, Registrierung in Seitenleiste, Marktplatz und Symbolen

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-02 14:49:02 +02:00
parent 7188733f70
commit 5ef7b0c6cc
31 changed files with 2513 additions and 2 deletions
@@ -57,6 +57,7 @@ const GLYPHS: Record<ModuleIconId, ReactNode> = {
<path d="M3.4 5.467a2 2 0 0 0-.4 1.2V20a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6.667a2 2 0 0 0-.4-1.2l-2-2.667A2 2 0 0 0 17 2H7a2 2 0 0 0-1.6.8z" />
</>
),
cloud: <path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" />,
tile: (
<>
<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" />
@@ -0,0 +1,81 @@
import type { NextcloudRating, RatingLevel } from '@/lib/nextcloud-status-api';
/**
* Darstellung der Ampel (quick-261002-k67), gemeinsam fuer Modulseite und
* Dashboard-Kachel. Bewusst vollstaendige, statische Klassenzeichenketten —
* Tailwind findet Klassen nur, wenn sie woertlich im Quelltext stehen.
*
* `fill` = Flaeche (Leiste, Punkt), `pill` = Pille (12 % Flaeche + Textvariante),
* `text` = Schrift in Statusfarbe.
*/
export const RATING_STYLE: Record<RatingLevel, { fill: string; pill: string; text: string }> = {
green: {
fill: 'bg-status-ok',
pill: 'bg-status-ok/12 text-status-ok-fg',
text: 'text-status-ok-fg',
},
yellow: {
fill: 'bg-status-warn',
pill: 'bg-status-warn/12 text-status-warn-fg',
text: 'text-status-warn-fg',
},
red: {
fill: 'bg-status-down',
pill: 'bg-status-down/12 text-status-down-fg',
text: 'text-status-down-fg',
},
unknown: {
fill: 'bg-status-idle',
pill: 'bg-status-idle/12 text-status-idle-fg',
text: 'text-status-idle-fg',
},
};
type Translate = (key: string, values?: Record<string, string | number>) => string;
/** 'YYYY-MM-DD' als Datum nach Sprache, in UTC (kein Tagesversatz durch die Zeitzone). */
export function formatEolDate(isoDay: string, locale: string): string {
const date = new Date(`${isoDay}T00:00:00Z`);
if (Number.isNaN(date.getTime())) return isoDay;
return new Intl.DateTimeFormat(locale, {
timeZone: 'UTC',
day: '2-digit',
month: '2-digit',
year: 'numeric',
}).format(date);
}
/**
* Kurzer Klartext zur Bewertung. `t` ist der Uebersetzer des Namensraums
* `nextcloudStatus`; die API liefert nur Grundkennung plus Parameter (D-B).
*/
export function ratingReasonText(t: Translate, rating: NextcloudRating, locale: string): string {
switch (rating.reason) {
case 'current':
return t('reason.current');
case 'update-available':
return t('reason.updateAvailable', { version: rating.updateTo ?? '' });
case 'eol-soon':
return t('reason.eolSoon', {
date: rating.eolDate ? formatEolDate(rating.eolDate, locale) : '',
});
case 'eol-passed':
return rating.eolDate
? t('reason.eolPassed', { date: formatEolDate(rating.eolDate, locale) })
: t('reason.eolPassedNoDate');
case 'unreachable':
return t('reason.unreachable');
case 'invalid-response':
return t('reason.invalidResponse');
case 'maintenance':
return t('reason.maintenance');
case 'needs-db-upgrade':
return t('reason.needsDbUpgrade');
case 'no-reference':
return t('reason.noReference');
case 'version-unknown':
return t('reason.versionUnknown');
case 'not-checked':
return t('reason.notChecked');
}
}