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;
|
created: number;
|
||||||
updated: number;
|
updated: number;
|
||||||
deactivated: 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[];
|
errors: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +93,7 @@ export default function AdminLdapPage() {
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [syncResult, setSyncResult] = useState<SyncResult | null>(null);
|
const [syncResult, setSyncResult] = useState<SyncResult | null>(null);
|
||||||
const [syncing, setSyncing] = useState(false);
|
const [syncing, setSyncing] = useState(false);
|
||||||
|
const [syncRequestError, setSyncRequestError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Form state for connection settings.
|
// Form state for connection settings.
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
@@ -265,6 +275,7 @@ export default function AdminLdapPage() {
|
|||||||
const handleSync = async () => {
|
const handleSync = async () => {
|
||||||
setSyncing(true);
|
setSyncing(true);
|
||||||
setSyncResult(null);
|
setSyncResult(null);
|
||||||
|
setSyncRequestError(null);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/ldap/sync`, {
|
const res = await fetch(`${API_URL}/ldap/sync`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -274,9 +285,13 @@ export default function AdminLdapPage() {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setSyncResult(data);
|
setSyncResult(data);
|
||||||
await fetchConfig();
|
await fetchConfig();
|
||||||
|
} else {
|
||||||
|
setSyncResult(null);
|
||||||
|
setSyncRequestError(t('sync.requestError'));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setSyncResult({ created: 0, updated: 0, deactivated: 0, errors: ['Network error'] });
|
setSyncResult(null);
|
||||||
|
setSyncRequestError(t('sync.requestError'));
|
||||||
} finally {
|
} finally {
|
||||||
setSyncing(false);
|
setSyncing(false);
|
||||||
}
|
}
|
||||||
@@ -1287,6 +1302,12 @@ export default function AdminLdapPage() {
|
|||||||
{syncing ? t('sync.syncing') : t('sync.trigger')}
|
{syncing ? t('sync.syncing') : t('sync.trigger')}
|
||||||
</button>
|
</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 */}
|
{/* Sync result display */}
|
||||||
{syncResult && (
|
{syncResult && (
|
||||||
<div className="rounded-md border border-border bg-muted/30 p-4">
|
<div className="rounded-md border border-border bg-muted/30 p-4">
|
||||||
@@ -1297,6 +1318,26 @@ export default function AdminLdapPage() {
|
|||||||
deactivated: syncResult.deactivated,
|
deactivated: syncResult.deactivated,
|
||||||
})}
|
})}
|
||||||
</p>
|
</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 && (
|
{syncResult.errors.length > 0 && (
|
||||||
<div className="mt-2 space-y-1">
|
<div className="mt-2 space-y-1">
|
||||||
{syncResult.errors.map((err, i) => (
|
{syncResult.errors.map((err, i) => (
|
||||||
|
|||||||
@@ -329,7 +329,11 @@
|
|||||||
"neverSynced": "Noch nie synchronisiert",
|
"neverSynced": "Noch nie synchronisiert",
|
||||||
"trigger": "Jetzt synchronisieren",
|
"trigger": "Jetzt synchronisieren",
|
||||||
"syncing": "Synchronisiere...",
|
"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": {
|
"groupFilter": {
|
||||||
"title": "Import-Filter (Gruppen/OUs)",
|
"title": "Import-Filter (Gruppen/OUs)",
|
||||||
|
|||||||
@@ -329,7 +329,11 @@
|
|||||||
"neverSynced": "Never synced",
|
"neverSynced": "Never synced",
|
||||||
"trigger": "Sync now",
|
"trigger": "Sync now",
|
||||||
"syncing": "Syncing...",
|
"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": {
|
"groupFilter": {
|
||||||
"title": "Import filter (groups/OUs)",
|
"title": "Import filter (groups/OUs)",
|
||||||
|
|||||||
Reference in New Issue
Block a user