feat: per-stack storage inventory and portability guardrails (#1399)

* feat: per-stack storage inventory and portability guardrails

Add a Storage tab to the stack Anatomy panel that derives a per-stack mount
inventory (bind mounts, named/anonymous volumes, tmpfs, docker socket;
read-only vs read-write; host-path existence, type, and owner) from the
effective Compose model, and classifies the stack as Portable, Partially
portable, Node-bound, or Unknown with the reasons behind it.

- New GET /api/stacks/:stackName/storage route (stack:read, Community), served
  by an on-demand, non-persisted service that renders the effective model,
  probes within-stack bind sources (symlink-escape aware), and runs the
  deterministic portability classifier.
- Extend the effective-model parser additively with a full per-mount inventory
  and service-level tmpfs, leaving the rule-facing binds/namedVolumes
  byte-identical for the existing preflight rules.
- New anonymous-volume preflight finding.
- Admin-visible "no recent snapshot" warning that reuses the existing hub-local
  snapshot-coverage endpoint, plus a static note distinguishing config
  snapshots from application-data backups.
- Surface storage assumptions in the Stack Dossier markdown export.
- Gate the tab behind a new compose-storage capability on both sides.

* docs: phrase the Storage tab availability as current behavior

Replace the "older Sencho version / until it is updated" wording in the
Storage feature page with present-tense, capability-based phrasing.
This commit is contained in:
Anso
2026-06-20 15:06:26 -04:00
committed by GitHub
parent 57a0856ffc
commit 9ea2864d60
26 changed files with 1591 additions and 12 deletions
@@ -12,6 +12,7 @@ import { StackActivityTimeline } from './stack/StackActivityTimeline';
import StackDossierPanel from './stack/StackDossierPanel';
import DriftPanel from './stack/DriftPanel';
import PreflightPanel from './stack/PreflightPanel';
import StoragePanel from './stack/StoragePanel';
import EnvironmentPanel from './stack/EnvironmentPanel';
import StackNetworkingPanel from './stack/StackNetworkingPanel';
import { useNodes } from '@/context/NodeContext';
@@ -93,6 +94,7 @@ export default function StackAnatomyPanel({
const { hasCapability, activeNode } = useNodes();
const doctorEnabled = hasCapability('compose-doctor');
const networkingEnabled = hasCapability('compose-networking');
const storageEnabled = hasCapability('compose-storage');
const envInventoryEnabled = hasCapability('env-inventory');
const [gitSource, setGitSource] = useState<{ stack: string; info: GitSourceInfo; multiFile: boolean } | null>(null);
@@ -370,6 +372,9 @@ export default function StackAnatomyPanel({
</span>
</TabsTrigger>
)}
{storageEnabled && (
<TabsTrigger value="storage" data-testid="storage-tab" className="h-6 px-2.5 font-mono text-[10px] uppercase tracking-[0.18em]">Storage</TabsTrigger>
)}
</TabsList>
</ScrollableTabRow>
<div className="flex items-center gap-3 shrink-0">
@@ -608,6 +613,11 @@ export default function StackAnatomyPanel({
<PreflightPanel stackName={stackName} />
</TabsContent>
)}
{storageEnabled && (
<TabsContent value="storage" className="flex flex-col flex-1 min-h-0 mt-0">
<StoragePanel stackName={stackName} />
</TabsContent>
)}
</Tabs>
</div>
);