mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-13 12:17:34 +00:00
feat(fleet-sync): replica self-demote endpoint and role UX (#969)
A replica admin can now demote the instance back to a standalone
control without raw SQLite access. The Settings → Security UI surfaces
a confirm-gated button when the role is replica; the role probe also
surfaces a soft banner when it cannot determine fleet role rather
than silently defaulting to control.
Backend:
- POST /api/fleet/role/demote (admin, requires `{confirm: true}`):
flips fleet_role to 'control', clears fleet_self_identity,
fleet_control_identity, and both received_pushed_at:* watermarks,
drops every replicated_from_control row from scan_policies and
cve_suppressions, nulls out any orphaned policy_evaluation cache.
Returns 409 ALREADY_CONTROL when invoked on a control.
- DatabaseService gains `clearOrphanPolicyEvaluations()` and
`clearReplicatedRows()` helpers. Reanchor consolidates onto
clearReplicatedRows so it shares the same code path.
- `FleetSyncService.demote()` returns boolean for the route to
translate into 200 or 409.
Frontend:
- SecuritySection probes /fleet/role and now records explicit success
vs failure rather than silently treating an error as control. A
soft banner appears when probe fails.
- Replica banner gains a "Demote to control" button and a destructive
ConfirmModal explaining the wipe.
Tests:
- 4 new route-level vitest cases (401, 400 without confirm,
end-to-end demote with replica setup, 409 ALREADY_CONTROL with
explicit precondition).
- Service unit test asserts the consolidated clearReplicatedRows path.
- Full backend suite: 1773 pass / 5 skipped. Frontend: 185 pass.
This commit is contained in:
@@ -371,6 +371,36 @@ fleetRouter.post('/sync/:resource', authMiddleware, (req: Request, res: Response
|
||||
}
|
||||
});
|
||||
|
||||
// Demote this replica back to a standalone control. Wipes all replicated
|
||||
// security rules and the cached fingerprint, then flips `fleet_role` to
|
||||
// 'control'. The local UI's write controls become available again.
|
||||
// `{confirm: true}` body is required so a misclick cannot destroy mirrored
|
||||
// state.
|
||||
fleetRouter.post('/role/demote', authMiddleware, (req: Request, res: Response): void => {
|
||||
if (!requireAdmin(req, res)) return;
|
||||
const body = req.body ?? {};
|
||||
if (body.confirm !== true) {
|
||||
res.status(400).json({
|
||||
error: 'Demote requires explicit confirmation. Send { "confirm": true } to proceed.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const demoted = FleetSyncService.getInstance().demote();
|
||||
if (!demoted) {
|
||||
res.status(409).json({
|
||||
error: 'This instance is already a control; nothing to demote.',
|
||||
code: 'ALREADY_CONTROL',
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.json({ success: true, role: 'control' });
|
||||
} catch (error) {
|
||||
console.error('[FleetSync] Demote failed:', error);
|
||||
res.status(500).json({ error: 'Failed to demote replica' });
|
||||
}
|
||||
});
|
||||
|
||||
// Reset the control anchor on this replica. An admin must opt in explicitly
|
||||
// with `{override: true}` because reanchor wipes all replicated rows; the
|
||||
// next push from a different control will re-populate them. Used when a
|
||||
|
||||
Reference in New Issue
Block a user