fix(dashboard): improve touch targets and i18n for interactive elements#4409
Open
OneStepAt4time wants to merge 459 commits into
Open
fix(dashboard): improve touch targets and i18n for interactive elements#4409OneStepAt4time wants to merge 459 commits into
OneStepAt4time wants to merge 459 commits into
Conversation
Refactor listSessions from single-page client-side filtering to paginated server-side fetching. Adds pagination loop with MAX_PAGES=100 safety cap, backward-compatible fallback when server omits pagination metadata, and forwards status/project as query params. Closes #3947.
Replace hardcoded Tailwind colors (amber, red, gray, hex) with CSS variables across 21 dashboard files. Adds --color-placeholder and --color-accent-cyan-glow to all 4 theme variants.
Adds per-session cost breakdown table showing name, model, tokens, cost, duration, and cache hit rate. Sortable columns, responsive design, batched fetching with concurrency limit. Includes drive-by fixes for ConfirmDialog/EmptyState CSS var migration. 7 tests.
Competitive intel (docs/competitive-intel/): - multica-v1-battle-card.md — executive summary, feature matrix, moat analysis, strategic response - multica-feature-gap-raw.md — 52-row feature comparison table - multica-architecture.md — server, agent lifecycle, squads, skills, auth analysis - multica-security-assessment.md — 10-area security audit with findings New feature: - ag update — self-update command (adapts Multica's update pattern) - Detects npm global vs direct binary install - GitHub Releases API for latest version check - SHA-256 checksum verification before binary replace - --check flag for CI/automation, --yes to skip prompt - 18 tests passing
Inspired by Multica's board view. Adds a 'Board' tab to SessionsPage that displays sessions in Kanban-style columns by status: - Running (working, compacting, plan_mode, settings) - Waiting (permission_prompt, ask_question, bash_approval, etc.) - Idle - Other (fallback for edge states) Each session card shows: name, model badge, duration, last activity. Waiting sessions get a pulsing 'Needs attention' indicator. Header shows aggregate counts (total, running, waiting). Features: - Responsive horizontal scroll for columns - Loading skeleton, error state, empty state per column - SSE reconnect triggers refetch - 8 tests covering all states and accessibility - All existing tests pass (SessionsPage: 8, a11y: 31) Zero backend changes — uses existing getSessions() API. — Daedalus 🏛️
Kanban board tab on Sessions page with Running/Waiting/Idle columns. Session cards show name, model badge, duration, activity. Pulsing needs-attention indicator. Lazy-loaded, 8 tests, zero backend changes.
18 SessionBoard tests (column grouping, Other column, counts, sort, accessibility) + 7 SessionsPage tab routing tests (URL params, click switching, fallback, tablist). Closes #3991
One-character fix: /-d{8}$/ → /-\d{8}$/. The regex matched literal d instead of \d (digit class), so model names like claude-sonnet-4-20250514 displayed as sonnet-4-20250514 instead of sonnet-4.
Closes #3994
Team-wide competitive review: Orpheus source assessment, Boss strategic direction, Argus threat downgrade (🔴→🟡), Daedalus dashboard analysis. Battle card now referenceable in docs/competitive-intel/.
- Add sweepOrphanedActions() to AcpActionQueue interface - Implement in MemoryAcpActionQueue and PostgresAcpActionQueue - New ActionSweeper utility with configurable interval (default 60s) - Configurable via AEGIS_ACTION_SWEEPER_ENABLED and AEGIS_ACTION_SWEEPER_INTERVAL_MS env vars - Wire sweeper in server.ts with startup/shutdown lifecycle - Emit structured logs for recovered actions and errors - Unit tests: past lease recovery, within lease untouched, disabled sweeper, env config resolution Closes #4004 Co-authored-by: Hephaestus <hep@aegis.dev>
…#3996) - Board now uses page-based fetching via getSessions({ page, limit }) - "Load X more" button appears when sessions.length < total - Button shows remaining count (e.g. "Load 116 more") per Athena's design - Header shows "216 sessions (100 loaded)" when hasMore - Second page appended to existing sessions, no refetch - Loading spinner on button during fetch, disabled while in-flight - 7 new tests for pagination behavior Closes #3996 Co-authored-by: Hephaestus <hep@aegis.dev>
…ract fields (#3948) Resume sessions now inherit model and effort from the original session when no explicit override is provided. CreateSessionRequest TypeScript type aligned with Zod schema (added model, effort, systemPrompt, isolationPolicy). 6 tests.
Add AEGIS_ACTION_SWEEPER_ENABLED and AEGIS_ACTION_SWEEPER_INTERVAL_MS to enterprise config docs.
…ise docs Phase 3.5 complete: M3 (contract cleanup) and M5 (soak/cutover) checkboxes updated. Add AEGIS_ACTION_SWEEPER_* env vars to enterprise config table.
…4009) Replace wipe-and-reset with merge-by-ID strategy: update existing sessions in-place, prepend new ones, keep pages 2+ intact. Fixes polling reset that nuked pagination state.
…tions disclosure (#3998) ADR-0030: network isolation deferred to Phase 4 for multi-user deployments. Security best practices updated with known limitation section disclosing unrestricted egress. Sweeper env vars added to enterprise config.
Clarify that resumeSessionId inherits model and effort from the original session unless explicitly overridden. Follow-up to PR #4013.
Prevent user-configured args from overriding protocol-critical flags (--output-format, --resume, --session-id, --api-key, --model). Configurable via AEGIS_BLOCKED_ACP_ARGS env var. 16 tests.
CHANGELOG updated with 71 missing PR entries. API reference updated with resume carry-over docs.
…3980) Four hard security requirements for multi-agent features with gate test checklists, blocking relationships, and 3-step review protocol (author self-certify → Themis review → Argus gate). CHANGELOG catch-up and api-reference resume docs included.
AEGIS_ALLOWED_WORK_DIRS restricts session root directories at creation time — not adapter-level filesystem sandbox. Clarified to avoid overstating control scope.
Update 4 docs locations with CC ≥ 2.1.145 as minimum version. Security baseline: fixes bash permission-prompt bypass where bare env var assignments were auto-approved.
OBSERVABILITY.md: document aegis.agent.id and aegis.agent.parent_id span attributes. enterprise.md: document AEGIS_BLOCKED_ACP_ARGS env var.
Covers system, user, assistant, thinking, tool_use, tool_result, tool_error messages plus focus, keyboard shortcuts, timestamps, element ID generation, and clipboard interactions. Co-authored-by: Hephaestus <hep@aegis.dev>
- jsonl-watcher-worker.ts: worker_threads Worker that accepts watch/unwatch/setOffset commands, uses fs.watch with debouncing, reads JSONL entries via readNewEntries(), posts parsed results back to main thread via parentPort - jsonl-watcher-bridge.ts: main-thread bridge implementing JsonlWatcher-compatible public API (onEntries, watch, unwatch, stop, destroy, setOffset, isWatching, getOffset) with EventBus integration for session.lineParsed events - 10 tests: lifecycle, offset tracking, truncation detection, large fixture streaming, resume from offset, memory bounds, cleanup Co-authored-by: Hephaestus <hep@aegis.dev>
…#4361) * fix(api): remove session counts from unauthenticated /health response Issue #4355: The /health endpoint was returning session counts (active, total) without requiring authentication. This is minor info disclosure. Fix: unauthenticated requests now only receive {status: "ok"}. Authenticated requests still get the full response including session counts, version, uptime, and claude status. Monitors that relied on unauthenticated session counts (Issue #3739) should use authenticated requests going forward. * test: update health endpoint tests for unauthenticated response change Issue #4355: Tests expected session counts on unauthenticated /health responses. Updated to expect only {status} for unauthenticated callers. * chore: trigger PR sync --------- Co-authored-by: OneStepAt4time <noreply@onestepat4time.com>
#4364) * feat(eventbus): add Redis Streams EventBus + SSE bridge (#4229 phase C-D) * fix(eventbus): type-safe tests + LogContext fixes + handler error isolation - Rewrote redis-event-bus.test.ts with proper types (RedisLike, BusEvent) - Rewrote sse-bridge.test.ts with typed mocks - Fixed LogContext err→attributes.error in redis-event-bus.ts - Wrapped setImmediate handler calls in try-catch for error isolation - Removed unused @ts-expect-error directives - 17 tests passing (10 Redis + 7 SSE bridge) * fix(eventbus): address review feedback — async replay, publish fix, SSE types, auth, rescan Review fixes for PR #4364: 1. CRITICAL: replaySince now async — awaits xrange (was silently returning [] with real async Redis clients) 2. CRITICAL: publish() returns local seq immediately; xadd is fire-and-forget with error logging (no silent data loss) 3. CRITICAL: SSE bridge uses lastEventId to replay missed events 4. Pattern subscriptions rescan every 5s to discover new streams 5. New subscriptions start from '+' (no history replay) 6. SSE bridge uses proper Fastify types (no any) 7. Auth covered by global onRequest hook (setupAuth) 8. Test files updated for async replaySince interface * fix(eventbus): bump bundle threshold + replace Function type in tests Rebase onto develop brought threshold to 2480KB; fix lint errors in sse-bridge.test.ts by replacing bare Function type with proper signature. --------- Co-authored-by: OneStepAt4time <noreply@onestepat4time.com> Co-authored-by: Hephaestus <hep@aegis.dev>
…#4246 step 5) Extract approval/reject/timeout logic into SessionApprovalService.\n\n- approval-flow.ts: 86-line service with DI, approve/reject/timeout\n- session.ts: 1648→1102 lines (-33%)\n- 10 tests covering approve, reject, auto-reject, cancel, error tolerance\n\nPart of #4246 server.ts god object decomposition.
Update api-reference.md to reflect actual unauthenticated response (status only, no session counts).
…standalone modules (#4246 step 6) Extract readHookSecretFromSettingsFile to hook-secret-reader.ts and computeLatencyMetrics to latency-metrics.ts. 23 new tests. session.ts: 1648→1052 (-36.2% cumulative). Bundle threshold 2480→2490.
…ep 7) Extract pure construction logic from _createSession into buildSessionInfo() factory function. session.ts: 1648 → 926 lines (-43.8%). 14 new unit tests.
…h.ts (#4246 step 8) (#4369) * refactor(session): extract health/monitoring methods to session-health.ts (#4246 step 8) - Extract computeLatencyMetrics, checkWaitingForInput, buildSessionHealth to src/services/session/session-health.ts as standalone functions - Export LatencyMetrics and SessionHealthInfo types - SessionManager delegates to extracted functions - 12 new tests for all three functions - session.ts: 1102 → 1047 lines (-55, -5%) 🤖 Generated with Aegis Session: ag-hep heartbeat Verification: tsc ✅ build ✅ 5768 tests ✅ * fix(lint): replace @ts-ignore with @ts-expect-error in session-health test * test(session): remove unnecessary @ts-expect-error directives in session-health tests (#4369 requested) * fix(session-health): accept nullable SessionInfo in computeLatencyMetrics * fix(session-health): remove duplicate computeLatencyMetrics, use latency-metrics.ts --------- Co-authored-by: Hephaestus <hep@aegis.dev>
Replace blind 10s polling with SSE-triggered fetch for audit live tail.\n\n- SSE trigger: watches activities count, fetches on new activity\n- 2s debounce prevents burst-fetching\n- Degraded polling fallback when SSE disconnected\n- Shared liveTailFetch helper eliminates duplication\n\nPart of #4346.
…4349) Add immediate visual feedback and audit logging for inline Telegram callback approvals/rejections. - editMessage shows approver/rejector name inline - StructuredLogger audit trail for approve/reject actions - cb_option also shows confirmed selection - All edits wrapped in try/catch (non-critical failures) - Only telegram-polling.ts changed (+8/-1) Closes #4349
Wire SSE bridge in server.ts. Follow-up to #4229.
Co-authored-by: OneStepAt4time <noreply@onestepat4time.com>
- Add SSEStatusIndicator component with colored dot + label (green/Live, yellow/Reconnecting…, red/Offline) - Wire into Header.tsx next to ApprovalBadge - Includes accessibility: role=status, aria-label, animate-pulse dot - 5 unit tests covering all states - Uses existing useStore (sseConnected, sseError) — no new deps Co-authored-by: Hephaestus <hep@aegis.dev>
…ep 2) (#4378) - session-reaper.ts: reapStaleSessions + reapZombieSessions + zombie constants - config-watcher.ts: setupConfigWatcher + handleConfigReload - server.ts: 840 → 672 lines (-20%) - Zero behavior changes, all 5804 tests pass Co-authored-by: Hephaestus <hep@aegis.dev>
…ch 6) (#4381) 24 new tests across 3 files: ProtectedRoute (4), Code (10), PipelineStatusBadge (10). Zero production code changes.
Two new guides:\n\n- agent-bootstrap-best-practice.md: GitHub as source of truth pattern\n- real-time-events.md: SSE event reference, auth, reconnection strategy\n\n357 lines, 2 files. Docs-only.
Upgrade onboarding wizard from static to health-aware.\n\n- Connect step: shows Claude status (connected/warning/unavailable)\n- First Session step: shows active sessions or Create Session CTA\n- Explore step: shows total session count\n- Auto-marks step 2 complete when Claude healthy\n- Auto-opens session drawer on completion when no sessions\n- 3s timeout fallback for health fetch\n- 18 tests covering all states and transitions\n\n455 additions, 2 files.
#4384) Co-authored-by: Hephaestus <hep@aegis.dev>
…ests (batch 8) (#4387) Co-authored-by: Hephaestus <hep@aegis.dev>
…sts (batch 9) (#4388) 46 new tests across 3 components. All pass, zero TS errors, no production code changes. Components tested: - TokenBreakdown (14 tests) - ConfirmDestructive (16 tests) - RateLimitCard (16 tests)
Add Security section to real-time-events.md covering authentication, authorization (tenant scoping, ownership checks), connection limits, and audit findings. Addresses Themis audit findings from #4393.
…prep) (#4386) - Add src/boot/boot-auth.ts re-export facade for auth setup - Extract config watcher to src/boot/boot-config-watcher.ts with DI timers - Add 9 unit tests for boot-config-watcher - Bump bundle size threshold to 2508KB - Update server.ts import path to boot facade Prep work for #4227 — no behavior changes.
Documents the SSE bridge endpoint in: - api-reference.md: full endpoint spec with auth, tenant scoping, connection limits, and error responses - api-quick-ref.md: quick reference entry - real-time-events.md: SSE bridge section alongside global/per-session Endpoint was introduced in #4373 and hardened in #4395 but never documented. Co-authored-by: Hephaestus <hep@aegis.dev>
- TabBar tabs: 36px → 44px minimum height for mobile tap targets - StreamTab view mode buttons: add 44px min-height - TimelineSparkline range tabs: add 32px min-height with more padding - SessionTable directory filter select: 36px → 44px - MessageFooter inline command buttons: add min-height and hover background - TranscriptBubble collapse buttons: replace hardcoded English aria-labels with i18n keys (aria.expandEntry / aria.collapseEntry) - Add en + it translations for new aria keys
Comment on lines
+79
to
+239
| getSession: (sessionId: string | undefined) => | ||
| ctx.dashboardTokenSessions.get(sessionId) ?? ctx.dashboardOidc?.getSession(sessionId) ?? null, | ||
| }); | ||
| if (dashboardAuthContext) { | ||
| requestKeyMap.set(req.id, dashboardAuthContext.keyId); | ||
| if (ctx.auditLogger) { | ||
| void ctx.auditLogger.log( | ||
| dashboardAuthContext.actor, | ||
| 'api.authenticated', | ||
| `${req.method} ${req.url?.split('?')[0] ?? req.url}`, | ||
| undefined, | ||
| dashboardAuthContext.tenantId, | ||
| ); | ||
| } | ||
| if (checkIpRateLimit(clientIp, false, dashboardAuthContext.keyId)) { | ||
| addRateLimitHeaders(reply, rateLimiter.getIpBucketInfo(clientIp, false, dashboardAuthContext.keyId)); | ||
| return reply.status(429).send({ error: 'Rate limit exceeded — IP throttled' }); | ||
| } | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| const isNoAuthLocalhost = !ctx.auth.authEnabled && ctx.auth.isLocalhostBinding; | ||
| if (isNoAuthLocalhost) { | ||
| if (checkIpRateLimit(clientIp, false)) { | ||
| addRateLimitHeaders(reply, rateLimiter.getIpBucketInfo(clientIp, false)); | ||
| return reply.status(429).send({ error: 'Rate limit exceeded — IP throttled' }); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| if (!token) { | ||
| if (checkIpRateLimit(clientIp, false)) { | ||
| addRateLimitHeaders(reply, rateLimiter.getIpBucketInfo(clientIp, false)); | ||
| return reply.status(429).send({ error: 'Rate limit exceeded — too many unauthenticated requests' }); | ||
| } | ||
| return reply.status(401).send({ error: 'Unauthorized — Bearer token required' }); | ||
| } | ||
|
|
||
| const tokenMode = classifyBearerTokenForRoute(token, !!isSSERoute); | ||
|
|
||
| if (tokenMode === 'sse') { | ||
| if (await ctx.auth.validateSSEToken(token)) { | ||
| return; | ||
| } | ||
| if (checkAuthFailRateLimit(clientIp)) { | ||
| addRateLimitHeaders(reply, rateLimiter.getAuthFailBucketInfo(clientIp)); | ||
| return reply.status(429).send({ error: 'Too many auth failures — try again later' }); | ||
| } | ||
| recordAuthFailureOnce(req, clientIp); | ||
| return reply.status(401).send({ error: 'Unauthorized — SSE token invalid or expired' }); | ||
| } | ||
|
|
||
| if (tokenMode === 'reject') { | ||
| if (checkAuthFailRateLimit(clientIp)) { | ||
| addRateLimitHeaders(reply, rateLimiter.getAuthFailBucketInfo(clientIp)); | ||
| return reply.status(429).send({ error: 'Too many auth failures — try again later' }); | ||
| } | ||
| recordAuthFailureOnce(req, clientIp); | ||
| return reply.status(401).send({ error: 'Unauthorized — SSE token required for event streams' }); | ||
| } | ||
|
|
||
| const result = ctx.auth.validate(token); | ||
|
|
||
| if (!result.valid) { | ||
| if (checkAuthFailRateLimit(clientIp)) { | ||
| addRateLimitHeaders(reply, rateLimiter.getAuthFailBucketInfo(clientIp)); | ||
| return reply.status(429).send({ error: 'Too many auth failures — try again later' }); | ||
| } | ||
| recordAuthFailureOnce(req, clientIp); | ||
| if (result.reason === 'expired') { | ||
| return reply.status(401).send({ error: 'Unauthorized — API key has expired', code: 'KEY_EXPIRED' }); | ||
| } | ||
| return reply.status(401).send({ error: 'Unauthorized — invalid API key' }); | ||
| } | ||
|
|
||
| if (result.rateLimited) { | ||
| addRateLimitHeaders(reply, rateLimiter.getIpBucketInfo(clientIp, result.keyId === 'master', result.keyId ?? undefined)); | ||
| return reply.status(429).send({ error: 'Rate limit exceeded — 100 req/min per key' }); | ||
| } | ||
|
|
||
| requestKeyMap.set(req.id, result.keyId ?? 'anonymous'); | ||
| req.authKeyId = result.keyId; | ||
| req.authRole = ctx.auth.getRole(result.keyId); | ||
| req.authPermissions = ctx.auth.getPermissions(result.keyId); | ||
| req.authActor = ctx.auth.getAuditActor(result.keyId, result.keyId ?? 'anonymous'); | ||
| req.tenantId = result.tenantId; | ||
|
|
||
| rateLimiter.resetAuthFailures(clientIp); | ||
|
|
||
| if (ctx.auditLogger) { | ||
| void ctx.auditLogger.log(result.keyId ?? 'anonymous', 'api.authenticated', `${req.method} ${req.url?.split('?')[0] ?? req.url}`, undefined, result.tenantId); | ||
| } | ||
|
|
||
| const isMaster = result.keyId === 'master'; | ||
| if (checkIpRateLimit(clientIp, isMaster, result.keyId ?? undefined)) { | ||
| addRateLimitHeaders(reply, rateLimiter.getIpBucketInfo(clientIp, isMaster, result.keyId ?? undefined)); | ||
| return reply.status(429).send({ error: 'Rate limit exceeded — IP throttled' }); | ||
| } | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Audit of UI interaction bugs found multiple undersized touch targets and hardcoded English strings in aria-labels.
Changes
Touch target fixes (WCAG 2.5.5 / Apple HIG):
TabBarsession detail tabs:min-h-[36px]→min-h-[44px]StreamTabview mode buttons (Terminal/Transcript/Split): addedmin-h-[44px]TimelineSparklinerange tabs (1h/6h/24h/7d): addedmin-h-[32px]with paddingSessionTabledirectory filter select:min-h-[36px]→min-h-[44px]MessageFooterinline command buttons (/help, /status, /cost): addedmin-h-[28px]with hover backgroundi18n fix:
TranscriptBubblecollapse buttons: replaced hardcoded"Collapse transcript entry"witht("aria.expandEntry")/t("aria.collapseEntry")en+ittranslation keys forexpandEntryandcollapseEntryFiles changed
Testing