diff --git a/docs/features/alerts-notifications.mdx b/docs/features/alerts-notifications.mdx
index bc949794..97043bb4 100644
--- a/docs/features/alerts-notifications.mdx
+++ b/docs/features/alerts-notifications.mdx
@@ -167,7 +167,11 @@ Each stack carries its own set of threshold rules that fire when a metric stays
Rules live on the node where the stack runs and are evaluated locally on a 30-second tick.
-Open the rules editor by right-clicking a stack in the sidebar and choosing **Alerts**, or by pressing `A` while focused on a stack.
+Open the rules editor from any of these entry points:
+
+- Stack header **More actions** → **Monitor** (Alerts tab)
+- Container card **Monitor** (prefers that Compose service in add forms)
+- Sidebar context menu **Alerts**, or press `A` while focused on a stack
diff --git a/docs/features/auto-heal-policies.mdx b/docs/features/auto-heal-policies.mdx
index 1b978208..0c8c77bd 100644
--- a/docs/features/auto-heal-policies.mdx
+++ b/docs/features/auto-heal-policies.mdx
@@ -28,10 +28,12 @@ Policies live next to your stack-level alert rules in the stack's **Monitor** sh
## Workflow
-1. In the sidebar, right-click the stack you want to protect (or focus the stack and press **H**).
-2. Click **Auto-Heal** in the **inspect** group. The stack's **Monitor** sheet opens directly on the **Auto-heal** tab.
-3. In **Add new policy**, pick a **Service** (or leave the combobox on **All services** to cover every container in the stack) and tune the four thresholds.
-4. Click **Add Policy**. The new policy appears in **Active policies** above the form, with its enable toggle already on.
+1. Open the stack's **Monitor** sheet any of these ways:
+ - Stack header **More actions** → **Monitor**, then switch to the **Auto-heal** tab
+ - Container card **Monitor** (prefers that Compose service in **Add new policy**), then switch to **Auto-heal**
+ - Sidebar: right-click the stack (or focus it and press **H**) → **Auto-Heal** in the **inspect** group (opens directly on the **Auto-heal** tab)
+2. In **Add new policy**, pick a **Service** (or leave the combobox on **All services** to cover every container in the stack) and tune the four thresholds.
+3. Click **Add Policy**. The new policy appears in **Active policies** above the form, with its enable toggle already on.
diff --git a/docs/features/stack-management.mdx b/docs/features/stack-management.mdx
index be87bb04..08cdf81d 100644
--- a/docs/features/stack-management.mdx
+++ b/docs/features/stack-management.mdx
@@ -368,6 +368,7 @@ The stack header groups actions by frequency of use. The most common action is t
| Secondary | **Update** | `docker compose pull` + `up -d` (or build-aware rebuild when services declare `build:`) | Pulls registry images and recreates containers. When one or more services use `build:`, Update rebuilds those images from source (`compose build --pull`), pulls any remaining registry images, then recreates containers. |
| Overflow | **Rollback** | Restores backup | Reverts compose and env files to the pre-deploy snapshot and redeploys. Only shown when a backup exists. |
| Overflow | **Scan config** | Trivy config scan | Scans the compose file for misconfigurations (admin role). |
+| Overflow | **Monitor** | Opens Monitor sheet | Opens the stack **Monitor** sheet on the Alerts tab (alert rules and Auto-heal). Same sheet as sidebar **Alerts** / **Auto-Heal**. |
| Overflow | **Mute** | Creates a mute rule | Quick presets to mute notifications, deploy-success noise, or monitor alerts for this stack, plus a link to manage its mute rules in full. See [Alerts and Notifications](/features/alerts-notifications). |
| Overflow | **Delete** | `down --volumes` + removes files | Stops and removes containers and volumes, then deletes the stack directory. |
@@ -377,6 +378,7 @@ The stack header groups actions by frequency of use. The most common action is t
|-----------|--------|---------|--------------|
| Primary | **Start** | `docker compose up -d` | Starts the stack. |
| Secondary | **Update** | `docker compose pull` + `up -d` (or build-aware rebuild when services declare `build:`) | Pulls registry images and recreates containers. When one or more services use `build:`, Update rebuilds those images from source (`compose build --pull`), pulls any remaining registry images, then recreates containers. |
+| Overflow | **Monitor** | Opens Monitor sheet | Opens the stack **Monitor** sheet on the Alerts tab. |
| Overflow | **Delete** | Removes files | Deletes the stack directory. |
Use the sidebar context menu or `⌘↓` / `Ctrl+↓` for **Take down** on a stopped stack.
@@ -414,8 +416,9 @@ Press `B` again or toggle the bulk mode button to leave bulk mode.
## Controlling a single service
-Inside the stack detail view, each container card has a **Service actions** kebab on the right side. Use it to act on that service without touching the rest of the stack.
+Inside the stack detail view, each container card has quick actions (View logs, Monitor, Open bash when available) and a **Service actions** kebab on the right side. Use them to act on that service without touching the rest of the stack.
+- **Monitor**: opens the stack **Monitor** sheet with that Compose service preferred in the add forms. Shown only when the container reports a Compose service name.
- **Restart service**: stops and starts all containers for that service.
- **Stop service**: stops all containers for that service. Only shown when the service is running.
- **Start service**: starts all containers for that service. Only shown when the service is stopped or exited.
diff --git a/frontend/src/components/EditorLayout.tsx b/frontend/src/components/EditorLayout.tsx
index be11b41e..be3611eb 100644
--- a/frontend/src/components/EditorLayout.tsx
+++ b/frontend/src/components/EditorLayout.tsx
@@ -660,6 +660,10 @@ export default function EditorLayout() {
changeEnvFile={stackActions.changeEnvFile}
openLogViewer={stackActions.openLogViewer}
openBashModal={stackActions.openBashModal}
+ onOpenMonitor={stackName ? () => overlayState.openAlertSheet(stackName) : undefined}
+ onOpenServiceMonitor={stackName
+ ? (serviceName) => overlayState.openAlertSheet(stackName, { serviceName })
+ : undefined}
serviceAction={stackActions.serviceAction}
effectiveServices={effectiveServices}
serviceUpdateStatuses={serviceUpdateStatuses}
diff --git a/frontend/src/components/EditorLayout/EditorView.tsx b/frontend/src/components/EditorLayout/EditorView.tsx
index 3f4d8504..d2ad501b 100644
--- a/frontend/src/components/EditorLayout/EditorView.tsx
+++ b/frontend/src/components/EditorLayout/EditorView.tsx
@@ -177,6 +177,8 @@ export interface EditorViewProps {
// Container / service actions
openLogViewer: (containerId: string, containerName: string) => void;
openBashModal: (containerId: string, containerName: string) => void;
+ onOpenMonitor?: () => void;
+ onOpenServiceMonitor?: (serviceName: string) => void;
serviceAction: (
action: 'start' | 'stop' | 'restart',
serviceName: string,
@@ -278,6 +280,8 @@ export function EditorView(props: EditorViewProps) {
changeEnvFile,
openLogViewer,
openBashModal,
+ onOpenMonitor,
+ onOpenServiceMonitor,
serviceAction,
effectiveServices = [],
serviceUpdateStatuses = [],
@@ -447,6 +451,7 @@ export function EditorView(props: EditorViewProps) {
showTakeDown={showTakeDown}
isSelfStack={isSelfStack}
stackMuteActions={stackMuteActions}
+ onOpenMonitor={onOpenMonitor}
/>
{recoveryResult && loadingAction == null && (
@@ -484,6 +489,7 @@ export function EditorView(props: EditorViewProps) {
activeNode={activeNode}
openLogViewer={openLogViewer}
openBashModal={openBashModal}
+ onOpenServiceMonitor={onOpenServiceMonitor}
serviceAction={serviceAction}
effectiveServices={effectiveServices}
serviceUpdateStatuses={serviceUpdateStatuses}
@@ -508,6 +514,7 @@ export function EditorView(props: EditorViewProps) {
activeNode={activeNode}
openLogViewer={openLogViewer}
openBashModal={openBashModal}
+ onOpenServiceMonitor={onOpenServiceMonitor}
serviceAction={serviceAction}
effectiveServices={effectiveServices}
serviceUpdateStatuses={serviceUpdateStatuses}
diff --git a/frontend/src/components/EditorLayout/MobileStackDetail.tsx b/frontend/src/components/EditorLayout/MobileStackDetail.tsx
index dce74aa1..c2d409dd 100644
--- a/frontend/src/components/EditorLayout/MobileStackDetail.tsx
+++ b/frontend/src/components/EditorLayout/MobileStackDetail.tsx
@@ -61,6 +61,8 @@ export function MobileStackDetail(props: EditorViewProps) {
changeEnvFile,
openLogViewer,
openBashModal,
+ onOpenMonitor,
+ onOpenServiceMonitor,
serviceAction,
effectiveServices = [],
serviceUpdateStatuses,
@@ -159,6 +161,7 @@ export function MobileStackDetail(props: EditorViewProps) {
showTakeDown={showTakeDown}
isSelfStack={isSelfStack}
stackMuteActions={stackMuteActions}
+ onOpenMonitor={onOpenMonitor}
/>
@@ -229,6 +232,7 @@ export function MobileStackDetail(props: EditorViewProps) {
activeNode={activeNode}
openLogViewer={openLogViewer}
openBashModal={openBashModal}
+ onOpenServiceMonitor={onOpenServiceMonitor}
serviceAction={serviceAction}
effectiveServices={effectiveServices}
serviceUpdateStatuses={serviceUpdateStatuses}
diff --git a/frontend/src/components/EditorLayout/ShellOverlays.tsx b/frontend/src/components/EditorLayout/ShellOverlays.tsx
index b7dfb8de..e0e8cc1c 100644
--- a/frontend/src/components/EditorLayout/ShellOverlays.tsx
+++ b/frontend/src/components/EditorLayout/ShellOverlays.tsx
@@ -117,6 +117,7 @@ export function ShellOverlays({
onOpenChange={(open) => { if (!open) closeStackMonitor(); }}
stackName={stackMonitor?.stackName ?? ''}
initialTab={stackMonitor?.tab ?? 'alerts'}
+ initialService={stackMonitor?.serviceName}
/>
{/* Pre-update readiness check */}
diff --git a/frontend/src/components/EditorLayout/__tests__/ContainersHealth.test.tsx b/frontend/src/components/EditorLayout/__tests__/ContainersHealth.test.tsx
index 4d2dd253..083e9283 100644
--- a/frontend/src/components/EditorLayout/__tests__/ContainersHealth.test.tsx
+++ b/frontend/src/components/EditorLayout/__tests__/ContainersHealth.test.tsx
@@ -509,4 +509,43 @@ describe('containers load states', () => {
await user.click(screen.getByRole('button', { name: /retry/i }));
expect(onRetry).toHaveBeenCalledTimes(1);
});
+
+ it('shows Monitor when Service is set and calls onOpenServiceMonitor', async () => {
+ const onOpenServiceMonitor = vi.fn();
+ const user = userEvent.setup();
+ const c = { ...container([{ PrivatePort: 80, PublicPort: 8080 }]), Service: 'web' } as ContainerInfo;
+ render(
+ ,
+ );
+
+ await user.click(screen.getByRole('button', { name: 'Monitor web' }));
+ expect(onOpenServiceMonitor).toHaveBeenCalledWith('web');
+ });
+
+ it('hides Monitor when the container has no Service label', () => {
+ render(
+ ,
+ );
+ expect(screen.queryByRole('button', { name: /Monitor / })).toBeNull();
+ });
});
diff --git a/frontend/src/components/EditorLayout/__tests__/StackIdentityHeader.test.tsx b/frontend/src/components/EditorLayout/__tests__/StackIdentityHeader.test.tsx
index f61b63f7..43e98509 100644
--- a/frontend/src/components/EditorLayout/__tests__/StackIdentityHeader.test.tsx
+++ b/frontend/src/components/EditorLayout/__tests__/StackIdentityHeader.test.tsx
@@ -103,4 +103,15 @@ describe('StackIdentityHeader', () => {
expect(screen.queryByRole('menuitem', { name: /Take down/i })).toBeNull();
});
+
+ it('shows Monitor in More actions and calls onOpenMonitor', async () => {
+ const user = userEvent.setup();
+ const onOpenMonitor = vi.fn();
+ renderHeader({ onOpenMonitor, backupInfo: { exists: false, timestamp: null } });
+
+ await user.click(screen.getByRole('button', { name: 'More actions' }));
+ await user.click(screen.getByRole('menuitem', { name: 'Monitor' }));
+
+ expect(onOpenMonitor).toHaveBeenCalledTimes(1);
+ });
});
diff --git a/frontend/src/components/EditorLayout/editor-view-blocks.tsx b/frontend/src/components/EditorLayout/editor-view-blocks.tsx
index 496ab00f..09735b60 100644
--- a/frontend/src/components/EditorLayout/editor-view-blocks.tsx
+++ b/frontend/src/components/EditorLayout/editor-view-blocks.tsx
@@ -22,7 +22,8 @@ import {
Maximize2,
Minimize2,
AlertCircle,
- RefreshCw
+ RefreshCw,
+ HeartPulse,
} from 'lucide-react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button } from '../ui/button';
@@ -144,6 +145,8 @@ export interface StackIdentityHeaderProps {
/** True when this stack is the running Sencho instance on the active node. */
isSelfStack?: boolean;
stackMuteActions?: ReturnType;
+ /** Opens the stack Monitor sheet on the Alerts tab. */
+ onOpenMonitor?: () => void;
}
// Breadcrumb + serif title + state pill + action bar. The action buttons grow
@@ -170,6 +173,7 @@ export function StackIdentityHeader({
showTakeDown,
isSelfStack = false,
stackMuteActions,
+ onOpenMonitor,
}: StackIdentityHeaderProps) {
const selfProtected = isSelfStack;
return (
@@ -206,7 +210,7 @@ export function StackIdentityHeader({
const canScan = trivy.available && isAdmin;
const canMute = stackMuteActions?.canMute ?? false;
const hasOverflowExtras = canRollback || canScan;
- const hasOverflow = hasOverflowExtras || canDelete || canMute;
+ const hasOverflow = hasOverflowExtras || canDelete || canMute || onOpenMonitor;
if (!canDeploy && !hasOverflow) return null;
return (