+
+
+Each row includes:
+
+- **Health badge.** A colored glyph reports the Docker healthcheck state: `✓` green for passing, `✗` red for failing, `…` amber while the healthcheck start period is still in flight. Containers without a `healthcheck:` block show a neutral `✓`.
+- **Container name** in mono.
+- **Meta line.** Uptime (`up 20 hours`), healthcheck state when one is defined, and the primary port mapping (`6875 → 80/tcp`).
+- **Open link.** When the container publishes a port, an `open ↗` link launches the app in a new tab using the active node's hostname.
+- **Live stat tiles.** Three tiles show CPU, memory, and network I/O with a rolling 60-sample sparkline. The sparkline uses the cyan data color and refreshes roughly every 1.5 seconds.
+- **Action icons.** Shortcuts to open this container's logs or a bash shell.
+
+## Logs viewer
+
+The logs area at the bottom of the stack view has two modes. Toggle between them with the segmented control in the top-right of the panel; your choice is remembered for next time.
+
+**Structured** (default) parses every line into a DOM row with three columns: local time, level badge, and message. Level detection is automatic:
+
+- `err` rows get a red left rail and a subtle rose tint.
+- `warn` rows get a warm tint.
+- `info` rows render plainly.
+
+The toolbar includes:
+
+- **Following indicator.** A pulsing green dot reads `following` while auto-scroll is engaged. Scrolling up stops follow; a `resume follow` link re-engages it and jumps to the bottom.
+- **Level filter.** `all · info · warn · err` pills. The `err` pill shows the running error count as a badge.
+- **Download.** Exports the current buffer as a plain text file.
+
+The structured viewer holds up to 10,000 lines; older entries are dropped from the top as new ones arrive.
+
+**Raw terminal** keeps the original xterm-based viewer for cases where ANSI art, control sequences, or interactive TTY output matter. Lines include the raw ISO timestamp.
+
## 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/health-strip-logs.png b/docs/images/stack-view/health-strip-logs.png
new file mode 100644
index 00000000..402d4624
Binary files /dev/null and b/docs/images/stack-view/health-strip-logs.png differ
diff --git a/frontend/src/components/EditorLayout.tsx b/frontend/src/components/EditorLayout.tsx
index fdb51aa8..8d5a0caf 100644
--- a/frontend/src/components/EditorLayout.tsx
+++ b/frontend/src/components/EditorLayout.tsx
@@ -37,7 +37,6 @@ import { Checkbox } from './ui/checkbox';
import { GitSourceFields, type ApplyMode } from './stack/GitSourceFields';
import { Skeleton } from './ui/skeleton';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip';
-import { HoverCard, HoverCardContent, HoverCardTrigger } from './ui/hover-card';
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from './ui/dropdown-menu';
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger } from './ui/context-menu';
@@ -50,6 +49,8 @@ import { StackAutoHealSheet } from '@/components/StackAutoHealSheet';
import { GitSourcePanel } from './stack/GitSourcePanel';
import { AppStoreView } from './AppStoreView';
import { LogViewer } from './LogViewer';
+import StructuredLogViewer from './StructuredLogViewer';
+import { Sparkline } from './ui/sparkline';
import { GlobalObservabilityView } from './GlobalObservabilityView';
import { FleetView } from './FleetView';
import { AuditLogView } from './AuditLogView';
@@ -104,6 +105,23 @@ const formatBytes = (bytes: number) => {
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
+// Extract the "up X time" portion from a Docker status string like
+// "Up 12 days (healthy)" → "up 12 days". Returns null when the container
+// is not in an uptime-reporting state (exited, created, restarting, etc.).
+const extractUptime = (status: string | undefined): string | null => {
+ if (!status) return null;
+ const match = status.match(/^\s*Up\s+(.+?)(?:\s*\(.*\))?\s*$/i);
+ if (!match) return null;
+ return `up ${match[1].trim()}`;
+};
+
+const healthcheckLabel = (health?: 'healthy' | 'unhealthy' | 'starting' | 'none'): string | null => {
+ if (!health || health === 'none') return null;
+ if (health === 'healthy') return 'healthcheck passing';
+ if (health === 'unhealthy') return 'healthcheck failing';
+ return 'healthcheck starting';
+};
+
type StackPill = { label: string; dotClass: string; className: string; pulse: boolean };
const getStackStatePill = (containers: ContainerInfo[]): StackPill | null => {
@@ -184,15 +202,39 @@ export default function EditorLayout() {
const [envFiles, setEnvFiles] = useState- {container?.Status || 'No status details available'} -
-