Merge pull request #25 from okieselbach/claude/tenant-config-report-9WElF

Add tenant config report page for galactic admins
This commit is contained in:
Oliver Kieselbach
2026-03-22 18:06:47 +01:00
committed by GitHub
5 changed files with 706 additions and 16 deletions
@@ -0,0 +1,665 @@
'use client';
import { useEffect, useState, useCallback } from 'react';
import { ProtectedRoute } from '../../components/ProtectedRoute';
import { useAuth } from '../../contexts/AuthContext';
import { API_BASE_URL } from '@/lib/config';
import { authenticatedFetch, TokenExpiredError } from '@/lib/authenticatedFetch';
import { trackEvent } from '@/lib/appInsights';
// ── Types ────────────────────────────────────────────────────────────────────
interface TenantInfo {
tenantId: string;
domainName: string;
}
/** Full tenant configuration as returned by GET /api/config/{tenantId}. */
interface TenantConfig {
tenantId: string;
domainName?: string;
lastUpdated?: string;
updatedBy?: string;
onboardedAt?: string;
// Tenant status
disabled?: boolean;
disabledReason?: string;
disabledUntil?: string;
// Security
rateLimitRequestsPerMinute?: number;
customRateLimitRequestsPerMinute?: number | null;
manufacturerWhitelist?: string;
modelWhitelist?: string;
validateAutopilotDevice?: boolean;
validateCorporateIdentifier?: boolean;
allowInsecureAgentRequests?: boolean;
// Data management
dataRetentionDays?: number;
sessionTimeoutHours?: number;
maxNdjsonPayloadSizeMB?: number;
// Collectors
enablePerformanceCollector?: boolean;
performanceCollectorIntervalSeconds?: number;
helloWaitTimeoutSeconds?: number;
// Auth circuit breaker
maxAuthFailures?: number | null;
authFailureTimeoutMinutes?: number | null;
agentMaxLifetimeMinutes?: number | null;
// Agent behavior
selfDestructOnComplete?: boolean | null;
keepLogFile?: boolean | null;
rebootOnComplete?: boolean | null;
rebootDelaySeconds?: number | null;
enableGeoLocation?: boolean | null;
enableImeMatchLog?: boolean | null;
logLevel?: string | null;
maxBatchSize?: number | null;
showEnrollmentSummary?: boolean | null;
enrollmentSummaryTimeoutSeconds?: number | null;
enrollmentSummaryBrandingImageUrl?: string | null;
enrollmentSummaryLaunchRetrySeconds?: number | null;
showScriptOutput?: boolean | null;
sendTraceEvents?: boolean;
// Analyzers
enableLocalAdminAnalyzer?: boolean | null;
enableSoftwareInventoryAnalyzer?: boolean | null;
localAdminAllowedAccountsJson?: string | null;
// Bootstrap & unrestricted
bootstrapTokenEnabled?: boolean;
unrestrictedModeEnabled?: boolean;
unrestrictedMode?: boolean;
// Diagnostics
diagnosticsBlobSasUrl?: string | null;
diagnosticsUploadMode?: string;
diagnosticsLogPathsJson?: string | null;
// Webhook notifications (new)
webhookProviderType?: number;
webhookUrl?: string | null;
webhookNotifyOnSuccess?: boolean;
webhookNotifyOnFailure?: boolean;
// Teams notifications (legacy)
teamsWebhookUrl?: string | null;
teamsNotifyOnSuccess?: boolean;
teamsNotifyOnFailure?: boolean;
}
// ── Defaults (mirrors C# TenantConfiguration defaults) ──────────────────────
const DEFAULTS: Record<string, unknown> = {
disabled: false,
rateLimitRequestsPerMinute: 100,
customRateLimitRequestsPerMinute: null,
manufacturerWhitelist: 'Dell*,HP*,Lenovo*,Microsoft Corporation',
modelWhitelist: '*',
validateAutopilotDevice: false,
validateCorporateIdentifier: false,
allowInsecureAgentRequests: false,
dataRetentionDays: 90,
sessionTimeoutHours: 5,
maxNdjsonPayloadSizeMB: 5,
enablePerformanceCollector: true,
performanceCollectorIntervalSeconds: 30,
helloWaitTimeoutSeconds: 30,
maxAuthFailures: null,
authFailureTimeoutMinutes: null,
agentMaxLifetimeMinutes: null,
selfDestructOnComplete: true,
keepLogFile: false,
rebootOnComplete: null,
rebootDelaySeconds: null,
enableGeoLocation: null,
enableImeMatchLog: null,
logLevel: null,
maxBatchSize: null,
showEnrollmentSummary: null,
enrollmentSummaryTimeoutSeconds: null,
enrollmentSummaryBrandingImageUrl: null,
enrollmentSummaryLaunchRetrySeconds: null,
showScriptOutput: true,
sendTraceEvents: true,
enableLocalAdminAnalyzer: null,
enableSoftwareInventoryAnalyzer: null,
localAdminAllowedAccountsJson: null,
bootstrapTokenEnabled: false,
unrestrictedModeEnabled: false,
unrestrictedMode: false,
diagnosticsBlobSasUrl: null,
diagnosticsUploadMode: 'Off',
diagnosticsLogPathsJson: null,
webhookProviderType: 0,
webhookUrl: null,
webhookNotifyOnSuccess: true,
webhookNotifyOnFailure: true,
teamsWebhookUrl: null,
teamsNotifyOnSuccess: true,
teamsNotifyOnFailure: true,
};
// Runtime defaults (what the agent actually receives if tenant didn't set a value)
const RUNTIME_DEFAULTS: Record<string, unknown> = {
uploadIntervalSeconds: 30,
selfDestructOnComplete: true,
keepLogFile: false,
enableGeoLocation: true,
enableImeMatchLog: false,
maxAuthFailures: 5,
authFailureTimeoutMinutes: 0,
logLevel: 'Info',
rebootOnComplete: false,
rebootDelaySeconds: 10,
showEnrollmentSummary: false,
enrollmentSummaryTimeoutSeconds: 60,
enrollmentSummaryLaunchRetrySeconds: 120,
maxBatchSize: 100,
diagnosticsUploadMode: 'Off',
sendTraceEvents: true,
unrestrictedMode: false,
// Collectors
enablePerformanceCollector: true,
performanceIntervalSeconds: 30,
collectorIdleTimeoutMinutes: 15,
enableAgentSelfMetrics: true,
agentSelfMetricsIntervalSeconds: 60,
helloWaitTimeoutSeconds: 30,
agentMaxLifetimeMinutes: 360,
// Analyzers
enableLocalAdminAnalyzer: true,
enableSoftwareInventoryAnalyzer: false,
};
// ── Helpers ──────────────────────────────────────────────────────────────────
const WEBHOOK_PROVIDERS: Record<number, string> = {
0: 'None',
1: 'Teams (Legacy Connector)',
2: 'Teams (Workflow Webhook)',
10: 'Slack',
};
function formatValue(val: unknown): string {
if (val === null || val === undefined) return '—';
if (typeof val === 'boolean') return val ? 'Yes' : 'No';
if (typeof val === 'string') return val || '—';
return String(val);
}
function isNonDefault(key: string, val: unknown, defaults: Record<string, unknown>): boolean {
if (!(key in defaults)) return false;
const def = defaults[key];
// Both null/undefined → same
if ((val === null || val === undefined) && (def === null || def === undefined)) return false;
return val !== def;
}
function maskSasUrl(url: string | null | undefined): string {
if (!url) return '—';
try {
const u = new URL(url);
return `${u.origin}${u.pathname}?***`;
} catch {
return url.length > 40 ? url.substring(0, 40) + '...' : url;
}
}
function formatDate(val: string | null | undefined): string {
if (!val) return '—';
try {
return new Date(val).toLocaleString();
} catch {
return val;
}
}
// ── Section component ────────────────────────────────────────────────────────
interface ConfigRowProps {
label: string;
value: unknown;
configKey?: string;
defaults?: Record<string, unknown>;
masked?: boolean;
isDate?: boolean;
}
function ConfigRow({ label, value, configKey, defaults, masked, isDate }: ConfigRowProps) {
const highlight = configKey && defaults ? isNonDefault(configKey, value, defaults) : false;
const displayVal = masked
? maskSasUrl(value as string)
: isDate
? formatDate(value as string)
: formatValue(value);
return (
<tr className={highlight ? 'bg-purple-50 dark:bg-purple-900/20' : ''}>
<td className="py-1.5 px-3 text-sm text-gray-600 dark:text-gray-400 font-medium whitespace-nowrap">{label}</td>
<td className="py-1.5 px-3 text-sm text-gray-900 dark:text-gray-100 font-mono break-all">
{displayVal}
{highlight && <span className="ml-2 text-xs text-purple-600 dark:text-purple-400 font-sans">(custom)</span>}
</td>
</tr>
);
}
interface SectionProps {
title: string;
children: React.ReactNode;
}
function Section({ title, children }: SectionProps) {
return (
<div className="mb-6">
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-2">{title}</h3>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
<table className="w-full">
<tbody className="divide-y divide-gray-100 dark:divide-gray-700">{children}</tbody>
</table>
</div>
</div>
);
}
// ── Compute runtime parameters ───────────────────────────────────────────────
function computeRuntime(c: TenantConfig): Record<string, unknown> {
return {
uploadIntervalSeconds: 30,
selfDestructOnComplete: c.selfDestructOnComplete ?? true,
keepLogFile: c.keepLogFile ?? false,
enableGeoLocation: c.enableGeoLocation ?? true,
enableImeMatchLog: c.enableImeMatchLog ?? false,
maxAuthFailures: c.maxAuthFailures ?? 5,
authFailureTimeoutMinutes: c.authFailureTimeoutMinutes ?? 0,
logLevel: c.logLevel ?? 'Info',
rebootOnComplete: c.rebootOnComplete ?? false,
rebootDelaySeconds: c.rebootDelaySeconds ?? 10,
showEnrollmentSummary: c.showEnrollmentSummary ?? false,
enrollmentSummaryTimeoutSeconds: c.enrollmentSummaryTimeoutSeconds ?? 60,
enrollmentSummaryBrandingImageUrl: c.enrollmentSummaryBrandingImageUrl ?? null,
enrollmentSummaryLaunchRetrySeconds: c.enrollmentSummaryLaunchRetrySeconds ?? 120,
maxBatchSize: c.maxBatchSize ?? 100,
diagnosticsUploadEnabled: !!(c.diagnosticsBlobSasUrl && c.diagnosticsUploadMode && c.diagnosticsUploadMode !== 'Off'),
diagnosticsUploadMode: c.diagnosticsUploadMode ?? 'Off',
sendTraceEvents: c.sendTraceEvents ?? true,
unrestrictedMode: c.unrestrictedMode ?? false,
// Collectors
enablePerformanceCollector: c.enablePerformanceCollector ?? true,
performanceIntervalSeconds: c.performanceCollectorIntervalSeconds ?? 30,
collectorIdleTimeoutMinutes: 15,
enableAgentSelfMetrics: true,
agentSelfMetricsIntervalSeconds: 60,
helloWaitTimeoutSeconds: c.helloWaitTimeoutSeconds ?? 30,
agentMaxLifetimeMinutes: c.agentMaxLifetimeMinutes ?? 360,
// Analyzers
enableLocalAdminAnalyzer: c.enableLocalAdminAnalyzer ?? true,
enableSoftwareInventoryAnalyzer: c.enableSoftwareInventoryAnalyzer ?? false,
};
}
// ── Main page ────────────────────────────────────────────────────────────────
function TenantConfigReportContent() {
const { user, getAccessToken } = useAuth();
useEffect(() => { trackEvent('tenant_config_report_viewed'); }, []);
const [tenants, setTenants] = useState<TenantInfo[]>([]);
const [selectedTenantId, setSelectedTenantId] = useState('');
const [config, setConfig] = useState<TenantConfig | null>(null);
const [loading, setLoading] = useState(false);
const [loadingTenants, setLoadingTenants] = useState(true);
const [error, setError] = useState<string | null>(null);
// Fetch tenant list
useEffect(() => {
if (!user?.isGalacticAdmin) return;
const fetchTenants = async () => {
try {
setLoadingTenants(true);
const response = await authenticatedFetch(`${API_BASE_URL}/api/config/all`, getAccessToken);
if (response.ok) {
const data = await response.json();
const mapped: TenantInfo[] = data.map((t: { tenantId: string; domainName: string }) => ({
tenantId: t.tenantId,
domainName: t.domainName || '',
}));
mapped.sort((a, b) => (a.domainName || a.tenantId).localeCompare(b.domainName || b.tenantId));
setTenants(mapped);
if (mapped.length > 0 && !selectedTenantId) {
setSelectedTenantId(mapped[0].tenantId);
}
}
} catch (err) {
if (err instanceof TokenExpiredError) {
setError('Session expired. Please refresh.');
} else {
console.error('Error fetching tenant list:', err);
}
} finally {
setLoadingTenants(false);
}
};
fetchTenants();
}, [user?.isGalacticAdmin, getAccessToken]);
// Fetch config for selected tenant
const fetchConfig = useCallback(async () => {
if (!selectedTenantId) return;
try {
setLoading(true);
setError(null);
const response = await authenticatedFetch(
`${API_BASE_URL}/api/config/${selectedTenantId}`,
getAccessToken,
);
if (!response.ok) {
throw new Error(`Failed to load config: ${response.status} ${response.statusText}`);
}
const data: TenantConfig = await response.json();
setConfig(data);
} catch (err) {
if (err instanceof TokenExpiredError) {
setError('Session expired. Please refresh.');
} else {
setError((err as Error).message);
}
setConfig(null);
} finally {
setLoading(false);
}
}, [selectedTenantId, getAccessToken]);
useEffect(() => {
fetchConfig();
}, [fetchConfig]);
// Computed runtime
const runtime = config ? computeRuntime(config) : null;
// Webhook display helper
const webhookDisplay = config
? (() => {
if (config.webhookUrl && config.webhookProviderType) {
return { url: config.webhookUrl, provider: config.webhookProviderType, onSuccess: config.webhookNotifyOnSuccess, onFailure: config.webhookNotifyOnFailure };
}
if (config.teamsWebhookUrl) {
return { url: config.teamsWebhookUrl, provider: 1, onSuccess: config.teamsNotifyOnSuccess, onFailure: config.teamsNotifyOnFailure };
}
return { url: null, provider: 0, onSuccess: true, onFailure: true };
})()
: null;
const selectedTenant = tenants.find((t) => t.tenantId === selectedTenantId);
return (
<div className="max-w-5xl mx-auto px-4 py-8">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Tenant Config Report</h1>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Read-only overview of all tenant configuration and runtime parameters
</p>
</div>
<div className="flex items-center gap-3">
<label className="text-sm text-gray-500 whitespace-nowrap">Tenant:</label>
<select
value={selectedTenantId}
onChange={(e) => setSelectedTenantId(e.target.value)}
className="text-sm border border-gray-300 dark:border-gray-600 rounded-md px-2 py-1.5 max-w-xs bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
disabled={loadingTenants}
>
{loadingTenants && <option>Loading...</option>}
{tenants.map((t) => (
<option key={t.tenantId} value={t.tenantId}>
{t.domainName
? `${t.domainName} (${t.tenantId.substring(0, 8)}...)`
: t.tenantId}
</option>
))}
</select>
<button
onClick={fetchConfig}
disabled={loading || !selectedTenantId}
className="text-sm px-3 py-1.5 bg-purple-600 text-white rounded-md hover:bg-purple-700 disabled:opacity-50"
>
{loading ? 'Loading...' : 'Refresh'}
</button>
</div>
</div>
{/* Error */}
{error && (
<div className="mb-6 p-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg text-sm text-red-700 dark:text-red-300">
{error}
</div>
)}
{/* Loading */}
{loading && (
<div className="text-center py-12 text-gray-500">
<svg className="animate-spin h-8 w-8 mx-auto mb-3 text-purple-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Loading configuration...
</div>
)}
{/* Config report */}
{!loading && config && (
<>
{/* Tenant header card */}
<div className="mb-6 p-4 bg-gradient-to-r from-purple-50 to-indigo-50 dark:from-purple-900/20 dark:to-indigo-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 text-sm">
<div>
<span className="text-gray-500 dark:text-gray-400 block">Domain</span>
<span className="font-medium text-gray-900 dark:text-gray-100">{config.domainName || '—'}</span>
</div>
<div>
<span className="text-gray-500 dark:text-gray-400 block">Tenant ID</span>
<span className="font-mono text-gray-900 dark:text-gray-100 text-xs">{config.tenantId}</span>
</div>
<div>
<span className="text-gray-500 dark:text-gray-400 block">Last Updated</span>
<span className="font-medium text-gray-900 dark:text-gray-100">{formatDate(config.lastUpdated)}</span>
</div>
<div>
<span className="text-gray-500 dark:text-gray-400 block">Updated By</span>
<span className="font-medium text-gray-900 dark:text-gray-100">{config.updatedBy || '—'}</span>
</div>
</div>
{config.disabled && (
<div className="mt-3 p-2 bg-red-100 dark:bg-red-900/40 rounded text-sm text-red-700 dark:text-red-300 font-medium">
TENANT DISABLED{config.disabledReason ? `: ${config.disabledReason}` : ''}
{config.disabledUntil ? ` (until ${formatDate(config.disabledUntil)})` : ''}
</div>
)}
</div>
{/* Legend */}
<div className="mb-4 text-xs text-gray-500 dark:text-gray-400 flex items-center gap-2">
<span className="inline-block w-3 h-3 bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-700 rounded" />
<span>Purple rows = non-default value (custom)</span>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* ────── LEFT: Tenant Configuration ────── */}
<div>
<h2 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">Tenant Configuration</h2>
<Section title="Tenant Status">
<ConfigRow label="Disabled" value={config.disabled} configKey="disabled" defaults={DEFAULTS} />
<ConfigRow label="Disabled Reason" value={config.disabledReason} />
<ConfigRow label="Disabled Until" value={config.disabledUntil} isDate />
<ConfigRow label="Onboarded At" value={config.onboardedAt} isDate />
</Section>
<Section title="Security & Validation">
<ConfigRow label="Validate Autopilot Device" value={config.validateAutopilotDevice} configKey="validateAutopilotDevice" defaults={DEFAULTS} />
<ConfigRow label="Validate Corporate Identifier" value={config.validateCorporateIdentifier} configKey="validateCorporateIdentifier" defaults={DEFAULTS} />
<ConfigRow label="Allow Insecure Agent Requests" value={config.allowInsecureAgentRequests} configKey="allowInsecureAgentRequests" defaults={DEFAULTS} />
<ConfigRow label="Rate Limit (req/min)" value={config.rateLimitRequestsPerMinute} configKey="rateLimitRequestsPerMinute" defaults={DEFAULTS} />
<ConfigRow label="Custom Rate Limit" value={config.customRateLimitRequestsPerMinute} configKey="customRateLimitRequestsPerMinute" defaults={DEFAULTS} />
</Section>
<Section title="Hardware Whitelist">
<ConfigRow label="Manufacturers" value={config.manufacturerWhitelist} configKey="manufacturerWhitelist" defaults={DEFAULTS} />
<ConfigRow label="Models" value={config.modelWhitelist} configKey="modelWhitelist" defaults={DEFAULTS} />
</Section>
<Section title="Data Management">
<ConfigRow label="Data Retention (days)" value={config.dataRetentionDays} configKey="dataRetentionDays" defaults={DEFAULTS} />
<ConfigRow label="Session Timeout (hours)" value={config.sessionTimeoutHours} configKey="sessionTimeoutHours" defaults={DEFAULTS} />
<ConfigRow label="Max NDJSON Payload (MB)" value={config.maxNdjsonPayloadSizeMB} configKey="maxNdjsonPayloadSizeMB" defaults={DEFAULTS} />
</Section>
<Section title="Agent Collectors">
<ConfigRow label="Performance Collector" value={config.enablePerformanceCollector} configKey="enablePerformanceCollector" defaults={DEFAULTS} />
<ConfigRow label="Perf. Interval (sec)" value={config.performanceCollectorIntervalSeconds} configKey="performanceCollectorIntervalSeconds" defaults={DEFAULTS} />
<ConfigRow label="Hello Wait Timeout (sec)" value={config.helloWaitTimeoutSeconds} configKey="helloWaitTimeoutSeconds" defaults={DEFAULTS} />
</Section>
<Section title="Auth Circuit Breaker">
<ConfigRow label="Max Auth Failures" value={config.maxAuthFailures} configKey="maxAuthFailures" defaults={DEFAULTS} />
<ConfigRow label="Auth Failure Timeout (min)" value={config.authFailureTimeoutMinutes} configKey="authFailureTimeoutMinutes" defaults={DEFAULTS} />
<ConfigRow label="Agent Max Lifetime (min)" value={config.agentMaxLifetimeMinutes} configKey="agentMaxLifetimeMinutes" defaults={DEFAULTS} />
</Section>
<Section title="Agent Behavior">
<ConfigRow label="Self-Destruct On Complete" value={config.selfDestructOnComplete} configKey="selfDestructOnComplete" defaults={DEFAULTS} />
<ConfigRow label="Keep Log File" value={config.keepLogFile} configKey="keepLogFile" defaults={DEFAULTS} />
<ConfigRow label="Reboot On Complete" value={config.rebootOnComplete} configKey="rebootOnComplete" defaults={DEFAULTS} />
<ConfigRow label="Reboot Delay (sec)" value={config.rebootDelaySeconds} configKey="rebootDelaySeconds" defaults={DEFAULTS} />
<ConfigRow label="Geo-Location" value={config.enableGeoLocation} configKey="enableGeoLocation" defaults={DEFAULTS} />
<ConfigRow label="IME Match Log" value={config.enableImeMatchLog} configKey="enableImeMatchLog" defaults={DEFAULTS} />
<ConfigRow label="Log Level" value={config.logLevel} configKey="logLevel" defaults={DEFAULTS} />
<ConfigRow label="Max Batch Size" value={config.maxBatchSize} configKey="maxBatchSize" defaults={DEFAULTS} />
<ConfigRow label="Show Script Output" value={config.showScriptOutput} configKey="showScriptOutput" defaults={DEFAULTS} />
<ConfigRow label="Send Trace Events" value={config.sendTraceEvents} configKey="sendTraceEvents" defaults={DEFAULTS} />
</Section>
<Section title="Enrollment Summary">
<ConfigRow label="Show Summary" value={config.showEnrollmentSummary} configKey="showEnrollmentSummary" defaults={DEFAULTS} />
<ConfigRow label="Timeout (sec)" value={config.enrollmentSummaryTimeoutSeconds} configKey="enrollmentSummaryTimeoutSeconds" defaults={DEFAULTS} />
<ConfigRow label="Branding Image URL" value={config.enrollmentSummaryBrandingImageUrl} configKey="enrollmentSummaryBrandingImageUrl" defaults={DEFAULTS} />
<ConfigRow label="Launch Retry (sec)" value={config.enrollmentSummaryLaunchRetrySeconds} configKey="enrollmentSummaryLaunchRetrySeconds" defaults={DEFAULTS} />
</Section>
<Section title="Analyzers">
<ConfigRow label="Local Admin Analyzer" value={config.enableLocalAdminAnalyzer} configKey="enableLocalAdminAnalyzer" defaults={DEFAULTS} />
<ConfigRow label="Software Inventory Analyzer" value={config.enableSoftwareInventoryAnalyzer} configKey="enableSoftwareInventoryAnalyzer" defaults={DEFAULTS} />
<ConfigRow label="Allowed Local Admin Accounts" value={config.localAdminAllowedAccountsJson} configKey="localAdminAllowedAccountsJson" defaults={DEFAULTS} />
</Section>
<Section title="Webhooks">
<ConfigRow label="Provider" value={WEBHOOK_PROVIDERS[webhookDisplay?.provider ?? 0] ?? 'Unknown'} />
<ConfigRow label="Webhook URL" value={webhookDisplay?.url} masked />
<ConfigRow label="Notify On Success" value={webhookDisplay?.onSuccess} />
<ConfigRow label="Notify On Failure" value={webhookDisplay?.onFailure} />
</Section>
<Section title="Diagnostics">
<ConfigRow label="Upload Mode" value={config.diagnosticsUploadMode} configKey="diagnosticsUploadMode" defaults={DEFAULTS} />
<ConfigRow label="Blob SAS URL" value={config.diagnosticsBlobSasUrl} masked />
<ConfigRow label="Custom Log Paths" value={config.diagnosticsLogPathsJson} configKey="diagnosticsLogPathsJson" defaults={DEFAULTS} />
</Section>
<Section title="Feature Flags (Galactic)">
<ConfigRow label="Bootstrap Token Enabled" value={config.bootstrapTokenEnabled} configKey="bootstrapTokenEnabled" defaults={DEFAULTS} />
<ConfigRow label="Unrestricted Mode Enabled" value={config.unrestrictedModeEnabled} configKey="unrestrictedModeEnabled" defaults={DEFAULTS} />
<ConfigRow label="Unrestricted Mode Active" value={config.unrestrictedMode} configKey="unrestrictedMode" defaults={DEFAULTS} />
</Section>
</div>
{/* ────── RIGHT: Runtime Parameters (Agent Config) ────── */}
<div>
<h2 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">Runtime Parameters (Agent)</h2>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
These are the effective values the agent receives at runtime, with defaults applied for unset fields.
</p>
{runtime && (
<>
<Section title="Upload & Batching">
<ConfigRow label="Upload Interval (sec)" value={runtime.uploadIntervalSeconds} configKey="uploadIntervalSeconds" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Max Batch Size" value={runtime.maxBatchSize} configKey="maxBatchSize" defaults={RUNTIME_DEFAULTS} />
</Section>
<Section title="Agent Behavior">
<ConfigRow label="Self-Destruct" value={runtime.selfDestructOnComplete} configKey="selfDestructOnComplete" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Keep Log File" value={runtime.keepLogFile} configKey="keepLogFile" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Reboot On Complete" value={runtime.rebootOnComplete} configKey="rebootOnComplete" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Reboot Delay (sec)" value={runtime.rebootDelaySeconds} configKey="rebootDelaySeconds" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Geo-Location" value={runtime.enableGeoLocation} configKey="enableGeoLocation" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="IME Match Log" value={runtime.enableImeMatchLog} configKey="enableImeMatchLog" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Log Level" value={runtime.logLevel} configKey="logLevel" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Send Trace Events" value={runtime.sendTraceEvents} configKey="sendTraceEvents" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Unrestricted Mode" value={runtime.unrestrictedMode} configKey="unrestrictedMode" defaults={RUNTIME_DEFAULTS} />
</Section>
<Section title="Auth Circuit Breaker">
<ConfigRow label="Max Auth Failures" value={runtime.maxAuthFailures} configKey="maxAuthFailures" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Auth Failure Timeout (min)" value={runtime.authFailureTimeoutMinutes} configKey="authFailureTimeoutMinutes" defaults={RUNTIME_DEFAULTS} />
</Section>
<Section title="Enrollment Summary">
<ConfigRow label="Show Summary" value={runtime.showEnrollmentSummary} configKey="showEnrollmentSummary" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Timeout (sec)" value={runtime.enrollmentSummaryTimeoutSeconds} configKey="enrollmentSummaryTimeoutSeconds" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Branding Image URL" value={runtime.enrollmentSummaryBrandingImageUrl} />
<ConfigRow label="Launch Retry (sec)" value={runtime.enrollmentSummaryLaunchRetrySeconds} configKey="enrollmentSummaryLaunchRetrySeconds" defaults={RUNTIME_DEFAULTS} />
</Section>
<Section title="Collectors">
<ConfigRow label="Performance Collector" value={runtime.enablePerformanceCollector} configKey="enablePerformanceCollector" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Perf. Interval (sec)" value={runtime.performanceIntervalSeconds} configKey="performanceIntervalSeconds" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Idle Timeout (min)" value={runtime.collectorIdleTimeoutMinutes} configKey="collectorIdleTimeoutMinutes" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Agent Self-Metrics" value={runtime.enableAgentSelfMetrics} configKey="enableAgentSelfMetrics" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Self-Metrics Interval (sec)" value={runtime.agentSelfMetricsIntervalSeconds} configKey="agentSelfMetricsIntervalSeconds" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Hello Wait Timeout (sec)" value={runtime.helloWaitTimeoutSeconds} configKey="helloWaitTimeoutSeconds" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Agent Max Lifetime (min)" value={runtime.agentMaxLifetimeMinutes} configKey="agentMaxLifetimeMinutes" defaults={RUNTIME_DEFAULTS} />
</Section>
<Section title="Diagnostics">
<ConfigRow label="Upload Enabled" value={runtime.diagnosticsUploadEnabled} />
<ConfigRow label="Upload Mode" value={runtime.diagnosticsUploadMode} configKey="diagnosticsUploadMode" defaults={RUNTIME_DEFAULTS} />
</Section>
<Section title="Analyzers">
<ConfigRow label="Local Admin Analyzer" value={runtime.enableLocalAdminAnalyzer} configKey="enableLocalAdminAnalyzer" defaults={RUNTIME_DEFAULTS} />
<ConfigRow label="Software Inventory Analyzer" value={runtime.enableSoftwareInventoryAnalyzer} configKey="enableSoftwareInventoryAnalyzer" defaults={RUNTIME_DEFAULTS} />
</Section>
</>
)}
</div>
</div>
</>
)}
{/* Empty state */}
{!loading && !error && !config && (
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
Select a tenant to view its configuration report.
</div>
)}
</div>
);
}
export default function TenantConfigReportPage() {
return (
<ProtectedRoute requireGalacticAdmin={true}>
<TenantConfigReportContent />
</ProtectedRoute>
);
}
@@ -40,6 +40,14 @@ function MonitorIcon({ className = "w-5 h-5" }: { className?: string }) {
);
}
function DocumentMagnifyingGlassIcon({ className = "w-5 h-5" }: { className?: string }) {
return (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m5.231 13.481L15 17.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v16.5c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9zm3.75 11.625a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
</svg>
);
}
function ClipboardDocumentIcon({ className = "w-5 h-5" }: { className?: string }) {
return (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
@@ -113,6 +121,7 @@ export const NAV_GROUPS: NavGroup[] = [
items: [
{ id: "platform-usage-metrics", label: "Platform Usage", href: "/platform-usage-metrics", icon: <GlobeAltIcon /> },
{ id: "platform-metrics", label: "Platform Metrics", href: "/platform-metrics", icon: <ChartBarIcon /> },
{ id: "tenant-config-report", label: "Tenant Config", href: "/tenant-config-report", icon: <DocumentMagnifyingGlassIcon /> },
],
},
];
@@ -3,21 +3,21 @@
* Run: node scripts/generate-lastmod.js
*/
export const PAGE_LASTMOD: Record<string, string> = {
"/": "2026-03-12T10:26:31+00:00",
"/about": "2026-03-19T19:44:38+01:00",
"/roadmap": "2026-03-18T12:04:27+01:00",
"/changelog": "2026-03-19T19:02:11+01:00",
"/privacy": "2026-03-19T22:04:59+01:00",
"/terms": "2026-03-19T19:18:49+01:00",
"/docs": "2026-03-20T08:07:17+01:00",
"/docs/private-preview": "2026-03-11T10:48:23+01:00",
"/": "2026-03-19T18:27:37+01:00",
"/about": "2026-03-20T21:47:19+01:00",
"/roadmap": "2026-03-20T21:47:19+01:00",
"/changelog": "2026-03-20T21:47:19+01:00",
"/privacy": "2026-03-20T21:47:19+01:00",
"/terms": "2026-03-20T21:47:19+01:00",
"/docs": "2026-03-20T08:51:20+01:00",
"/docs/private-preview": "2026-03-19T18:27:37+01:00",
"/docs/overview": "2026-03-19T19:18:49+01:00",
"/docs/general": "2026-03-19T18:45:11+01:00",
"/docs/setup": "2026-02-27T20:06:14+01:00",
"/docs/agent": "2026-03-10T18:31:56+01:00",
"/docs/agent-setup": "2026-02-27T20:06:14+01:00",
"/docs/settings": "2026-03-18T13:23:49+01:00",
"/docs/gather-rules": "2026-03-17T23:32:03+01:00",
"/docs/analyze-rules": "2026-03-17T23:32:03+01:00",
"/docs/ime-log-patterns": "2026-03-18T09:00:57+01:00",
"/docs/setup": "2026-03-19T18:27:37+01:00",
"/docs/agent": "2026-03-19T18:27:37+01:00",
"/docs/agent-setup": "2026-03-19T18:27:37+01:00",
"/docs/settings": "2026-03-19T18:27:37+01:00",
"/docs/gather-rules": "2026-03-19T18:27:37+01:00",
"/docs/analyze-rules": "2026-03-19T18:27:37+01:00",
"/docs/ime-log-patterns": "2026-03-19T18:27:37+01:00",
};
File diff suppressed because one or more lines are too long
+16
View File
@@ -0,0 +1,16 @@
# Tenant Config Report (Galactic Admin)
## Goal
New galactic-admin-only page that shows a read-only report of a tenant's full configuration,
including admin-set parameters and computed runtime (agent) parameters.
## Plan
- [x] Write plan
- [ ] Create `/app/tenant-config-report/page.tsx` — galactic admin only, read-only report
- Tenant selector (reuse pattern from gather-rules page)
- Fetch config via `GET /api/config/{tenantId}` (existing endpoint, GA cross-tenant)
- Display all TenantConfiguration fields grouped by section
- Show computed runtime parameters (AgentConfigResponse equivalents with defaults)
- Highlight non-default values for quick scanning
- [ ] Add nav item to galactic admin section in `globalNavConfig.tsx`
- [ ] Commit and push to feature branch