+
+
+Each row maps one compose concept to the value it resolves to right now:
+
+- **Services** — Cyan pills, one per key under `services:`.
+- **Ports** — `{host} → {container}/{proto}` per service, host port highlighted.
+- **Volumes** — `{host-path} → {container-path}` per mount.
+- **Restart** — The effective restart policy.
+- **Env file** — Filename plus a count of defined variables. If the compose file references a `${VAR}` that the env file does not define and has no `:-default`, the missing name is called out inline in rose italic.
+- **Network** — Derived network name and driver (default `{stack}_default · bridge`).
+- **Source** — `git · {host}/{repo}#{branch}` when Git Source is configured, else `local`. The whole row is clickable and opens the Git Source dialog. A pulsing cyan dot appears when upstream has moved ahead of the working copy.
+
+If an image update is available for the primary service, an inline banner appears below the rows with the version bump (`27.1.4 → 27.1.5`), risk classification (`safe · patch`, `minor`, or `major · review required`), and an **apply** button that runs the update against the current scheduled-task pipeline. Major bumps show a rose banner and require explicit review before applying.
+
+### Editing compose.yaml
+
+Anatomy is the default surface. When you need to change the compose file, click **edit compose.yaml** in the anatomy header. The Monaco editor slides in over the anatomy panel with the full editing toolbar (tabs for `compose.yaml` and `.env`, Git Source shortcut, Save / Save & Deploy). Close the editor with the `×` button in its header to return to the anatomy view; unsaved changes are discarded when you close.
+
## Deploying a stack
Select a stack and click **Start** (or **Deploy** from the context menu) in the stack header. This runs `docker compose up -d`, pulling images if needed and creating or recreating containers.
diff --git a/docs/images/stack-view/anatomy-panel.png b/docs/images/stack-view/anatomy-panel.png
new file mode 100644
index 00000000..5f6e0ebf
Binary files /dev/null and b/docs/images/stack-view/anatomy-panel.png differ
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 8568d737..9207940c 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -49,7 +49,8 @@
"react-use-measure": "^2.1.7",
"recharts": "^3.8.1",
"tailwind-merge": "^3.5.0",
- "tailwindcss-animate": "^1.0.7"
+ "tailwindcss-animate": "^1.0.7",
+ "yaml": "^2.8.3"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
@@ -7187,6 +7188,21 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/yaml": {
+ "version": "2.8.3",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
+ "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
+ "license": "ISC",
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/eemeli"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 21def523..c39a4d51 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -53,7 +53,8 @@
"react-use-measure": "^2.1.7",
"recharts": "^3.8.1",
"tailwind-merge": "^3.5.0",
- "tailwindcss-animate": "^1.0.7"
+ "tailwindcss-animate": "^1.0.7",
+ "yaml": "^2.8.3"
},
"overrides": {
"dompurify": "^3.3.3"
diff --git a/frontend/src/components/EditorLayout.tsx b/frontend/src/components/EditorLayout.tsx
index 8d5a0caf..8cff9f78 100644
--- a/frontend/src/components/EditorLayout.tsx
+++ b/frontend/src/components/EditorLayout.tsx
@@ -50,6 +50,7 @@ import { GitSourcePanel } from './stack/GitSourcePanel';
import { AppStoreView } from './AppStoreView';
import { LogViewer } from './LogViewer';
import StructuredLogViewer from './StructuredLogViewer';
+import StackAnatomyPanel from './StackAnatomyPanel';
import { Sparkline } from './ui/sparkline';
import { GlobalObservabilityView } from './GlobalObservabilityView';
import { FleetView } from './FleetView';
@@ -314,6 +315,7 @@ export default function EditorLayout() {
const [activeView, setActiveView] = useState<'dashboard' | 'editor' | 'host-console' | 'resources' | 'templates' | 'global-observability' | 'fleet' | 'audit-log' | 'scheduled-ops' | 'auto-updates' | 'security-history'>('dashboard');
const [filterNodeId, setFilterNodeId] = useState