Publish curated Unraid-AppOps-Gateway source from ad0a390e431166c3198a2ac3bec010c2b44f2dea

This commit is contained in:
Public Source Publisher committed 2026-10-02 09:56:13 +00:00
1 parent 29c1e3d5a1
commit 98d31477f9
35 files changed
+7264 -185

No files matched your search

+5 -1
View File
@@ -4,7 +4,7 @@ import { AISummary, AIRecommendation, AIChatMessage, AIAutopilotRule, AITool } f
import { toIsoString } from '@/lib/backendDateTime';
import type { AiSecurityFinding, AiSecurityRecommendation, AiSecurityReviewResult, AiSecurityScope, AiSecuritySummary, NotificationChannel, NotificationEvaluationResult, NotificationEvent, NotificationRule, NotificationSummary, RemediationApprovalProposal, RemediationDryRunResult, RemediationPlan, RemediationPlanCollection, SafetyDashboard, SafetyFinding, SafetyRecommendation, SafetyScoreBreakdown, SafetySection, SafetySummary, UnraidDisksResponse, UnraidDockerResponse, UnraidSharesResponse, UnraidVmCapabilitiesResponse, UnraidVmsResponse } from '../types/appops';
import type { GiteaActionsStatus, IntegrationStatus, GatewayUser, IdentityProvider, PermissionDefinition, UserAccessUpdate, UserAuthSession, RagQueryResult, RagStatus, RunnerFleetStatus } from '../types/appops';
import type { DeliveryStatus, DeploymentJob, Enrollment, EnrollmentCapabilities, WorkMeshPromotion } from '../types/appops';
import type { DeliveryStatus, DeploymentJob, Enrollment, EnrollmentCapabilities, EnrollmentStorageRootSelection, EnrollmentTrustStatus, WorkMeshPromotion } from '../types/appops';
import type { ExposurePreferences, ManagedAiConfig, ManagedAiConfigUpdate, ManagedAiStatus, ManagedAiInference } from '../types/appops';
type HealthStatus = {
@@ -372,6 +372,10 @@ export const createLiveClient = () => ({
},
async getDeliveryStatus(): Promise<DeliveryStatus> { return get<DeliveryStatus>('/api/deployments/delivery-status', defaultConfig); },
async getEnrollmentCapabilities(): Promise<EnrollmentCapabilities> { return get<EnrollmentCapabilities>('/api/enrollments/capabilities', defaultConfig); },
async getEnrollmentTrustStatus(): Promise<EnrollmentTrustStatus> { return get<EnrollmentTrustStatus>('/api/enrollments/trust', defaultConfig); },
async updateEnrollmentStorageRoot(payload: EnrollmentStorageRootSelection): Promise<EnrollmentTrustStatus> {
return patch<EnrollmentTrustStatus>('/api/enrollments/trust/storage-root', payload, defaultConfig);
},
async getEnrollments(): Promise<{ items: Enrollment[] }> { return get<{ items: Enrollment[] }>('/api/enrollments', defaultConfig); },
async getSystemSummary(): Promise<SystemSummary> { return get<SystemSummary>('/api/system/summary', defaultConfig); },
async getHealth(): Promise<HealthStatus> { return get<HealthStatus>('/api/health', defaultConfig); },
+392
View File
@@ -0,0 +1,392 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { AlertCircle, CheckCircle2, Database, LoaderCircle, RefreshCw, ShieldAlert } from 'lucide-react';
import { appopsClient } from '@/api/appopsClient';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { formatOperatorError } from '@/lib/operatorError';
import {
buildEnrollmentStorageView,
buildStorageRootSelection,
canSaveStorageRootSelection,
canRequestManagedStorageInitialization,
normalizeManagedStoragePreview,
normalizeManagedStorageActionOutcome,
LEGACY_APPDATA_ROOT,
MANAGED_APPDATA_ROOT,
MANAGED_STORAGE_INITIALIZE_ACTION,
MANAGED_STORAGE_TARGET,
managedStorageBlockerLabel,
managedStorageStatusLabel,
rootReadinessLabel,
} from '@/lib/enrollmentStorageView';
import { useUserSession } from '@/hooks/useUserSession';
import type { ActionPlan, EnrollmentCapabilities, EnrollmentTrustStatus, PreflightPreview, TrustedAppdataRoot } from '@/types/appops';
interface StorageSnapshot {
trust: EnrollmentTrustStatus;
capabilities: EnrollmentCapabilities;
view: ReturnType<typeof buildEnrollmentStorageView>;
preview: PreflightPreview | null;
previewError: string | null;
}
interface StorageDraft {
root: TrustedAppdataRoot;
backupAcknowledged: boolean;
}
const labelForRoot = (root: TrustedAppdataRoot) => (
root === LEGACY_APPDATA_ROOT ? 'Unraid appdata share' : 'Dedicated managed Docker-root volume'
);
const isConflict = (error: unknown): boolean => (
error instanceof Error && /Backend 409:/.test(error.message)
);
export const ManagedStorageSettings = () => {
const session = useUserSession();
const isAdmin = session.ready && session.authenticated && session.user?.role === 'admin';
const canManage = isAdmin && session.permissions.includes('settings.manage');
const [snapshot, setSnapshot] = useState<StorageSnapshot | null>(null);
const [draft, setDraft] = useState<StorageDraft>({ root: LEGACY_APPDATA_ROOT, backupAcknowledged: false });
const [refreshing, setRefreshing] = useState(false);
const [loadError, setLoadError] = useState('');
const [saveError, setSaveError] = useState('');
const [conflict, setConflict] = useState('');
const [saveNotice, setSaveNotice] = useState('');
const [saving, setSaving] = useState(false);
const [initializing, setInitializing] = useState(false);
const [actionNotice, setActionNotice] = useState<{ tone: 'status' | 'alert'; message: string } | null>(null);
const requestSequence = useRef(0);
const snapshotRef = useRef<StorageSnapshot | null>(null);
const draftRef = useRef(draft);
const updateDraft = (next: StorageDraft) => {
draftRef.current = next;
setDraft(next);
};
const load = useCallback(async (options: { preserveDraft?: boolean } = {}) => {
if (!canManage) return;
const sequence = ++requestSequence.current;
setRefreshing(true);
setLoadError('');
try {
const [trust, capabilities] = await Promise.all([
appopsClient.getEnrollmentTrustStatus(),
appopsClient.getEnrollmentCapabilities(),
]);
const view = buildEnrollmentStorageView(trust, capabilities);
let preview: PreflightPreview | null = null;
let previewError: string | null = null;
if (view.managedStatus === 'initialization_required') {
try {
const rawPreview: unknown = await appopsClient.previewAction(MANAGED_STORAGE_INITIALIZE_ACTION, MANAGED_STORAGE_TARGET);
preview = normalizeManagedStoragePreview(rawPreview);
if (!preview) previewError = 'De policy-preview had geen geldig formaat; de actie blijft geblokkeerd.';
} catch (error) {
previewError = formatOperatorError(error, 'De policy-preview is niet beschikbaar.');
}
}
if (sequence !== requestSequence.current) return;
const previous = snapshotRef.current;
const currentDraft = draftRef.current;
const draftChanged = previous !== null && (
currentDraft.root !== previous.view.currentRoot ||
currentDraft.backupAcknowledged !== previous.view.backupAcknowledged
);
const nextSnapshot = { trust, capabilities, view, preview, previewError };
snapshotRef.current = nextSnapshot;
setSnapshot(nextSnapshot);
if (!options.preserveDraft && !draftChanged && view.currentRoot) {
updateDraft({ root: view.currentRoot, backupAcknowledged: view.backupAcknowledged });
}
if (!options.preserveDraft) setConflict('');
} catch (error) {
if (sequence !== requestSequence.current) return;
setLoadError(formatOperatorError(error, 'Opslaginstellingen konden niet worden geladen.'));
} finally {
if (sequence === requestSequence.current) setRefreshing(false);
}
}, [canManage]);
useEffect(() => {
if (canManage) void load();
}, [canManage, load]);
if (!isAdmin) return null;
const view = snapshot?.view;
const selectedDraft = draft.root;
const draftIsManaged = selectedDraft === MANAGED_APPDATA_ROOT;
const dirty = Boolean(view?.currentRoot && (
draft.root !== view.currentRoot ||
(draftIsManaged && draft.backupAcknowledged !== view.backupAcknowledged)
));
const requestPayload = buildStorageRootSelection(draft.root, draft.backupAcknowledged);
const saveDisabled = !canSaveStorageRootSelection({
canManage,
configured: Boolean(snapshot?.view.configured),
payloadAvailable: Boolean(requestPayload),
dirty,
loadError,
refreshing,
saving,
initializing,
});
const initializationGate = {
canManage,
view,
preview: snapshot?.preview,
dirty,
loadError,
conflict,
refreshing,
saving,
initializing,
};
const initializeAllowed = canRequestManagedStorageInitialization(initializationGate);
const save = async () => {
if (!canManage || !requestPayload || saveDisabled) return;
const sequence = ++requestSequence.current;
setSaving(true);
setSaveError('');
setConflict('');
setSaveNotice('');
setActionNotice(null);
try {
const updatedTrust = await appopsClient.updateEnrollmentStorageRoot(requestPayload);
if (sequence !== requestSequence.current) return;
const current = snapshotRef.current;
if (current) {
const updatedView = buildEnrollmentStorageView(updatedTrust, current.capabilities);
const updatedSnapshot = { ...current, trust: updatedTrust, view: updatedView };
snapshotRef.current = updatedSnapshot;
setSnapshot(updatedSnapshot);
if (updatedView.currentRoot) updateDraft({ root: updatedView.currentRoot, backupAcknowledged: updatedView.backupAcknowledged });
}
setSaveNotice('Storagevoorkeur opgeslagen. De controller-readiness wordt opnieuw gecontroleerd; bestaande targets blijven ongewijzigd.');
await load();
} catch (error) {
if (sequence !== requestSequence.current) return;
if (isConflict(error)) {
setConflict('De trustconfiguratie is intussen gewijzigd. De actuele status wordt geladen; jouw keuze blijft als concept bewaard. Controleer de selectie en sla opnieuw op als die nog klopt.');
await load({ preserveDraft: true });
} else {
setSaveError(formatOperatorError(error, 'De storagevoorkeur is niet opgeslagen. Jouw concept blijft behouden.'));
}
} finally {
setSaving(false);
}
};
const initialize = async () => {
if (!snapshot?.preview || !view || !canRequestManagedStorageInitialization(initializationGate)) return;
const preview = snapshot.preview;
const plan: ActionPlan = {
intent: MANAGED_STORAGE_INITIALIZE_ACTION,
target: MANAGED_STORAGE_TARGET,
source: 'manual',
risk: preview.risk,
policy: preview.policyDecision,
reason: preview.auditPreview,
plan: preview.preChecks,
postChecks: preview.postChecks,
};
setInitializing(true);
setActionNotice(null);
try {
const result: unknown = await appopsClient.executeAction(plan);
setActionNotice(normalizeManagedStorageActionOutcome(result));
await load({ preserveDraft: true });
} catch (error) {
setActionNotice({ tone: 'alert', message: formatOperatorError(error, 'Het initialisatieverzoek is niet bevestigd.') });
} finally {
setInitializing(false);
}
};
return (
<Card className="border-slate-700 bg-slate-900/50" aria-busy={refreshing || saving || initializing}>
<CardHeader className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0">
<CardTitle className="flex items-center gap-2 text-sm font-bold uppercase tracking-wider text-slate-300">
<Database className="h-4 w-4 shrink-0 text-blue-400" /> Opslag voor nieuwe targets
</CardTitle>
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-400">
Kies de vertrouwde opslagroot voor toekomstige registraties. Dit wijzigt of verplaatst geen bestaande targetregistraties.
</p>
</div>
<Button type="button" variant="outline" className="w-full shrink-0 border-slate-700 bg-slate-950 text-slate-200 sm:w-auto" onClick={() => void load()} disabled={!canManage || refreshing || saving || initializing}>
{refreshing ? <LoaderCircle className="mr-2 h-4 w-4 animate-spin" aria-hidden="true" /> : <RefreshCw className="mr-2 h-4 w-4" aria-hidden="true" />}
{refreshing ? 'Status vernieuwen…' : 'Vernieuw status'}
</Button>
</CardHeader>
<CardContent className="space-y-5">
{!canManage && (
<p className="text-sm text-slate-400">Alleen een beheerder met de instellingentoestemming kan deze opslagkeuze bekijken of wijzigen.</p>
)}
{canManage && refreshing && !snapshot && <p role="status" aria-live="polite" className="text-sm text-slate-400">Opslagstatus laden…</p>}
{canManage && loadError && (
<div role="alert" className="flex items-start gap-3 rounded-lg border border-amber-500/30 bg-amber-500/5 p-3 text-sm text-amber-100">
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
<div className="min-w-0">
<p>{snapshot ? `De laatste opslagstatus kon niet worden vernieuwd. De waarden hieronder zijn de laatst succesvol gelezen gegevens. ${loadError}` : loadError}</p>
{!snapshot && <Button type="button" variant="outline" className="mt-3 border-amber-500/30" onClick={() => void load()}>Opnieuw proberen</Button>}
</div>
</div>
)}
{snapshot && view && canManage && (
<>
<div className="grid min-w-0 gap-3 rounded-lg border border-slate-800 bg-slate-950/70 p-3 sm:grid-cols-2">
<div className="min-w-0">
<p className="text-xs font-semibold uppercase tracking-wide text-slate-500">Opgeslagen root</p>
<p className="mt-1 break-all text-sm text-slate-100">{view.currentRoot || 'Niet bevestigd door de API'}</p>
<p className="mt-1 text-xs text-slate-400">{view.currentRoot ? labelForRoot(view.currentRoot) : 'De huidige trustconfiguratie kan niet veilig worden gewijzigd.'}</p>
</div>
<div className="min-w-0">
<p className="text-xs font-semibold uppercase tracking-wide text-slate-500">Truststatus</p>
<p className="mt-1 text-sm text-slate-100">
{view.trustStatus === 'ready' ? 'Configuratie gelezen' : view.trustStatus === 'blocked' ? 'Trustconfiguratie heeft andere blockers' : view.trustStatus === 'needs_setup' ? 'Trustconfiguratie ontbreekt' : 'Status onbekend'}
</p>
{view.trustStatus === 'needs_setup' && <p className="mt-1 text-xs text-amber-200">Rond de trustconfiguratie eerst af via Setup.</p>}
</div>
</div>
{view.trustStatus === 'needs_setup' && (
<div className="rounded-lg border border-amber-500/20 bg-amber-500/5 p-3 text-sm text-amber-100">Deze beperkte kaart kan geen volledige trustconfiguratie maken.</div>
)}
{snapshot.view.configured && (
<fieldset disabled={saving || refreshing || initializing} className="min-w-0 space-y-3 disabled:opacity-80">
<legend className="text-sm font-semibold text-slate-200">Vertrouwde opslagroot</legend>
<RadioGroup
aria-label="Vertrouwde opslagroot voor toekomstige targetregistraties"
value={draft.root}
onValueChange={(value) => {
if (value !== LEGACY_APPDATA_ROOT && value !== MANAGED_APPDATA_ROOT) return;
setSaveNotice('');
setConflict('');
updateDraft({ root: value, backupAcknowledged: false });
}}
className="grid min-w-0 gap-3 md:grid-cols-2"
>
<label htmlFor="appdata-root-legacy" className="flex min-w-0 cursor-pointer gap-3 rounded-lg border border-slate-700 bg-slate-950/70 p-3 has-[:disabled]:cursor-not-allowed">
<RadioGroupItem id="appdata-root-legacy" value={LEGACY_APPDATA_ROOT} className="mt-1 shrink-0" />
<span className="min-w-0">
<span className="block text-sm font-semibold text-slate-100">{labelForRoot(LEGACY_APPDATA_ROOT)}</span>
<code className="mt-1 block break-all text-xs text-slate-400">{LEGACY_APPDATA_ROOT}</code>
<span className="mt-2 block text-xs text-slate-300">{rootReadinessLabel(view.rootReadiness[LEGACY_APPDATA_ROOT])}</span>
</span>
</label>
<label htmlFor="appdata-root-managed" className="flex min-w-0 cursor-pointer gap-3 rounded-lg border border-slate-700 bg-slate-950/70 p-3 has-[:disabled]:cursor-not-allowed">
<RadioGroupItem id="appdata-root-managed" value={MANAGED_APPDATA_ROOT} className="mt-1 shrink-0" />
<span className="min-w-0">
<span className="block text-sm font-semibold text-slate-100">{labelForRoot(MANAGED_APPDATA_ROOT)}</span>
<code className="mt-1 block break-all text-xs text-slate-400">{MANAGED_APPDATA_ROOT}</code>
<span className="mt-2 block text-xs text-slate-300">{rootReadinessLabel(view.rootReadiness[MANAGED_APPDATA_ROOT])}</span>
</span>
</label>
</RadioGroup>
</fieldset>
)}
{draftIsManaged && snapshot.view.configured && (
<section aria-labelledby="managed-root-backup-title" className="space-y-3 rounded-lg border border-amber-500/25 bg-amber-500/5 p-3">
<div className="flex items-start gap-3">
<ShieldAlert className="mt-0.5 h-5 w-5 shrink-0 text-amber-300" aria-hidden="true" />
<div className="min-w-0">
<h3 id="managed-root-backup-title" className="text-sm font-semibold text-amber-100">Back-upverantwoordelijkheid voor DockerRootDir</h3>
<p className="mt-1 break-words text-sm leading-6 text-amber-100/80">
Deze vaste root ligt onder <code className="break-all">/var/lib/docker</code> en kan deel uitmaken van <code>docker.img</code>. Gebruikelijke back-ups van <code>/srv/example/appdata</code> nemen dit niet mee. Beheer en herstel van DockerRootDir vallen onder jouw back-upplan.
</p>
</div>
</div>
<div className="flex min-w-0 items-start gap-3 rounded-md border border-amber-500/20 bg-slate-950/60 p-3">
<Checkbox
id="managed-root-backup-ack"
checked={draft.backupAcknowledged}
onCheckedChange={(checked) => {
setSaveNotice('');
setConflict('');
updateDraft({ ...draftRef.current, backupAcknowledged: checked === true });
}}
disabled={saving || refreshing || initializing}
aria-describedby="managed-root-backup-copy"
className="mt-0.5 shrink-0"
/>
<Label htmlFor="managed-root-backup-ack" id="managed-root-backup-copy" className="cursor-pointer text-sm leading-6 text-slate-200">
Ik bevestig dat reguliere appdata-back-ups DockerRootDir en <code>docker.img</code> niet dekken en dat ik daarvoor afzonderlijk back-up- en herstelbeheer regel.
</Label>
</div>
</section>
)}
<div className="rounded-lg border border-slate-800 bg-slate-950/60 p-3">
<p className="text-xs font-semibold uppercase tracking-wide text-slate-500">Controller-readiness van de beheerde volume</p>
<p className="mt-1 text-sm text-slate-100">{managedStorageStatusLabel(view.managedStatus)}</p>
{managedStorageBlockerLabel(view.managedBlocker) && <p className="mt-1 text-xs leading-5 text-amber-200">{managedStorageBlockerLabel(view.managedBlocker)}</p>}
{!view.managedCapabilityValid && <p className="mt-1 text-xs text-amber-200">De controller leverde geen geldige readinessbevestiging; acties blijven geblokkeerd.</p>}
<p className="mt-2 text-xs leading-5 text-slate-400">De rootkeuze geldt voor toekomstige registraties. Bestaande targets blijven aan hun eerder vastgelegde root gekoppeld en worden niet verplaatst.</p>
</div>
{view.managedStatus === 'initialization_required' && (
<div className="space-y-3 rounded-lg border border-slate-800 bg-slate-950/60 p-3">
<div>
<p className="text-sm font-semibold text-slate-100">Eenmalige controlleractie</p>
<p className="mt-1 text-xs leading-5 text-slate-400">Alleen beschikbaar als de exacte handmatige high-risk policy-preview dit toestaat. Een aanvraag is geen bewijs dat de volume klaar is.</p>
</div>
{snapshot.previewError && <p role="alert" className="text-sm text-amber-200">Policy-preview niet beschikbaar: {snapshot.previewError}</p>}
{snapshot.preview && (
<div className="space-y-2 rounded-md border border-slate-800 p-3">
<p className="text-xs font-semibold uppercase tracking-wide text-slate-400">Policy: {snapshot.preview.policyDecision === 'allowed' ? 'Toegestaan' : snapshot.preview.policyDecision === 'approval_required' ? 'Approval vereist' : 'Geblokkeerd'}</p>
<p className="break-words text-sm text-slate-200">{snapshot.preview.auditPreview}</p>
{snapshot.preview.preChecks.length > 0 && (
<ul className="list-disc space-y-1 pl-5 text-xs text-slate-400">
{snapshot.preview.preChecks.map((check, index) => <li key={`${index}-${check}`} className="break-words">{check}</li>)}
</ul>
)}
{(snapshot.preview.action !== MANAGED_STORAGE_INITIALIZE_ACTION || snapshot.preview.target !== MANAGED_STORAGE_TARGET) && (
<p role="alert" className="text-xs text-rose-200">De preview bevestigt niet de vaste actie en controllerdoel; uitvoering blijft geblokkeerd.</p>
)}
</div>
)}
{initializeAllowed && (
<Button type="button" onClick={() => void initialize()} disabled={initializing || saving || refreshing} className="w-full sm:w-auto">
{initializing ? <LoaderCircle className="mr-2 h-4 w-4 animate-spin" aria-hidden="true" /> : <ShieldAlert className="mr-2 h-4 w-4" aria-hidden="true" />}
{initializing ? 'Actieverzoek indienen…' : 'Vraag volume-initialisatie aan'}
</Button>
)}
</div>
)}
{actionNotice && (
<div role={actionNotice.tone === 'alert' ? 'alert' : 'status'} aria-live={actionNotice.tone === 'status' ? 'polite' : undefined} className={`rounded-lg border p-3 text-sm ${actionNotice.tone === 'alert' ? 'border-amber-500/30 bg-amber-500/5 text-amber-100' : 'border-blue-500/25 bg-blue-500/5 text-blue-100'}`}>
{actionNotice.message}
</div>
)}
{conflict && <div role="alert" className="rounded-lg border border-amber-500/30 bg-amber-500/5 p-3 text-sm text-amber-100">{conflict}</div>}
{saveError && <div role="alert" className="rounded-lg border border-rose-500/30 bg-rose-500/5 p-3 text-sm text-rose-100">{saveError}</div>}
{saveNotice && <div role="status" aria-live="polite" className="flex items-start gap-2 rounded-lg border border-emerald-500/25 bg-emerald-500/5 p-3 text-sm text-emerald-100"><CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0" aria-hidden="true" />{saveNotice}</div>}
{view.configured && canManage && (
<div className="flex flex-col gap-3 border-t border-slate-800 pt-4 sm:flex-row sm:items-center sm:justify-between">
<p className="text-xs text-slate-400">Opslaan wijzigt alleen de trustinstelling voor volgende registraties.</p>
<Button type="button" onClick={() => void save()} disabled={saveDisabled} className="w-full sm:w-auto">
{saving ? <LoaderCircle className="mr-2 h-4 w-4 animate-spin" aria-hidden="true" /> : null}
{saving ? 'Opslaan…' : dirty ? 'Opslagkeuze opslaan' : 'Geen wijzigingen'}
</Button>
</div>
)}
</>
)}
</CardContent>
</Card>
);
};
+216
View File
@@ -0,0 +1,216 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
buildEnrollmentStorageView,
buildStorageRootSelection,
canInitializeManagedStorage,
canRequestManagedStorageInitialization,
canSaveStorageRootSelection,
LEGACY_APPDATA_ROOT,
MANAGED_APPDATA_ROOT,
MANAGED_STORAGE_INITIALIZE_ACTION,
MANAGED_STORAGE_TARGET,
normalizeManagedStoragePreview,
normalizeManagedStorageActionOutcome,
} from './enrollmentStorageView.ts';
const baseTrust = {
configured: true,
status: 'ready',
trustedAppdataRoot: MANAGED_APPDATA_ROOT,
managedAppdataBackupAcknowledged: true,
};
const baseCapabilities = {
availableTrustedAppdataRoots: [
{ root: LEGACY_APPDATA_ROOT, status: 'ready' },
{ root: MANAGED_APPDATA_ROOT, status: 'needs_initialization' },
],
managedStorage: {
status: 'initialization_required',
blockerCode: 'managed_appdata_initialization_required',
root: MANAGED_APPDATA_ROOT,
backupScope: 'dockerRootDir',
backupAcknowledged: true,
},
};
test('malformed or unknown roots fail closed without echoing untrusted paths', () => {
const view = buildEnrollmentStorageView({ ...baseTrust, trustedAppdataRoot: '/tmp/unknown-secret-path' }, {
...baseCapabilities,
availableTrustedAppdataRoots: [{ root: '/tmp/private', status: 'ready' }, ...baseCapabilities.availableTrustedAppdataRoots],
});
assert.equal(view.currentRoot, null);
assert.equal(view.configured, false);
assert.equal(view.rootReadiness[LEGACY_APPDATA_ROOT], 'unavailable');
assert.equal(view.rootReadiness[MANAGED_APPDATA_ROOT], 'unavailable');
assert.equal(view.managedStatus, 'unavailable');
assert.equal(view.managedCapabilityValid, false);
});
test('readiness and saved selection fail closed when root evidence or acknowledgement contradicts', () => {
const unavailableManagedRoot = buildEnrollmentStorageView(baseTrust, {
...baseCapabilities,
availableTrustedAppdataRoots: [
{ root: LEGACY_APPDATA_ROOT, status: 'ready' },
{ root: MANAGED_APPDATA_ROOT, status: 'unavailable' },
],
managedStorage: { ...baseCapabilities.managedStorage, status: 'ready' },
});
assert.equal(unavailableManagedRoot.managedStatus, 'unavailable');
assert.equal(unavailableManagedRoot.managedCapabilityValid, false);
const contradictoryManagedAcknowledgement = buildEnrollmentStorageView(baseTrust, {
...baseCapabilities,
managedStorage: { ...baseCapabilities.managedStorage, backupAcknowledged: false },
});
assert.equal(contradictoryManagedAcknowledgement.configured, false);
assert.equal(contradictoryManagedAcknowledgement.currentRoot, null);
assert.equal(contradictoryManagedAcknowledgement.trustStatus, 'unknown');
assert.equal(contradictoryManagedAcknowledgement.managedStatus, 'unavailable');
const malformedRootEvidence = buildEnrollmentStorageView(baseTrust, {
...baseCapabilities,
availableTrustedAppdataRoots: [{ root: LEGACY_APPDATA_ROOT, status: 'ready' }],
managedStorage: { ...baseCapabilities.managedStorage, status: 'ready' },
});
assert.equal(malformedRootEvidence.managedStatus, 'unavailable');
});
test('managed-root choice requires explicit backup acknowledgement and sends only the two contract fields', () => {
assert.equal(buildStorageRootSelection(MANAGED_APPDATA_ROOT, false), null);
assert.deepEqual(buildStorageRootSelection(MANAGED_APPDATA_ROOT, true), {
trusted_appdata_root: MANAGED_APPDATA_ROOT,
managed_appdata_backup_acknowledged: true,
});
assert.deepEqual(buildStorageRootSelection(LEGACY_APPDATA_ROOT, true), {
trusted_appdata_root: LEGACY_APPDATA_ROOT,
managed_appdata_backup_acknowledged: false,
});
});
test('a failed refresh disables a dirty storage-root draft from being saved', () => {
const gate = {
canManage: true,
configured: true,
payloadAvailable: true,
dirty: true,
loadError: '',
refreshing: false,
saving: false,
initializing: false,
};
assert.equal(canSaveStorageRootSelection(gate), true);
assert.equal(canSaveStorageRootSelection({ ...gate, loadError: 'Refresh failed; showing last-known configuration.' }), false);
assert.equal(canSaveStorageRootSelection({ ...gate, refreshing: true }), false);
assert.equal(canSaveStorageRootSelection({ ...gate, initializing: true }), false);
});
test('initialization requires selected managed root, capability evidence, exact high-risk action and allowed manual policy', () => {
const view = buildEnrollmentStorageView(baseTrust, baseCapabilities);
const preview = {
action: MANAGED_STORAGE_INITIALIZE_ACTION,
target: MANAGED_STORAGE_TARGET,
risk: 'high',
policyDecision: 'allowed',
};
assert.equal(canInitializeManagedStorage(view, preview), true);
assert.equal(canInitializeManagedStorage(view, { ...preview, policyDecision: 'blocked' }), false);
assert.equal(canInitializeManagedStorage(view, { ...preview, target: 'tower/other' }), false);
assert.equal(canInitializeManagedStorage(view, { ...preview, risk: 'low' }), false);
assert.equal(canInitializeManagedStorage(view, { ...view, policyDecision: 'allowed' }), false);
assert.equal(canInitializeManagedStorage(
buildEnrollmentStorageView({ ...baseTrust, managedAppdataBackupAcknowledged: false }, baseCapabilities),
preview,
), false);
assert.equal(canInitializeManagedStorage(
buildEnrollmentStorageView(baseTrust, { ...baseCapabilities, managedStorage: { ...baseCapabilities.managedStorage, status: 'ready' } }),
preview,
), false);
});
test('initialization stays disabled during stale refresh, conflict recovery, dirty drafts and mutations', () => {
const view = buildEnrollmentStorageView(baseTrust, baseCapabilities);
const gate = {
canManage: true,
view,
preview: {
action: MANAGED_STORAGE_INITIALIZE_ACTION,
target: MANAGED_STORAGE_TARGET,
risk: 'high',
policyDecision: 'allowed',
},
dirty: false,
loadError: '',
conflict: '',
refreshing: false,
saving: false,
initializing: false,
};
assert.equal(canRequestManagedStorageInitialization(gate), true);
assert.equal(canRequestManagedStorageInitialization({ ...gate, loadError: 'Refresh failed; showing last known data.' }), false);
assert.equal(canRequestManagedStorageInitialization({ ...gate, conflict: 'The saved selection changed.' }), false);
assert.equal(canRequestManagedStorageInitialization({ ...gate, dirty: true }), false);
assert.equal(canRequestManagedStorageInitialization({ ...gate, refreshing: true }), false);
assert.equal(canRequestManagedStorageInitialization({ ...gate, saving: true }), false);
assert.equal(canRequestManagedStorageInitialization({ ...gate, initializing: true }), false);
assert.equal(canRequestManagedStorageInitialization({ ...gate, canManage: false }), false);
});
test('malformed runtime policy preview is rejected before UI uses its arrays or text', () => {
assert.equal(normalizeManagedStoragePreview({
action: MANAGED_STORAGE_INITIALIZE_ACTION,
target: MANAGED_STORAGE_TARGET,
risk: 'high',
policyDecision: 'allowed',
auditPreview: 'Initialize managed storage',
affectedApps: [],
dependencies: [],
preChecks: 'not-an-array',
postChecks: [],
}), null);
assert.equal(normalizeManagedStoragePreview({
action: MANAGED_STORAGE_INITIALIZE_ACTION,
target: MANAGED_STORAGE_TARGET,
risk: 'high',
policyDecision: 'allowed',
auditPreview: 42,
affectedApps: [],
dependencies: [],
preChecks: [],
postChecks: [],
}), null);
});
test('malformed initializer results never produce a success notice or stringify arbitrary messages', () => {
const malformedResults: unknown[] = [
{ success: true, jobId: {}, auditId: 'audit-1', message: 'Started' },
{ success: true, jobId: 'job-1', auditId: ' ', message: 'Started' },
{ success: true, jobId: `${' '.repeat(201)}job-1`, auditId: 'audit-1', message: 'Started' },
{ success: true, jobId: 'job-1', auditId: `${' '.repeat(201)}audit-1`, message: 'Started' },
{ success: true, jobId: 'job-1', auditId: 'audit-1', message: { detail: 'Started' } },
{ success: true, jobId: 'job-1', auditId: 'audit-1' },
{ success: 'true', jobId: 'job-1', auditId: 'audit-1', message: 'Started' },
{ success: false, message: { detail: 'Denied' } },
];
for (const result of malformedResults) {
assert.deepEqual(normalizeManagedStorageActionOutcome(result), {
tone: 'alert',
message: 'Het initialisatieverzoek is niet bevestigd met geldige referenties.',
});
}
assert.deepEqual(normalizeManagedStorageActionOutcome({
success: true,
jobId: 'job-123',
auditId: 'audit-456',
message: 'Accepted',
}), {
tone: 'status',
message: 'Initialisatieverzoek geregistreerd. Job job-123; audit audit-456. De opslag wordt pas als gereed getoond nadat de controller dat bevestigt.',
});
});
+277
View File
@@ -0,0 +1,277 @@
import type {
PreflightPreview,
EnrollmentStorageRootSelection,
ManagedStorageStatus,
TrustedAppdataRoot,
TrustedAppdataRootReadinessStatus,
} from '@/types/appops';
export const LEGACY_APPDATA_ROOT: TrustedAppdataRoot = '/srv/example/appdata';
export const MANAGED_APPDATA_ROOT: TrustedAppdataRoot = '/var/lib/docker/volumes/itworx-managed-appdata/_data';
export const MANAGED_STORAGE_INITIALIZE_ACTION = 'controller.managed-storage.initialize';
export const MANAGED_STORAGE_TARGET = 'tower/deployment-controller';
export interface EnrollmentStorageView {
configured: boolean;
trustStatus: 'ready' | 'blocked' | 'needs_setup' | 'unknown';
currentRoot: TrustedAppdataRoot | null;
backupAcknowledged: boolean;
rootReadiness: Record<TrustedAppdataRoot, TrustedAppdataRootReadinessStatus>;
managedStatus: ManagedStorageStatus;
managedBlocker: string | null;
managedCapabilityValid: boolean;
}
const isRecord = (value: unknown): value is Record<string, unknown> => (
typeof value === 'object' && value !== null && !Array.isArray(value)
);
const isRoot = (value: unknown): value is TrustedAppdataRoot => (
value === LEGACY_APPDATA_ROOT || value === MANAGED_APPDATA_ROOT
);
const isRootReadiness = (value: unknown): value is TrustedAppdataRootReadinessStatus => (
value === 'ready' || value === 'needs_initialization' || value === 'unavailable'
);
const isManagedStatus = (value: unknown): value is ManagedStorageStatus => (
value === 'not_selected' || value === 'blocked' || value === 'ready' || value === 'initialization_required' || value === 'unavailable'
);
export const buildEnrollmentStorageView = (trust: unknown, capabilities: unknown): EnrollmentStorageView => {
const safeTrust = isRecord(trust) ? trust : {};
const safeCapabilities = isRecord(capabilities) ? capabilities : {};
const trustedRoot = isRoot(safeTrust.trustedAppdataRoot) ? safeTrust.trustedAppdataRoot : null;
const managed = isRecord(safeCapabilities.managedStorage) ? safeCapabilities.managedStorage : null;
const reportedRoots = safeCapabilities.availableTrustedAppdataRoots;
let rootReadiness: Record<TrustedAppdataRoot, TrustedAppdataRootReadinessStatus> = {
[LEGACY_APPDATA_ROOT]: 'unavailable',
[MANAGED_APPDATA_ROOT]: 'unavailable',
};
let rootsValid = false;
if (Array.isArray(reportedRoots) && reportedRoots.length === 2) {
const entries = new Map<TrustedAppdataRoot, TrustedAppdataRootReadinessStatus>();
const valid = reportedRoots.every((entry) => {
if (!isRecord(entry) || !isRoot(entry.root) || !isRootReadiness(entry.status) || entries.has(entry.root)) return false;
entries.set(entry.root, entry.status);
return true;
});
if (valid && entries.has(LEGACY_APPDATA_ROOT) && entries.has(MANAGED_APPDATA_ROOT)) {
rootsValid = true;
rootReadiness = {
[LEGACY_APPDATA_ROOT]: entries.get(LEGACY_APPDATA_ROOT)!,
[MANAGED_APPDATA_ROOT]: entries.get(MANAGED_APPDATA_ROOT)!,
};
}
}
const backupAcknowledged = safeTrust.managedAppdataBackupAcknowledged;
const trustSelectionConsistent = typeof backupAcknowledged === 'boolean' && (
trustedRoot === MANAGED_APPDATA_ROOT
? backupAcknowledged && managed?.root === MANAGED_APPDATA_ROOT && managed.backupAcknowledged === backupAcknowledged
: trustedRoot === LEGACY_APPDATA_ROOT && !backupAcknowledged
);
const managedStatusConsistent = Boolean(
managed && (
(managed.status !== 'ready' || rootReadiness[MANAGED_APPDATA_ROOT] === 'ready') &&
(managed.status !== 'initialization_required' || rootReadiness[MANAGED_APPDATA_ROOT] === 'needs_initialization') &&
(trustedRoot !== MANAGED_APPDATA_ROOT || managed.status !== 'not_selected') &&
(trustedRoot !== MANAGED_APPDATA_ROOT || managed.backupAcknowledged === backupAcknowledged)
)
);
const managedCapabilityValid = Boolean(
rootsValid &&
trustSelectionConsistent &&
managed &&
managed.root === MANAGED_APPDATA_ROOT &&
managed.backupScope === 'dockerRootDir' &&
typeof managed.backupAcknowledged === 'boolean' &&
isManagedStatus(managed.status) &&
managedStatusConsistent,
);
const rawTrustStatus = safeTrust.status === 'ready' || safeTrust.status === 'blocked' || safeTrust.status === 'needs_setup'
? safeTrust.status
: 'unknown';
const configured = safeTrust.configured === true && trustedRoot !== null && trustSelectionConsistent;
const trustStatus = rawTrustStatus === 'needs_setup' || rawTrustStatus === 'blocked'
? rawTrustStatus
: configured && rawTrustStatus === 'ready' ? 'ready' : 'unknown';
return {
configured,
trustStatus,
currentRoot: configured ? trustedRoot : null,
backupAcknowledged: backupAcknowledged === true,
rootReadiness,
managedStatus: managedCapabilityValid ? managed!.status as ManagedStorageStatus : 'unavailable',
managedBlocker: managedCapabilityValid && typeof managed!.blockerCode === 'string' && managed!.blockerCode.length <= 100
? managed!.blockerCode || null
: null,
managedCapabilityValid,
};
};
const isBoundedStringArray = (value: unknown): value is string[] => (
Array.isArray(value) && value.length <= 100 && value.every((item) => typeof item === 'string' && item.length <= 500)
);
export const normalizeManagedStoragePreview = (value: unknown): PreflightPreview | null => {
if (!isRecord(value)) return null;
const rollbackAvailable = value.rollbackAvailable;
if (
typeof value.action !== 'string' || value.action.length > 120 ||
typeof value.target !== 'string' || value.target.length > 200 ||
(value.risk !== 'low' && value.risk !== 'medium' && value.risk !== 'high' && value.risk !== 'critical') ||
(value.policyDecision !== 'allowed' && value.policyDecision !== 'approval_required' && value.policyDecision !== 'blocked') ||
typeof value.auditPreview !== 'string' || value.auditPreview.length > 2000 ||
!isBoundedStringArray(value.affectedApps) ||
!isBoundedStringArray(value.dependencies) ||
!isBoundedStringArray(value.preChecks) ||
!isBoundedStringArray(value.postChecks) ||
(rollbackAvailable !== undefined && typeof rollbackAvailable !== 'boolean')
) return null;
const normalizedRollbackAvailable: boolean | undefined = typeof rollbackAvailable === 'boolean'
? rollbackAvailable
: undefined;
return {
action: value.action,
target: value.target,
risk: value.risk,
policyDecision: value.policyDecision,
auditPreview: value.auditPreview,
affectedApps: value.affectedApps,
dependencies: value.dependencies,
preChecks: value.preChecks,
postChecks: value.postChecks,
...(normalizedRollbackAvailable === undefined ? {} : { rollbackAvailable: normalizedRollbackAvailable }),
};
};
export const buildStorageRootSelection = (
root: TrustedAppdataRoot,
backupAcknowledged: boolean,
): EnrollmentStorageRootSelection | null => {
if (root === MANAGED_APPDATA_ROOT && !backupAcknowledged) return null;
return {
trusted_appdata_root: root,
managed_appdata_backup_acknowledged: root === MANAGED_APPDATA_ROOT,
};
};
export const canInitializeManagedStorage = (
view: EnrollmentStorageView,
preview: unknown,
): boolean => (
view.configured &&
view.currentRoot === MANAGED_APPDATA_ROOT &&
view.backupAcknowledged &&
view.managedCapabilityValid &&
view.managedStatus === 'initialization_required' &&
isRecord(preview) &&
preview.action === MANAGED_STORAGE_INITIALIZE_ACTION &&
preview.target === MANAGED_STORAGE_TARGET &&
preview.risk === 'high' &&
preview.policyDecision === 'allowed'
);
export interface ManagedStorageInitializationGate {
canManage: boolean;
view: EnrollmentStorageView | null | undefined;
preview: unknown;
dirty: boolean;
loadError: string;
conflict: string;
refreshing: boolean;
saving: boolean;
initializing: boolean;
}
export const canRequestManagedStorageInitialization = (gate: ManagedStorageInitializationGate): boolean => (
gate.canManage &&
Boolean(gate.view && canInitializeManagedStorage(gate.view, gate.preview)) &&
!gate.dirty &&
!gate.loadError &&
!gate.conflict &&
!gate.refreshing &&
!gate.saving &&
!gate.initializing
);
export interface StorageRootSaveGate {
canManage: boolean;
configured: boolean;
payloadAvailable: boolean;
dirty: boolean;
loadError: string;
refreshing: boolean;
saving: boolean;
initializing: boolean;
}
export const canSaveStorageRootSelection = (gate: StorageRootSaveGate): boolean => (
gate.canManage &&
gate.configured &&
gate.payloadAvailable &&
gate.dirty &&
!gate.loadError &&
!gate.refreshing &&
!gate.saving &&
!gate.initializing
);
export interface ManagedStorageActionOutcome {
tone: 'status' | 'alert';
message: string;
}
const UNCONFIRMED_INITIALIZATION_MESSAGE = 'Het initialisatieverzoek is niet bevestigd met geldige referenties.';
const isBoundedIdentifier = (value: unknown): value is string => (
typeof value === 'string' && value.trim().length > 0 && value.length <= 200
);
export const normalizeManagedStorageActionOutcome = (value: unknown): ManagedStorageActionOutcome => {
if (!isRecord(value) || typeof value.message !== 'string' || value.message.trim().length === 0 || value.message.length > 500) {
return { tone: 'alert', message: UNCONFIRMED_INITIALIZATION_MESSAGE };
}
if (value.success === true && isBoundedIdentifier(value.jobId) && isBoundedIdentifier(value.auditId)) {
return {
tone: 'status',
message: `Initialisatieverzoek geregistreerd. Job ${value.jobId.trim()}; audit ${value.auditId.trim()}. De opslag wordt pas als gereed getoond nadat de controller dat bevestigt.`,
};
}
if (value.success === false) {
return { tone: 'alert', message: value.message.trim() };
}
return { tone: 'alert', message: UNCONFIRMED_INITIALIZATION_MESSAGE };
};
export const rootReadinessLabel = (status: TrustedAppdataRootReadinessStatus): string => {
if (status === 'ready') return 'Controller confirms ready';
if (status === 'needs_initialization') return 'Controller reports initialization needed';
return 'Not currently confirmed';
};
export const managedStorageStatusLabel = (status: ManagedStorageStatus): string => {
switch (status) {
case 'not_selected': return 'Not selected for future targets';
case 'blocked': return 'Blocked by a required safety condition';
case 'ready': return 'Controller reports ready';
case 'initialization_required': return 'Controller requires one-time initialization';
default: return 'Unavailable or not confirmed';
}
};
export const managedStorageBlockerLabel = (blocker: string | null): string | null => {
switch (blocker) {
case 'managed_appdata_backup_acknowledgement_required': return 'Confirm the Docker-root backup responsibility before continuing.';
case 'managed_appdata_initialization_required': return 'The controller reports that the managed storage volume needs initialization.';
case 'managed_appdata_volume_unavailable': return 'The controller cannot currently verify the managed storage volume.';
default: return blocker ? 'The controller reports an additional storage blocker.' : null;
}
};
+2
View File
@@ -15,6 +15,7 @@ import { usePermission } from "@/hooks/useUserSession";
import { UserAccessSettings } from "@/components/UserAccessSettings";
import { ExposureSettings } from "@/components/ExposureSettings";
import { ManagedAiSettings } from "@/components/ManagedAiSettings";
import { ManagedStorageSettings } from "@/components/ManagedStorageSettings";
const settingsTabs = ["general", "security", "ai", "notifications", "audit", "voice", "maintenance", "backups"] as const;
type SettingsTab = typeof settingsTabs[number];
@@ -194,6 +195,7 @@ const Settings = () => {
</div>
<TabsContent value="general">
<ManagedStorageSettings />
<Card className="signal-rail border-slate-700 bg-slate-900/50">
<CardHeader>
<CardTitle className="text-sm font-bold uppercase tracking-wider text-slate-400">Runtime Profiel</CardTitle>
+28
View File
@@ -121,9 +121,37 @@ export interface EnrollmentCapabilities {
policyProvisioning: EnrollmentCapability;
rollback: EnrollmentCapability;
verification: EnrollmentCapability;
availableTrustedAppdataRoots?: TrustedAppdataRootReadiness[];
managedStorage?: ManagedStorageCapability;
profiles: Array<{ name: string; version: string; public: boolean; authentication: string }>;
}
export type TrustedAppdataRoot = '/srv/example/appdata' | '/var/lib/docker/volumes/itworx-managed-appdata/_data';
export type TrustedAppdataRootReadinessStatus = 'ready' | 'needs_initialization' | 'unavailable';
export interface TrustedAppdataRootReadiness {
root: TrustedAppdataRoot;
status: TrustedAppdataRootReadinessStatus;
}
export type ManagedStorageStatus = 'not_selected' | 'blocked' | 'ready' | 'initialization_required' | 'unavailable';
export interface ManagedStorageCapability {
status: ManagedStorageStatus;
blockerCode?: string;
root: '/var/lib/docker/volumes/itworx-managed-appdata/_data';
backupScope: 'dockerRootDir';
backupAcknowledged: boolean;
}
export interface EnrollmentTrustStatus {
status: 'ready' | 'blocked' | 'needs_setup' | string;
blockers?: string[];
configured?: boolean;
trustedAppdataRoot: TrustedAppdataRoot | string;
managedAppdataBackupAcknowledged: boolean;
}
export interface EnrollmentStorageRootSelection {
trusted_appdata_root: TrustedAppdataRoot;
managed_appdata_backup_acknowledged: boolean;
}
export interface Enrollment {
id: string;
applicationId: string;