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
+2 -1
View File
@@ -7,7 +7,7 @@
* (`--tile`, `--tile-foreground`, `--primary`, `--primary-foreground`).
*/
export type ModuleIconId = 'radar' | 'fuel' | 'certificate' | 'globe' | 'server' | 'utensils' | 'shopping-bag' | 'tile';
export type ModuleIconId = 'radar' | 'fuel' | 'certificate' | 'globe' | 'server' | 'utensils' | 'shopping-bag' | 'cloud' | 'tile';
const ICONS: Record<string, ModuleIconId> = {
'tender-radar': 'radar',
@@ -15,6 +15,7 @@ const ICONS: Record<string, ModuleIconId> = {
'cert-manager': 'certificate',
domaincheck: 'globe',
proxmox: 'server',
'nextcloud-status': 'cloud',
'kantine-datev': 'utensils',
'handelsware-datev': 'shopping-bag',
};
+6
View File
@@ -59,6 +59,12 @@ export const MODULE_REGISTRY: Record<string, ModuleRegistryEntry> = {
{ ssr: false },
),
},
'nextcloud-status': {
component: dynamic(
() => import('@/app/(portal)/modules/nextcloud-status/page'),
{ ssr: false },
),
},
'kantine-datev': {
component: dynamic(
() => import('@/app/(portal)/modules/kantine-datev/page'),
+86
View File
@@ -0,0 +1,86 @@
/**
* Nextcloud-Status Module API client (quick-261002-k67). Konsumiert
* `/modules/nextcloud-status/*`. Vorbild `proxmox-api.ts`:
* `credentials: 'include'` fuer Cookie-Auth, `NEXT_PUBLIC_API_URL` als Basis.
*
* Die Antworten tragen nur ausgewertete Felder — nie einen rohen
* Antworttext einer Cloud und nie Logo-Bytes (die kommen ueber die
* eigene Logo-Adresse, siehe `logoSrc`).
*/
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
export type RatingLevel = 'green' | 'yellow' | 'red' | 'unknown';
export type RatingReason =
| 'current'
| 'update-available'
| 'eol-soon'
| 'eol-passed'
| 'unreachable'
| 'invalid-response'
| 'maintenance'
| 'needs-db-upgrade'
| 'no-reference'
| 'version-unknown'
| 'not-checked';
export interface NextcloudRating {
level: RatingLevel;
reason: RatingReason;
/** Neuester Patchstand im eigenen Zweig, nur wenn ein Update ansteht. */
updateTo: string | null;
/** Support-Ende des Zweigs als 'YYYY-MM-DD', wenn bekannt. */
eolDate: string | null;
cycle: number | null;
}
export interface NextcloudInstanceStatus {
checkedAt: string | null;
reachable: boolean | null;
maintenance: boolean | null;
needsDbUpgrade: boolean | null;
versionString: string | null;
edition: string | null;
errorKind: string | null;
errorDetail: string | null;
}
export interface NextcloudInstance {
id: string;
customerName: string;
baseUrl: string;
logoUrl: string | null;
hasUploadedLogo: boolean;
logoVersion: number;
status: NextcloudInstanceStatus;
rating: NextcloudRating;
}
export interface NextcloudList {
instances: NextcloudInstance[];
reference: { newestVersion: string | null; fetchedAt: string | null };
}
/** GET /modules/nextcloud-status/instances — Clouds samt Bewertung und neuester Version. */
export async function listInstances(): Promise<NextcloudList> {
const res = await fetch(`${API_URL}/modules/nextcloud-status/instances`, {
credentials: 'include',
});
if (!res.ok) throw new Error('Failed to fetch nextcloud instances');
return res.json();
}
/**
* Bildquelle fuer das Logo einer Cloud: ein hochgeladenes Logo kommt ueber den
* same-origin-Proxy (angemeldet, mit `?v=` gegen alte Zwischenspeicher), eine
* Bildadresse laedt der Browser direkt; ohne beides `null` (Initialen).
*/
export function logoSrc(
instance: Pick<NextcloudInstance, 'id' | 'hasUploadedLogo' | 'logoVersion' | 'logoUrl'>,
): string | null {
if (instance.hasUploadedLogo) {
return `/api-proxy/modules/nextcloud-status/instances/${instance.id}/logo?v=${instance.logoVersion}`;
}
return instance.logoUrl ?? null;
}
+1
View File
@@ -25,6 +25,7 @@ export const useNavStore = create<NavState>()((set) => ({
/** Ersatztitel, solange die Seitenleiste die Module noch nicht geladen hat. */
const MODULE_TITLE_KEYS: Record<string, string> = {
proxmox: 'proxmox.title',
'nextcloud-status': 'nextcloudStatus.title',
domaincheck: 'domaincheck.title',
'cert-manager': 'certManager.title',
'dkv-fleet': 'dkvFleet.pageTitle',