refactor(sidebar): rebuild footer as priority-driven Ops Pulse strip (#1178)

* refactor(sidebar): rebuild footer as priority-driven Ops Pulse strip

Replace the simple notification ticker with a derived activity summary that
picks one of six states (active-op, failure, automation, recent-event,
quiet-live, disconnected) and routes per-state clicks to logs, schedules,
or activity. The hook owns the cascade; the component is pure presentation;
EditorLayout owns wiring.

Failure detection covers unread errors in the last 24h; recent-event is
limited to non-error stack notifications in the last hour; automation reads
the next /scheduled-tasks?action=update run and a debounced state-invalidate
listener; the deploy-panel composite key is used for elapsed-time tracking
so close-then-immediately-reopen counts as a new session.

* refactor(sidebar): apply Ops Pulse audit fixes

- countEnabledAutoUpdates now defaults missing autoUpdateSettings entries to
  enabled, matching the backend's getStackAutoUpdateSettingsForNode contract.
  Previously the automation state could not render even with the documented
  per-row default-true.
- findFailure now requires a stack_name so the sidebar does not select a
  system-level error whose click would no-op through navigateToNotification.
  System errors continue to surface via the top-bar NotificationPanel.
- DeployPanelState gains a monotonic sessionId sourced from the existing
  internal counter, and the new usePanelSessionStartedAt hook keys the
  elapsed-time tracker off it so a same-stack rerun always resets even when
  isOpen stays true across succeeded then preparing.
- buildConfig splits quiet-live out of the default and adds an exhaustiveness
  guard so future SidebarActivitySummary variants fail to compile.
- New unit tests cover the default-true aggregation, the same-stack session
  reset, the non-stack failure guard, and the useNextAutoUpdateRun debounce
  and cleanup paths. Frontend suite: 276 / 276 pass.
This commit is contained in:
Anso
2026-05-23 15:43:06 -04:00
committed by GitHub
parent 21ec5e7e0a
commit bb44db0cb1
11 changed files with 901 additions and 83 deletions
@@ -0,0 +1,82 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { renderHook } from '@testing-library/react';
import { usePanelSessionStartedAt } from '../usePanelSessionStartedAt';
import type { DeployPanelState } from '@/context/DeployFeedbackContext';
function panel(over: Partial<DeployPanelState> = {}): DeployPanelState {
return {
isOpen: false,
stackName: '',
action: 'deploy',
status: 'preparing',
sessionId: 0,
...over,
};
}
describe('usePanelSessionStartedAt', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-01-01T00:00:00Z'));
});
afterEach(() => {
vi.useRealTimers();
});
it('returns null while the panel is closed', () => {
const { result } = renderHook((p: DeployPanelState) => usePanelSessionStartedAt(p), {
initialProps: panel(),
});
expect(result.current).toBeNull();
});
it('captures Date.now() when the panel opens', () => {
const { result, rerender } = renderHook((p: DeployPanelState) => usePanelSessionStartedAt(p), {
initialProps: panel(),
});
expect(result.current).toBeNull();
const opened = Date.now();
rerender(panel({ isOpen: true, stackName: 'web', sessionId: 1 }));
expect(result.current).toBe(opened);
});
it('resets the timestamp when sessionId changes even if isOpen stays true (same stack rerun)', () => {
const { result, rerender } = renderHook((p: DeployPanelState) => usePanelSessionStartedAt(p), {
initialProps: panel({ isOpen: true, stackName: 'web', sessionId: 1 }),
});
const first = result.current;
expect(first).not.toBeNull();
// Status flows to succeeded but the panel stays visible.
rerender(panel({ isOpen: true, stackName: 'web', status: 'succeeded', sessionId: 1 }));
expect(result.current).toBe(first);
// Advance the clock and trigger a same-stack rerun under a new sessionId.
vi.advanceTimersByTime(5_000);
rerender(panel({ isOpen: true, stackName: 'web', status: 'preparing', sessionId: 2 }));
expect(result.current).not.toBe(first);
expect(result.current).toBe((first ?? 0) + 5_000);
});
it('does not flap when the same panel session re-renders with no changes', () => {
const { result, rerender } = renderHook((p: DeployPanelState) => usePanelSessionStartedAt(p), {
initialProps: panel({ isOpen: true, stackName: 'web', sessionId: 1 }),
});
const captured = result.current;
vi.advanceTimersByTime(10_000);
rerender(panel({ isOpen: true, stackName: 'web', sessionId: 1, status: 'streaming' }));
expect(result.current).toBe(captured);
});
it('clears the timestamp when the panel closes', () => {
const { result, rerender } = renderHook((p: DeployPanelState) => usePanelSessionStartedAt(p), {
initialProps: panel({ isOpen: true, stackName: 'web', sessionId: 1 }),
});
expect(result.current).not.toBeNull();
rerender(panel({ isOpen: false, sessionId: 1 }));
expect(result.current).toBeNull();
});
});