From f66546df7e6118d26ac43ca7753ab93c073576f8 Mon Sep 17 00:00:00 2001 From: Schalli Date: Thu, 6 Aug 2026 16:37:07 +0200 Subject: [PATCH] feat(16-05): wire full sync report + visible sync-request error (D-06, D-21) - SyncResult interface grows additively: groupMembershipsAdded/Removed (D-21 backend gap, existed since Phase 15 but never wired into the frontend) plus groupsAdopted/Renamed/Deleted and defaultMarkerMoved (Plan 16-03). - New syncRequestError state: a failed sync request (network error or !res.ok) now renders a visible text-sm text-destructive line instead of silently reporting a three-zero result as a successful no-op. - Sync report container gains three new lines: group-membership counts, AD-group counts (always visible, even at zero), and a conditional amber "default marker reassigned" line shown only when defaultMarkerMoved > 0. - Four new i18n keys under admin.ldap.sync in de.json/en.json. --- apps/web/src/app/(portal)/admin/ldap/page.tsx | 43 ++++++++++++++++++- apps/web/src/messages/de.json | 6 ++- apps/web/src/messages/en.json | 6 ++- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/(portal)/admin/ldap/page.tsx b/apps/web/src/app/(portal)/admin/ldap/page.tsx index 3c30021..20688a1 100644 --- a/apps/web/src/app/(portal)/admin/ldap/page.tsx +++ b/apps/web/src/app/(portal)/admin/ldap/page.tsx @@ -63,6 +63,15 @@ interface SyncResult { created: number; updated: number; deactivated: number; + // D-21: group-membership reconciliation counters — existed on the backend + // since Plan 15/D-21 but were never wired into this interface until now. + groupMembershipsAdded: number; + groupMembershipsRemoved: number; + // Plan 16-03/16-05: group reconciliation counters (SC-3/SC-4/D-05/D-06). + groupsAdopted: number; + groupsRenamed: number; + groupsDeleted: number; + defaultMarkerMoved: number; errors: string[]; } @@ -84,6 +93,7 @@ export default function AdminLdapPage() { } | null>(null); const [syncResult, setSyncResult] = useState(null); const [syncing, setSyncing] = useState(false); + const [syncRequestError, setSyncRequestError] = useState(null); // Form state for connection settings. const [formData, setFormData] = useState({ @@ -265,6 +275,7 @@ export default function AdminLdapPage() { const handleSync = async () => { setSyncing(true); setSyncResult(null); + setSyncRequestError(null); try { const res = await fetch(`${API_URL}/ldap/sync`, { method: 'POST', @@ -274,9 +285,13 @@ export default function AdminLdapPage() { const data = await res.json(); setSyncResult(data); await fetchConfig(); + } else { + setSyncResult(null); + setSyncRequestError(t('sync.requestError')); } } catch { - setSyncResult({ created: 0, updated: 0, deactivated: 0, errors: ['Network error'] }); + setSyncResult(null); + setSyncRequestError(t('sync.requestError')); } finally { setSyncing(false); } @@ -1287,6 +1302,12 @@ export default function AdminLdapPage() { {syncing ? t('sync.syncing') : t('sync.trigger')} + {/* Sync request failure (Owner-Entscheidung 2026-08-06): a failed + sync request must never render as a report full of zeros. */} + {syncRequestError && ( +

{syncRequestError}

+ )} + {/* Sync result display */} {syncResult && (
@@ -1297,6 +1318,26 @@ export default function AdminLdapPage() { deactivated: syncResult.deactivated, })}

+

+ {t('sync.resultGroupMemberships', { + groupMembershipsAdded: syncResult.groupMembershipsAdded, + groupMembershipsRemoved: syncResult.groupMembershipsRemoved, + })} +

+

+ {t('sync.resultGroups', { + groupsAdopted: syncResult.groupsAdopted, + groupsRenamed: syncResult.groupsRenamed, + groupsDeleted: syncResult.groupsDeleted, + })} +

+ {syncResult.defaultMarkerMoved > 0 && ( +

+ {t('sync.defaultMarkerMoved', { + defaultMarkerMoved: syncResult.defaultMarkerMoved, + })} +

+ )} {syncResult.errors.length > 0 && (
{syncResult.errors.map((err, i) => ( diff --git a/apps/web/src/messages/de.json b/apps/web/src/messages/de.json index e2a33c0..cb3d77b 100644 --- a/apps/web/src/messages/de.json +++ b/apps/web/src/messages/de.json @@ -329,7 +329,11 @@ "neverSynced": "Noch nie synchronisiert", "trigger": "Jetzt synchronisieren", "syncing": "Synchronisiere...", - "result": "Erstellt: {created}, aktualisiert: {updated}, deaktiviert: {deactivated}" + "result": "Erstellt: {created}, aktualisiert: {updated}, deaktiviert: {deactivated}", + "resultGroupMemberships": "Gruppenmitgliedschaften: {groupMembershipsAdded} hinzugefügt, {groupMembershipsRemoved} entfernt", + "resultGroups": "AD-Gruppen: {groupsAdopted} neu übernommen, {groupsRenamed} umbenannt, {groupsDeleted} gelöscht", + "defaultMarkerMoved": "Standardgruppen-Markierung musste neu vergeben werden ({defaultMarkerMoved}×).", + "requestError": "Die Synchronisation konnte nicht ausgeführt werden." }, "groupFilter": { "title": "Import-Filter (Gruppen/OUs)", diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index 9bed80f..4c723ee 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -329,7 +329,11 @@ "neverSynced": "Never synced", "trigger": "Sync now", "syncing": "Syncing...", - "result": "Created: {created}, updated: {updated}, deactivated: {deactivated}" + "result": "Created: {created}, updated: {updated}, deactivated: {deactivated}", + "resultGroupMemberships": "Group memberships: {groupMembershipsAdded} added, {groupMembershipsRemoved} removed", + "resultGroups": "AD groups: {groupsAdopted} newly adopted, {groupsRenamed} renamed, {groupsDeleted} deleted", + "defaultMarkerMoved": "Default-group marker had to be reassigned ({defaultMarkerMoved}×).", + "requestError": "The synchronization could not be run." }, "groupFilter": { "title": "Import filter (groups/OUs)",