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.
This commit is contained in:
@@ -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<SyncResult | null>(null);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [syncRequestError, setSyncRequestError] = useState<string | null>(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')}
|
||||
</button>
|
||||
|
||||
{/* Sync request failure (Owner-Entscheidung 2026-08-06): a failed
|
||||
sync request must never render as a report full of zeros. */}
|
||||
{syncRequestError && (
|
||||
<p className="text-sm text-destructive">{syncRequestError}</p>
|
||||
)}
|
||||
|
||||
{/* Sync result display */}
|
||||
{syncResult && (
|
||||
<div className="rounded-md border border-border bg-muted/30 p-4">
|
||||
@@ -1297,6 +1318,26 @@ export default function AdminLdapPage() {
|
||||
deactivated: syncResult.deactivated,
|
||||
})}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('sync.resultGroupMemberships', {
|
||||
groupMembershipsAdded: syncResult.groupMembershipsAdded,
|
||||
groupMembershipsRemoved: syncResult.groupMembershipsRemoved,
|
||||
})}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('sync.resultGroups', {
|
||||
groupsAdopted: syncResult.groupsAdopted,
|
||||
groupsRenamed: syncResult.groupsRenamed,
|
||||
groupsDeleted: syncResult.groupsDeleted,
|
||||
})}
|
||||
</p>
|
||||
{syncResult.defaultMarkerMoved > 0 && (
|
||||
<p className="text-sm font-medium text-amber-700 dark:text-amber-400">
|
||||
{t('sync.defaultMarkerMoved', {
|
||||
defaultMarkerMoved: syncResult.defaultMarkerMoved,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
{syncResult.errors.length > 0 && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{syncResult.errors.map((err, i) => (
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
Reference in New Issue
Block a user