fix(fleet): forward main node tier to remote config fetch and hide local-only fields (#811)

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.
This commit is contained in:
Anso
2026-04-27 15:47:51 -04:00
committed by GitHub
parent b5acfd8f58
commit ae8211c0b4
2 changed files with 29 additions and 16 deletions
+12 -4
View File
@@ -26,7 +26,7 @@ import { sanitizeForLog } from '../utils/safeLog';
import { CloudBackupService } from '../services/CloudBackupService';
import { NotificationService } from '../services/NotificationService';
import { buildLocalConfigurationStatus, type ConfigurationStatus } from './dashboard';
import { LicenseService } from '../services/LicenseService';
import { LicenseService, PROXY_TIER_HEADER, PROXY_VARIANT_HEADER } from '../services/LicenseService';
const updateTracker = FleetUpdateTrackerService.getInstance();
const UPDATE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
@@ -330,8 +330,9 @@ fleetRouter.get('/configuration', authMiddleware, async (req: Request, res: Resp
const db = DatabaseService.getInstance();
const nodes = db.getNodes();
const userId = req.user?.userId ?? 0;
const localTier = LicenseService.getInstance().getTier();
const localVariant = LicenseService.getInstance().getVariant();
const ls = LicenseService.getInstance();
const localTier = ls.getTier();
const localVariant = ls.getVariant();
const results = await Promise.allSettled(
nodes.map(async (node: Node): Promise<FleetNodeConfiguration> => {
@@ -352,7 +353,14 @@ fleetRouter.get('/configuration', authMiddleware, async (req: Request, res: Resp
try {
const resp = await fetch(
`${node.api_url.replace(/\/$/, '')}/api/dashboard/configuration`,
{ headers: { Authorization: `Bearer ${node.api_token}` }, signal: AbortSignal.timeout(10000) },
{
headers: {
Authorization: `Bearer ${node.api_token}`,
[PROXY_TIER_HEADER]: localTier,
[PROXY_VARIANT_HEADER]: localVariant ?? '',
},
signal: AbortSignal.timeout(10000),
},
);
const configuration = resp.ok ? (await resp.json() as ConfigurationStatus) : null;
return {
@@ -44,6 +44,7 @@ function SummaryRow({ icon: Icon, label, value, locked, requiredTier }: {
}
function NodeCard({ node }: { node: FleetNodeConfiguration }) {
const isRemote = node.type === 'remote';
if (!node.configuration) {
return (
<Card className="bg-card shadow-card-bevel">
@@ -51,7 +52,7 @@ function NodeCard({ node }: { node: FleetNodeConfiguration }) {
<div className="flex items-center gap-2 mb-3">
<span className="font-medium text-sm text-stat-value">{node.name}</span>
<Badge variant="outline" className="text-[10px] font-normal py-0 px-1.5 text-stat-subtitle">
{node.type === 'remote' ? 'Remote' : 'Local'}
{isRemote ? 'Remote' : 'Local'}
</Badge>
<WifiOff className="h-3.5 w-3.5 text-stat-subtitle ml-auto" strokeWidth={1.5} />
<span className="text-xs text-stat-subtitle">Offline</span>
@@ -76,7 +77,7 @@ function NodeCard({ node }: { node: FleetNodeConfiguration }) {
<div className="flex items-center gap-2 mb-3">
<span className="font-medium text-sm text-stat-value">{node.name}</span>
<Badge variant="outline" className="text-[10px] font-normal py-0 px-1.5 text-stat-subtitle">
{node.type === 'remote' ? 'Remote' : 'Local'}
{isRemote ? 'Remote' : 'Local'}
</Badge>
<div className="ml-auto flex items-center gap-1">
<CheckCircle2 className="h-3.5 w-3.5 text-success" strokeWidth={1.5} />
@@ -101,8 +102,10 @@ function NodeCard({ node }: { node: FleetNodeConfiguration }) {
}
locked={automation.webhooks.locked}
requiredTier={automation.webhooks.locked ? automation.webhooks.requiredTier : undefined} />
<SummaryRow icon={Shield} label="MFA"
value={security.mfaEnabled === null ? 'Not set' : security.mfaEnabled ? 'On' : 'Off'} />
{!isRemote && (
<SummaryRow icon={Shield} label="MFA"
value={security.mfaEnabled === null ? 'Not set' : security.mfaEnabled ? 'On' : 'Off'} />
)}
<SummaryRow icon={Shield} label="Scanning"
value={
security.scanPolicies.locked
@@ -111,14 +114,16 @@ function NodeCard({ node }: { node: FleetNodeConfiguration }) {
}
locked={security.scanPolicies.locked}
requiredTier={security.scanPolicies.locked ? security.scanPolicies.requiredTier : undefined} />
<SummaryRow icon={HardDrive} label="Backup"
value={
backup.locked
? ''
: backup.provider === 'disabled' ? 'Disabled' : 'Enabled'
}
locked={backup.locked}
requiredTier={backup.locked ? backup.requiredTier : undefined} />
{!isRemote && (
<SummaryRow icon={HardDrive} label="Backup"
value={
backup.locked
? ''
: backup.provider === 'disabled' ? 'Disabled' : 'Enabled'
}
locked={backup.locked}
requiredTier={backup.locked ? backup.requiredTier : undefined} />
)}
<SummaryRow icon={HardDrive} label="Crash detect"
value={thresholds.globalCrash ? 'On' : 'Off'} />
</div>