mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-11 19:26:56 +00:00
feat(stack): per-stack activity timeline with actor attribution (#852)
* 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.
This commit is contained in:
@@ -89,13 +89,25 @@ function formatRelative(ms: number): string {
|
||||
return new Date(ms).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
||||
}
|
||||
|
||||
const USER_OP_CATEGORIES = new Set([
|
||||
'deploy_success', 'stack_started', 'stack_stopped', 'stack_restarted', 'image_update_applied',
|
||||
]);
|
||||
|
||||
function isUserInitiatedSuccess(n: NotificationItem): boolean {
|
||||
return n.level === 'info'
|
||||
&& n.category !== undefined
|
||||
&& USER_OP_CATEGORIES.has(n.category)
|
||||
&& n.actor_username != null
|
||||
&& n.actor_username !== 'system';
|
||||
}
|
||||
|
||||
function applyFilter(
|
||||
items: NotificationItem[],
|
||||
filter: NotifFilter,
|
||||
nodeFilter: NodeFilter,
|
||||
categoryFilter: CategoryFilter,
|
||||
): NotificationItem[] {
|
||||
let result = items;
|
||||
let result = items.filter(n => !isUserInitiatedSuccess(n));
|
||||
if (filter === 'unread') result = result.filter((n) => !n.is_read);
|
||||
else if (filter === 'alerts') result = result.filter((n) => n.level === 'warning' || n.level === 'error');
|
||||
if (nodeFilter !== NODE_FILTER_ALL) result = result.filter((n) => n.nodeId === nodeFilter);
|
||||
|
||||
Reference in New Issue
Block a user