Treat 409 CONTROL_IDENTITY_MISMATCH from a replica as a non-retriable
failure instead of looping the same 409 through the 5-minute retry
service forever and silently writing identical failure rows.
Backend
- DatabaseService: add `sticky_error_code`, `sticky_error_expected`,
`sticky_error_got` columns to `fleet_sync_status` via an idempotent
migration. New methods setFleetSyncSticky, getFleetSyncStickyCode,
clearFleetSyncStickyForNode. recordFleetSyncSuccess clears the sticky
flag on a clean push. getFailedSyncTargets SQL adds
`AND sticky_error_code IS NULL` so the retry loop skips sticky rows.
- FleetSyncService.executePushToNode: short-circuits at the top when
sticky is set (covers event-driven pushResourceAsync calls). On a 409
with code CONTROL_IDENTITY_MISMATCH, records the failure once and
pins sticky with the expected/got fingerprints carried in the 409 body.
- routes/nodes.ts: new POST /api/nodes/:id/fleet-sync/reset-anchor.
Admin + paid + node:manage. Proxies POST /api/fleet/role/reanchor to
the peer with `{override:true}` using the stored Bearer node_proxy
token. On peer 200, clears every sticky row for the node so the next
push re-anchors and resumes replication. Distinct 502 / 504 responses
for peer-rejected / peer-unreachable so the UI can show a useful toast.
Frontend
- New lib/fleetSyncApi.ts + hooks/useFleetSyncStatus.ts. Polling hook
(30s visibilityInterval) skips fetch when !isPaid.
- NodeManager.tsx: destructive banner per affected node listing both
fingerprints, with `Reset anchor on peer` and `Remove node` buttons.
Hidden for community-tier users via empty hook data.
- FleetConfiguration.tsx (Fleet -> Status): read-only `Policy sync`
SummaryRow per remote node card. In sync / degraded / paused with
a tooltip; no action buttons (the action lives in NodeManager).
Tests
- fleet-sync-service.test.ts: 4 new cases for sticky-set on first
mismatch, short-circuit on subsequent pushes, null fingerprints,
and non-mismatch failures not setting sticky.
- database-fleet-sync-sticky.test.ts (new): 6 cases pinning the DB
contract incl. retry-loop SQL filter and migration idempotency.
- nodes-fleet-sync-reset-anchor.test.ts (new): 6 cases covering
happy path, peer 401 -> 502, peer unreachable -> 504, local-node
rejection, unknown node id, and community-tier 403.
Gate parity (Directive 30): the new POST .../reset-anchor enforces
requireAdmin + requirePaid + node:manage (matches the existing read at
GET /api/fleet/sync-status). UI banner + SummaryRow only render when the
hook returns data, which it only does for paid-tier authed users. No
existing tier-gate file moved; this is greenfield parity.
Auth audit: the peer's POST /api/fleet/role/reanchor route already uses
requireAdmin, which accepts the central's stored node_proxy Bearer
token because authMiddleware maps `scope === 'node_proxy'` to
`req.user = { username: 'node-proxy', role: 'admin', userId: 0 }`.
No widening required.
Backend tsc clean. Frontend tsc -b clean. 59 fleet-sync tests pass; full
backend suite green minus the pre-existing Windows-only file-lock flake
on filesystem-backup.test.ts that reproduces unchanged on main.
* feat(ui): hide paid features from community-tier dashboard
Community installs render only the features they can use. Tier-locked
sections, lock badges, upsell cards, and "Upgrade" buttons no longer
appear anywhere except the License page in Settings, which is the
single discoverable upgrade path.
Concretely:
- PaidGate and AdmiralGate now render null for non-qualifying tiers
instead of upsell cards.
- SectionGate (settings) hides tier-locked sections entirely.
- Settings sidebar and command palette filter out items the operator
cannot reach.
- Configuration Status widget on the dashboard drops the Automation
section for community and hides any locked rows in remaining
sections.
- Fleet > Status node cards drop locked summary rows.
- Stack action menu, sidebar bulk bar, file upload / download, scan
comparison, network topology toggle, node label picker all hide
for community instead of showing disabled affordances or "Upgrade"
literal text.
- Removes tierUpsell, TierLockChip, and useDismissalState (no longer
referenced).
Backend tier guards remain authoritative; this changes UI discovery
only.
* test(e2e): assert upload control is absent in community tier
The community-clean-ui change removes the "Upgrade to unlock upload"
pill from the file explorer. Update the matching e2e assertion to
verify the upload control is not rendered, instead of waiting for a
pill that no longer exists.
The /fleet/configuration endpoint fetched remote node config via a direct
backend-to-backend fetch that omitted the distributed license headers
(x-sencho-tier, x-sencho-variant). Remote nodes evaluated their own
Community tier and returned locked: true for Webhooks, Scanning, and
Backup even when the main node held a Skipper/Admiral license.
Forward the same tier/variant headers that remoteNodeProxy already
injects so tier gates on remote nodes honour the main node's license.
MFA and Backup are also hidden for remote node cards in the Status tab:
- MFA is a user session feature managed on the main node; remote nodes
are accessed via node_proxy Bearer tokens with no userId, so the value
was always "Not set" and provided no useful information.
- Backup (Sencho Cloud Backup) runs fleet-wide from the main node and
captures all nodes' compose files; remote nodes never configure it
independently, so showing it there was misleading.
Removing both fields from remote cards keeps the grid at 6 items (3
even pairs) and eliminates stale or irrelevant data.
* feat(dashboard): replace 24h charts with Configuration Status and Recent Activity
The 24-hour CPU/Memory area charts summed per-container metrics normalized
to each container's CPU quota, producing numbers that bore no honest
relationship to host load. The live ResourceGauges strip already shows
accurate host-level stats, making the historical charts both inaccurate
and redundant.
This commit replaces that row with two side-by-side cards:
- **Configuration Status**: aggregates every toggleable feature on the
active node (notification agents, alert rules, routing rules, auto-heal,
auto-update, webhooks, scheduled tasks, MFA, SSO, vulnerability scanning,
cloud backup, and alert thresholds) into a single at-a-glance card.
Tier-locked rows display an upgrade indicator instead of a value.
Each row is clickable and navigates to the relevant settings section.
Data refreshes every 60 s and immediately on state-invalidate events.
- **Recent Activity**: lists the ten most recent notification-history events
for the active node (deployments, image updates, auto-heal actions, scan
findings, cloud backup events, system notices) with category icons and
relative timestamps. Refreshes every 30 s.
New backend endpoints:
- GET /api/dashboard/configuration - per-node feature status with locked/
requiredTier markers so the frontend renders upgrade chips without extra
calls. The endpoint sits after authGate and before the remote proxy so
remote-node requests are transparently forwarded.
- GET /api/dashboard/recent-activity?limit=N - thin wrapper over
DatabaseService.getNotificationHistory.
- GET /api/fleet/configuration - fleet-wide fan-out using the same
Promise.allSettled dead-node-tolerant pattern as /fleet/overview.
Exposed as the new "Status" tab on the Fleet page (after Snapshots).
Shared utilities:
- visibilityInterval and formatCount extracted to frontend/src/lib/utils.ts
so the three polling hooks and two components share a single copy.
* docs(dashboard): fix stale alt text referencing removed historical charts