* fix(frontend): tighten bell notification panel toolbar
Restructure the popover so the segmented filter (All / Unread / Alerts)
and the action icons (filter toggle, mark all read, clear all) share a
single row inside the 360px panel. The previous layout wrapped the
segmented control onto a second row once the type filter was added.
Move the unread badge to the right of the title, collapse the node and
type dropdowns behind a filter-toggle icon (accent dot signals an active
filter), equalize both Select widths, and extract the duplicated
className strings into local constants.
* docs: refresh bell notification popover screenshot
Reflects the reworked toolbar (segmented filter + filter toggle +
mark-all-read + clear-all on a single row).
* feat(stack): per-stack activity timeline with actor attribution
Adds an Activity tab to the Stack Anatomy panel showing a timestamped
event log for each stack: deploys, restarts, starts, stops, and image
updates, attributed to the user who triggered them or 'system' for
automated actions.
Backend:
- Extends notification_history with actor_username column (idempotent
migration) and a partial composite index on (node_id, stack_name,
timestamp DESC) for efficient per-stack lookups.
- NotificationService.dispatchAlert() accepts an optional actor that
is written to the new column.
- Success-side dispatchAlert calls added after deploy, bulkContainerOp
(start/stop/restart), and update handlers in routes/stacks.ts so
user-initiated operations are recorded, not just failures.
- New GET /api/stacks/:stackName/activity?limit&before endpoint with
stack:read permission gate and cursor-based pagination.
Frontend:
- StackAnatomyPanel grows an Anatomy / Activity tab pair using the
existing Tabs primitive.
- StackActivityTimeline fetches the initial 50 events, paginates on
demand, and prepends live events arriving over the existing WS
notifications stream without duplicates.
- NotificationPanel bell dropdown suppresses user-initiated success
events (start/stop/restart/deploy/update triggered by a real user),
keeping the tray focused on alerts and system events.
* docs(stack): add stack activity timeline feature page and internal arch docs
* fix(test): add actor_username to notification-routing history assertions
dispatchAlert now passes actor_username to addNotificationHistory after
the activity timeline PR added the column. Update the two exact-match
assertions that were failing because the expected object shape was missing
this field.
- Add label_ids and categories columns to notification_routes via idempotent migration
- Matcher logic always evaluates routes (AND semantics across all non-empty matchers)
- getStackLabelIds skips DB call when no enabled route uses label filtering
- Extract ALL_NOTIFICATION_CATEGORIES array from NotificationService as single source of truth
- Derive VALID_CATEGORIES set from the array in the route handler
- Extract validateLabelIds and validateCategories helpers to remove POST/PUT duplication
- Extract tryAddColumn as a private DatabaseService class method (removes 5 local re-declarations)
- Extract CATEGORY_LABELS to frontend/src/lib/notificationCategories.ts (shared by NotificationPanel and NotificationRoutingSection)
- Frontend form adds label and category multiselects with AND-filter hint
- Route cards show label and category badges; empty-matcher routes show 'Matches all alerts'
- Add tests for category-only, label-only, and combined AND-semantics routing
Introduce a NotificationCategory string-literal union (11 values) and
thread it through dispatchAlert as a required second argument. All
callers (DockerEventService, AutoHealService, ImageUpdateService,
MonitorService, PolicyEnforcement, policyGate, SchedulerService,
imageUpdates route) pass an explicit category at every call site,
giving TypeScript compile-time enforcement that no new emit site can
be added without choosing a category.
DatabaseService gains an idempotent migration that adds a nullable
category TEXT column to notification_history; existing rows keep
category=NULL (displayed as Uncategorized in the UI). The
getNotificationHistory method accepts an optional category filter
that is forwarded from the GET /api/notifications/history route via
a ?category= query param.
NotificationPanel gains a category Select dropdown so users can
filter history by category. The frontend types mirror the backend
union so API responses are type-safe end-to-end.
All 75 test files (1410 tests) updated to the new 4-arg dispatchAlert
signature and passing.
Two polish improvements to the aggregated notifications inbox:
- Per-node filter dropdown in the bell panel (hidden on single-node
installs) so fleet operators can triage events from a specific box.
Selected node falls back to "All nodes" automatically if that node is
removed from the registry.
- 60-second safety-net poll that reconciles the list so events missed
during a WebSocket reconnect backoff appear without a manual refresh.
Uses a ref indirection to pin the interval to the latest
fetchNotifications closure.
Extract the app header into a dedicated TopBar component and apply the
same translucent chrome language used by the sidebar: bg-sidebar +
backdrop-blur-md + inset top highlight. A new --chrome-top-highlight
token drives the highlight in both themes via the shadow-chrome-top
utility alias.
Restore the pulsing-dot unread indicator on the notification bell
(replaces the temporary count pill) and keep its aria-label dynamic so
screen readers still hear the unread count. Tighten nav a11y with
aria-current on the active item, aria-label on icon-only buttons, and a
proper <nav> landmark around the desktop Highlight group.
Swap hardcoded Tailwind padding on NodeSwitcher, UserProfileDropdown,
and NotificationPanel for var(--density-row-x) / --density-row-y /
--density-cell-y / --density-tile-y custom properties so the three
sidebar/header overlays now tighten when the Appearance setting is
switched to Compact, matching the reference pattern in ui/table.tsx.
Add stack_name and container_name columns to notification_history so bell
rows can act as jump points. Producers (AutoHeal, Docker events) pass the
container context through dispatchAlert; the panel renders routable rows
as buttons that load the target stack and, when a container name is
present, open its logs modal. Non-structural notifications stay as
passive display rows.
* feat(ui): redesign user menu and notification panel per design audit
Align both floating surfaces with the stack-view design language: cyan
signal rails, identity headers, strip containers, day-banded streams,
severity rails, Instrument Serif for hero words, and tracked-mono for
metadata. Extract the notifications popover out of EditorLayout into its
own component and introduce a reusable SegmentedControl primitive for
3-way radiogroups (Auto/Light/Dark theme, All/Unread/Alerts filter).
* fix(ui): pin accessible names on avatar and bell buttons
After the redesign the avatar button's text content became the user's
initials and the bell button's text became the unread count, which
displaced the title-based accessible names and broke
getByRole('button', { name: /profile/i }). Set aria-label explicitly on
both triggers and mark the bell badge aria-hidden so screen readers and
role-based selectors see stable names regardless of dynamic content.