Skip to content

fix(dashboard): improve touch targets and i18n for interactive elements#4409

Open
OneStepAt4time wants to merge 459 commits into
mainfrom
fix/dashboard-touch-target-a11y-interactions
Open

fix(dashboard): improve touch targets and i18n for interactive elements#4409
OneStepAt4time wants to merge 459 commits into
mainfrom
fix/dashboard-touch-target-a11y-interactions

Conversation

@OneStepAt4time
Copy link
Copy Markdown
Owner

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):

  • TabBar session detail tabs: min-h-[36px]min-h-[44px]
  • StreamTab view mode buttons (Terminal/Transcript/Split): added min-h-[44px]
  • TimelineSparkline range tabs (1h/6h/24h/7d): added min-h-[32px] with padding
  • SessionTable directory filter select: min-h-[36px]min-h-[44px]
  • MessageFooter inline command buttons (/help, /status, /cost): added min-h-[28px] with hover background

i18n fix:

  • TranscriptBubble collapse buttons: replaced hardcoded "Collapse transcript entry" with t("aria.expandEntry") / t("aria.collapseEntry")
  • Added en + it translation keys for expandEntry and collapseEntry

Files changed

  • 8 files, 15 insertions, 9 deletions

Testing

  • All impacted test files pass (Layout, SessionHistoryPage, MetricCards — 38 tests, 0 failures)

OneStepAt4time and others added 30 commits May 21, 2026 20:39
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.
25 Zod schema tests for sendMessageSchema, commandSchema, bashSchema covering trim, whitespace rejection (spaces/tabs/newlines/mixed), empty string, boundary length, internal whitespace preservation, and non-string guard.

Closes #3990
Refs #3956, #3957
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
Replace two manual useEffects with useSseAwarePolling. Board now updates live with 5s fallback / 30s SSE polling. Loading state only shown on mount — no spinner flash on background refresh. Also includes regex fix from #4001.

Closes #3995
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.
AbortController dedup for loadMore race, Errors/Completed columns, secondary sort key.
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.
#3946)

CC v2.1.141+ emits agent_id and parent_agent_id in tool OTEL spans. Wire them through the Aegis tracing and metering pipeline so subagent work can be correlated to the originating session.

Closes #3946
…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.
…ted columns (#4011, #4016)

Extract 24 hardcoded strings to i18n keys (EN + IT). Add Errors column (error, rate_limit, killed) and Completed column (completed). Parameter interpolation fallback for tests.
OBSERVABILITY.md: document aegis.agent.id and aegis.agent.parent_id span attributes. enterprise.md: document AEGIS_BLOCKED_ACP_ARGS env var.
OneStepAt4time and others added 29 commits May 27, 2026 16:12
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.
26 MetricCards tests + PauseControlBar tests from closed #4375.\n\nCovers loading/error states, latency formatting, delivery rate colors,\ncost/token formatting, SSE status, error handling.\n\n27 tests, 2 files. Part of #4298.
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.
…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)
…4389)

Move budgetAlertSection and dismissBudgetAlert from root to cost namespace in en.ts and it.ts. Fixes i18n nesting bug. Closes #4389.
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' });
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants