[21e195cd] Panel-wide UI standardization and usability pass (#194)

* [4c179e3a] Add git pull, fetch, and rebase backend endpoints (#190)

* [f966f772] feat(git): add pull, fetch, and rebase endpoints with integration tests (#185)

- Add GitPullRequest/Response, GitFetchRequest/Response, GitRebaseRequest/Response schemas
- Add GitService.pull(), fetch(), and rebase() methods using _network_git_timeout()
- Add POST /api/git/pull, /api/git/fetch, /api/git/rebase route handlers
- Rebase detects conflicts via git diff --name-only --diff-filter=U and aborts cleanly
- Integration tests cover success path and GitCommandError→500 for all three endpoints
- Rebase conflict test verifies conflict=True with populated conflicted_files list

Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>

* [26e2b7af] test(git): add AsyncMock unit tests for rebase_onto_base conflict-state handling (#186)

New test_git_rebase.py covers three branches of rebase_onto_base:
- success path: rebase exits 0, returns rebased status, abort never called
- conflict path: non-zero exit → diff → abort → returns conflict+files
- resilience: both rebase and abort exit non-zero, still returns conflict dict without exception

All tests use AsyncMock with side_effect sequences to mock _run_git at the service-method level.

Co-authored-by: Backend Developer 2 <be-dev-2@agents.roboco.dev>

---------

Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
Co-authored-by: Backend Developer 2 <be-dev-2@agents.roboco.dev>

* [551b1dbf] Panel-wide frontend UI standardization and page fixes (#193)

* [1ec787b2] feat(panel): design-system sweep — full-width layouts, scrollbar fix, Secretary button, component audit (#188)

- Settings page: remove max-w-3xl, wrap cards in grid-cols-1 lg:grid-cols-2 two-column layout
- AI Providers page: remove max-w-5xl so AIRoutingCard fills available width
- Journals AgentList: replace ScrollArea with overflow-y-auto div to eliminate nested scrollbar
- Secretary chat input: add items-stretch to flex row so Send/Start button matches Textarea height
- Component audit: replace all raw <button>/<input>/hand-rolled badge spans outside components/ui/ with canonical Button, Checkbox, Badge variants across 15 files:
  - ai-routing-card.tsx: ModeButton → Button, checkbox → Checkbox, badge spans → Badge
  - self-hosted-section.tsx: eye-toggle → Button ghost icon-sm, badge spans → Badge
  - journals/agent-item.tsx, communications/channel-item.tsx → Button ghost
  - kb-search-bar.tsx, kb-filters.tsx → Button ghost
  - kb-category-nav.tsx, git-log-panel.tsx → Button ghost
  - communications/page.tsx (channel + group lists) → Button ghost
  - projects/project-table.tsx, products/product-table.tsx → Button link
  - git-branch-panel.tsx (local + remote lists) → Button ghost
  - tasks/dependency-selector.tsx: Button ghost + Checkbox for visual indicator
  - tasks/task-table.tsx: sortable header + expand toggle → Button ghost
  - business/goals-tab.tsx: hidden button → Button

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>

* [435b37b4] feat(metrics,notifications): URL-persisted tab state, semantic chart colors, humanized counts (#187)

- Notifications page: replace useState with useSearchParams/useRouter for
  ?tab= URL parameter (all/unread/pending, default: unread); Suspense wrapper
  with skeleton fallback for SSR compatibility.

- Metrics page: split into Performance tab (Velocity + Task Status + Agent
  Status + Team Health) and Token Usage tab (TokenUsageCostsSection) with
  ?tab= URL parameter (default: performance); Suspense wrapper; Refresh button
  moved inside PerformanceTabContent; humanizeCount() helper applies K/M
  suffixes to all MetricCard numeric values >= 1000.

- Chart components (usage-time-series, agent-usage, team-usage, model-donut):
  replace var(--chart-N) CSS vars with explicit semantic hex colors —
  #3b82f6 blue for informational, #f59e0b amber for warning/pending,
  #22c55e green for success/healthy, #ef4444 red for error/blocked,
  #a855f7 purple for supplemental.

pnpm lint and pnpm typecheck pass with zero new errors.

Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

* [ccd256f4] Kanban mobile viewport: 375px layout, column navigation, 44px touch targets (#191)

* [ccd256f4] feat(kanban): mobile 375px layout with column navigator and 44px touch targets

- KanbanBoard: add activeColumnIndex state + mobile prev/next column
  navigator (lg:hidden); existing horizontal-scroll layout hidden on
  mobile (hidden lg:flex). Desktop DnD behavior unchanged.
- KanbanColumn: add optional className prop (cn-based) so mobile view
  can pass w-full/sm:w-full to fill the viewport.
- KanbanCard: bump all action buttons to min-h-11 (44px) touch targets
  (Assign, Pass, Fail, Move-forward).

* [ccd256f4] fix(kanban): change breakpoint from lg to sm for mobile/desktop layout switch

AC3 requires >=640px viewport to show multi-column layout (sm: breakpoint).
Previous impl used lg: (1024px), leaving 640-1023px in single-column mode.

Change:
- Mobile navigator div: lg:hidden → sm:hidden
- Desktop multi-column div: hidden lg:flex → hidden sm:flex

At <640px: single-column with prev/next navigator (375px mobile use case).
At >=640px: full horizontal-scroll multi-column layout (per AC3).
DnD behavior and all other layout unchanged.

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>

* [23f02af4] Agents page On-Demand section + Board composition; Overview Quick Actions visibility + Team Health Intake/Secretary (#189)

* [23f02af4] feat(agents,overview): On-Demand section, Board composition fix, Intake/Secretary in Quick Actions + Team Health

- agent-definitions.ts: remove AgentRole.MAIN_PM from getBoardAgents
  (Main PM has its own dedicated section; including it there was redundant).
  Add getOnDemandAgents() that catches agents not in any standard team
  (board/main_pm/backend/frontend/ux_ui/marketing) and not a standard cell
  role — surfaces prompter/intake agents that the API may return.

- agents/page.tsx: import getOnDemandAgents; add a conditional
  'On-Demand Agents' AgentGrid section (only rendered when the API returns
  at least one matching agent, e.g. the Intake interviewer).

- quick-actions-bar.tsx: add 'Task Intake' button (→/prompter, Sparkles
  icon) and 'Secretary' button (→/business?tab=secretary, Bot icon)
  alongside existing quick actions so operators can reach on-demand agents
  from the Overview in one click.

- team-health-cards.tsx: add OnDemandAgentCard sub-component (link card
  with On-Demand badge) and render static cards for 'Task Intake' and
  'Secretary' appended after the API-driven TeamHealthCard list, giving
  them equal visual presence in the Team Health section.

pnpm lint and pnpm typecheck pass with zero new errors.

* [23f02af4] fix(agents,overview): QA revision — enum entries, QuickActions placement, On-Demand title, Board PR_REVIEWER

AC3: types/index.ts AgentRole enum adds PR_REVIEWER, PROMPTER, SECRETARY.
     agent-selector.tsx ROLE_LABELS exhaustive Record updated accordingly.

AC4: command-center.tsx QuickActionsBar moved to after Team Health section,
     before CEO Approval Queue and data-heavy grid rows — visible without
     scrolling on a 900px viewport.

AC1: agents/page.tsx On-Demand AgentGrid title fixed to 'On-Demand'
     (was 'On-Demand Agents' in prior commit).

AC2: agent-definitions.ts getBoardAgents adds explicit PR_REVIEWER inclusion
     and uses inclusion-based getOnDemandAgents (PROMPTER|SECRETARY roles).

AC5: team-health-cards.tsx static OnDemandAgentCard implementation refined
     with correct fallback rendering when no API team data.

AC6: pnpm lint and pnpm typecheck (src only) pass with zero new errors.

---------

Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>

* [b1c59206] Git page: Pull, Fetch, Rebase buttons wired to backend; Rebase destructive confirmation dialog (#192)

* [b1c59206] feat(git): add Pull, Fetch, Rebase operations to Git page with destructive confirmation dialog for Rebase

- Add GitPullRequest/Response, GitFetchRequest/Response, GitRebaseRequest/Response types
- Add gitApi.pull(), gitApi.fetch(), gitApi.rebase() with mock stubs for /git/pull, /git/fetch, /git/rebase
- Add useGitPull, useGitFetch, useGitRebase mutation hooks with cache invalidation; exported via useGitOperations
- Add Pull (Download icon), Fetch (RefreshCcw icon), Rebase (GitGraph icon) buttons to GitActionsPanel
- Rebase button triggers AlertDialog with destructive confirmation before calling API
- Wire handlePull, handleFetch, handleRebase handlers in git-browser.tsx with toast feedback

* [b1c59206] fix(git): add destructive styling and branch name to Rebase AlertDialog

- Add className='border-destructive bg-destructive/5' to AlertDialogContent
  so the dialog container has the required red-tinted styling (AC3)
- Update AlertDialogDescription to interpolate status?.current_branch so
  the dialog body explicitly names the branch being rebased (AC3)

---------

Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

* [3f305ed9] Frontend: Fix git control contract, complete Secretary restyling, and apply polish (CEO revision) (#199)

* [72de8a65] fix(git): correct Pull/Fetch/Rebase types, API mocks, request fields, and toast handlers (#197)

- types/git.ts: GitPullResponse and GitFetchResponse now have current_branch,
  has_changes, staged_files, unstaged_files, untracked_files, ahead, behind
  (matching backend GitStatusResponse); removed nonexistent commits_received/
  refs_updated/remote fields
- types/git.ts: GitRebaseRequest now uses target_branch: string (not onto?: string);
  GitRebaseResponse now has conflict: boolean and conflicted_files: string[]
  (removed branch/onto/commits_rebased); task_id made optional on all three
  request types
- lib/api/git.ts: Updated mock returns for pull/fetch/rebase to match new types
- git-actions-panel.tsx: onRebase prop now (targetBranch: string) => void;
  Rebase AlertDialog now contains an Input for target_branch; AlertDialogAction
  disabled when targetBranch empty and passes the value to onRebase
- git-browser.tsx: handlePull and handleFetch toast references result.current_branch;
  handleRebase accepts targetBranch, sends target_branch in payload, toasts
  result.conflict and result.conflicted_files; no 'manual' task_id for any
  pull/fetch/rebase operation

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>

* [be6a17fc] feat(ui): design-system polish — chart tokens, KB aria-label, Kanban touch targets (#196)

- kb-search-bar.tsx: add aria-label="Clear search" to the clear (X) button
- model-usage-donut.tsx: replace hex CHART_COLORS with var(--chart-1)…var(--chart-5)
- usage-time-series-chart.tsx: replace hex stopColor/stroke with var(--chart-1)/var(--chart-2)
- agent-usage-chart.tsx: Bar fill hex → var(--chart-1)
- team-usage-chart.tsx: Bar fill hex → var(--chart-1)
- kanban-card.tsx: min-h-11 → max-sm:min-h-11 (44px touch target mobile-only, 3 buttons)
- secretary-tab.tsx: already compliant (Button + design-system tokens), no change needed

Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

* [d62036bd] Backend: Fix git endpoint schemas, add safety gates, and unit tests (CEO revision) (#200)

* [d0593fe3] feat(git): remove agent_id from schemas and add service-layer safety gates (#195)

- Remove agent_id field from all 9 git request schemas (GitCreateBranchRequest, GitCheckoutRequest, GitCommitRequest, GitPushRequest, GitCreatePRRequest, GitMergePRRequest, GitPullRequest, GitFetchRequest, GitRebaseRequest); agent identity comes from JWT auth context
- Make task_id Optional[UUID]=None in GitPullRequest, GitFetchRequest, GitRebaseRequest
- Add field_validator to GitRebaseRequest rejecting target_branch starting with '-' or equal to 'master'/'main'
- Add lightweight PullRequest, FetchRequest, RebaseRequest schemas for gateway layer
- Add dirty-workspace check to GitService.pull() (raises ValidationError if porcelain output)
- Switch GitService.pull() to --ff-only; raises ValidationError with diverged-branch message on non-zero exit
- Add master/main guard to GitService.rebase() for both head_branch and target_branch
- Update callers: routes/tasks.py (2x), services/task.py, tests/unit/services/test_git.py (2x)

Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>

* [a2f96961] Add role-gated rebase endpoint and unit tests (test_git_rebase.py) (#198)

* [a2f96961] feat(git): add role-gated rebase endpoint and unit tests

Add role-gate to POST /rebase restricting access to DEVELOPER and
CELL_PM roles; add master/main protected-branch guard to
GitService.rebase() before any git subprocess runs; add 4 unit tests
in tests/unit/services/test_git_rebase.py covering both
target-branch and head-branch REBASE_FORBIDDEN cases

* [a2f96961] fix(git): invert rebase role gate, add ownership check, schema validator, and missing tests

- _REBASE_ALLOWED_ROLES changed from {DEVELOPER, CELL_PM} to {CEO, CELL_PM, MAIN_PM}
  so developers correctly receive 403 per AC1/AC2
- rebase_branch() now verifies task ownership for non-CEO PM callers: if task_id
  is provided and the task is not assigned to the calling agent, returns 403/404
- GitRebaseRequest.target_branch gets a @field_validator rejecting '-' prefix
  names and protected branch names (main, master, develop)
- GitService.pull() gains pre-flight safety gates: raises ValidationError
  DIRTY_TREE when staged/unstaged changes exist, DIVERGED_BRANCH when
  ahead > 0 and behind > 0
- test_git_rebase.py adds 9 new tests: pull() dirty-tree ValidationError,
  pull() diverged-branch ValidationError, pull() success path, schema
  validator for '-' prefix and protected names, and route-level tests
  confirming HTTP 403 for DEVELOPER and HTTP 200 for CELL_PM on POST /rebase

* [a2f96961] fix(tests): add type annotations for tuple variables in test_git_rebase.py

mypy needs explicit tuple type annotations when assigning bare tuples
to variables used as mock side_effect return values — fixes var-annotated
error caught by the server-side quality gate

---------

Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>

---------

Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>

* [94015c6d] Frontend R3: Fix legacy git taskId coercion + rebase placeholder + phantom fields (#204)

* [401ddb40] fix(git): remove phantom fields from GitPullRequest/GitFetchRequest and make task_id optional in write request interfaces; use taskId || undefined in git-browser.tsx handlers to avoid 422 errors when no task context is active (#201)

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>

* [cca8d0c0] fix(git): fix rebase placeholder and surface backend error in toast (#202)

git-actions-panel.tsx: change rebase target_branch Input placeholder
from "e.g. main or origin/main" to "Remote ref (e.g. origin/HEAD)" so
no default branch name (main/master/develop) is suggested.

git-browser.tsx: import getErrorMessage from @/lib/api/client and use
it in handleRebase catch block instead of the hardcoded string "Failed
to rebase". getErrorMessage extracts the real detail from
AxiosError.response.data.detail and falls back to a non-empty generic
message, satisfying both the detail-surfacing and fallback criteria.

Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>

* [1ea0fbcb] Backend R3: Relax legacy git schemas + fix integration tests (#206)

* [219c539b] Make task_id Optional in git request schemas and update service methods (#205)

* [219c539b] feat(git): make task_id Optional in git schemas and add None-guards in service methods

- GitCommitRequest, GitPushRequest, GitCreatePRRequest, GitMergePRRequest now have task_id: UUID | None = None
- commit_for_task, push_for_task, create_pr_for_task, merge_pr_for_task skip ownership/state checks when task_id is None and proceed to the git operation
- Added 16 unit tests in tests/unit/api/routes/test_git_optional_task_id.py covering schema validation and HTTP endpoint responses
- Added 4 integration tests in tests/integration/test_git_routes.py for no-422 behaviour
- All quality gates pass: ruff format, ruff check, mypy, pytest

* [219c539b] fix(tests): remove unused type-ignore comments, redundant cast, and invalid agent_id kwarg in git_optional_task_id unit tests

---------

Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>

* [de95ce94] test(git): fix 3 rebase integration tests to use non-protected target_branch (#203)

- Add pm_git_client fixture (CELL_PM role) needed for the role-gated rebase endpoint
- Change target_branch from 'main' to 'develop' in test_rebase_success, test_rebase_conflict, and test_rebase_git_command_error
- Remove task_id from request bodies (optional field; random UUIDs trigger 404)
- Switch all 3 rebase tests to use pm_git_client instead of git_client

Co-authored-by: Backend Developer 2 <be-dev-2@agents.roboco.dev>

---------

Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
Co-authored-by: Backend Developer 2 <be-dev-2@agents.roboco.dev>

* chore: ruff format test_agent_image_registry.py (unblock quality gate)

---------

Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
Co-authored-by: Backend Developer 2 <be-dev-2@agents.roboco.dev>
Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev>
Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-06-17 06:36:28 +02:00
committed by GitHub
co-authored by Backend Developer 2 Backend Developer 1 Frontend Developer 1 Frontend Developer 2 Renn F
parent e71a746499
commit 818f2ac7a6
48 changed files with 2867 additions and 575 deletions
+14
View File
@@ -17,6 +17,7 @@ import {
getBackendAgents,
getFrontendAgents,
getUxAgents,
getOnDemandAgents,
} from "@/lib/agent-definitions";
import {
OrchestratorStatusCards,
@@ -134,6 +135,19 @@ export default function AgentsPage() {
isLoading={(isLoading || agentsLoading) && !isOffline}
columns={4}
/>
{/* On-Demand section: Prompter/Intake and Secretary agents — only rendered
when the API returns at least one matching agent */}
{getOnDemandAgents(agents).length > 0 && (
<AgentGrid
title="On-Demand"
agents={getOnDemandAgents(agents)}
agentStatuses={agentStatuses}
agentUsage={agentUsageMap}
isLoading={(isLoading || agentsLoading) && !isOffline}
columns={4}
/>
)}
</div>
);
}
@@ -53,13 +53,14 @@ function ChannelList({ channels, selectedId, onSelect, isLoading }: ChannelListP
{title}
</h4>
{items.map((channel) => (
<button
<Button
key={channel.id}
onClick={() => onSelect(channel.id)}
variant="ghost"
className={
"w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-left transition-colors " +
"w-full h-auto justify-start gap-2 px-3 py-2 text-sm font-normal whitespace-normal " +
(selectedId === channel.id
? "bg-primary text-primary-foreground"
? "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground"
: "hover:bg-muted")
}
>
@@ -69,7 +70,7 @@ function ChannelList({ channels, selectedId, onSelect, isLoading }: ChannelListP
<Hash className="h-4 w-4 shrink-0" />
)}
<span className="truncate">{channel.name}</span>
</button>
</Button>
))}
</div>
);
@@ -135,13 +136,14 @@ function GroupList({ channelId, selectedId, onSelect }: GroupListProps) {
<ScrollArea className="h-full">
<div className="p-2 space-y-1">
{groups.map((group) => (
<button
<Button
key={group.id}
onClick={() => onSelect(group.id)}
variant="ghost"
className={
"w-full flex items-center justify-between px-3 py-2 rounded-lg text-sm text-left transition-colors " +
"w-full h-auto justify-between px-3 py-2 text-sm font-normal whitespace-normal " +
(selectedId === group.id
? "bg-primary text-primary-foreground"
? "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground"
: "hover:bg-muted")
}
>
@@ -152,7 +154,7 @@ function GroupList({ channelId, selectedId, onSelect }: GroupListProps) {
<Badge variant="secondary" className="text-xs shrink-0 ml-2">
{group.total_messages}
</Badge>
</button>
</Button>
))}
</div>
</ScrollArea>
+239 -156
View File
@@ -1,5 +1,7 @@
"use client";
import { Suspense } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import { useOrchestratorStatus } from "@/hooks/use-agents";
import { useTasks } from "@/hooks/use-tasks";
import {
@@ -14,10 +16,10 @@ import {
} from "@/hooks/use-usage";
import { TaskStatus, Team } from "@/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import { OfflineState } from "@/components/ui/offline-state";
import { Skeleton } from "@/components/ui/skeleton";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { OfflineState } from "@/components/ui/offline-state";
import {
UsageTimeSeriesChart,
ModelUsageDonut,
@@ -34,12 +36,30 @@ import {
Users,
CheckCircle,
XCircle,
RefreshCw,
Zap,
Timer,
Coins,
Sparkles,
} from "lucide-react";
import type { UsageProjection as UP, CacheEfficiencyResponse as CER } from "@/types";
// ─── Humanized number formatting ─────────────────────────────────────────────
/** Format counts with K/M suffix for values >= 1000. */
function humanizeCount(n: number): string {
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M";
if (n >= 1_000) return (n / 1_000).toFixed(1) + "K";
return String(n);
}
/** Format token counts (same as humanizeCount but used for token display). */
function fmtTokens(n: number): string {
if (n >= 1_000_000) return (n / 1_000_000).toFixed(2) + "M";
if (n >= 1_000) return (n / 1_000).toFixed(1) + "K";
return String(n);
}
// ─── Shared sub-components ────────────────────────────────────────────────────
interface MetricCardProps {
title: string;
@@ -51,6 +71,7 @@ interface MetricCardProps {
}
function MetricCard({ title, value, subtitle, icon, trend, trendValue }: MetricCardProps) {
const displayValue = typeof value === "number" ? humanizeCount(value) : value;
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
@@ -60,12 +81,12 @@ function MetricCard({ title, value, subtitle, icon, trend, trendValue }: MetricC
{icon}
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
<div className="text-2xl font-bold">{displayValue}</div>
{subtitle && (
<p className="text-xs text-muted-foreground mt-1">{subtitle}</p>
)}
{trend && trendValue && (
<div className={"flex items-center gap-1 mt-2 text-xs " +
<div className={"flex items-center gap-1 mt-2 text-xs " +
(trend === "up" ? "text-green-600" : trend === "down" ? "text-red-600" : "text-gray-500")
}>
<TrendingUp className={"h-3 w-3 " + (trend === "down" ? "rotate-180" : "")} />
@@ -118,7 +139,9 @@ function TeamHealthCard({ team, activeTasks, blockedTasks, completedToday }: Tea
);
}
export default function MetricsPage() {
// ─── Performance tab content ─────────────────────────────────────────────────
function PerformanceTabContent() {
const { data: tasks, error: tasksError, refetch: refetchTasks } = useTasks();
const { data: status, error: statusError, refetch: refetchStatus } = useOrchestratorStatus();
@@ -132,7 +155,6 @@ export default function MetricsPage() {
refetchStatus();
};
// Calculate metrics from local data
const taskList = tasks || [];
const agentList = status?.agents || [];
@@ -159,7 +181,7 @@ export default function MetricsPage() {
const awaitingQa = taskList.filter((t) => t.status === TaskStatus.AWAITING_QA).length;
const completed = taskList.filter((t) => t.status === TaskStatus.COMPLETED).length;
// Agent counts (from by_state if available, or count from agents array)
// Agent counts
const runningAgents = status?.by_state?.running || agentList.filter((a) => a.state === "running").length;
const idleAgents = status?.by_state?.idle || agentList.filter((a) => a.state === "idle" || a.state === "stopped").length;
const waitingAgents = status?.waiting_count || agentList.filter((a) => a.state === "waiting_long").length;
@@ -170,164 +192,145 @@ export default function MetricsPage() {
const teamTasks = taskList.filter((t) => t.team === team);
return {
team,
activeTasks: teamTasks.filter((t) =>
activeTasks: teamTasks.filter((t) =>
[TaskStatus.IN_PROGRESS, TaskStatus.CLAIMED].includes(t.status)
).length,
blockedTasks: teamTasks.filter((t) => t.status === TaskStatus.BLOCKED).length,
completedToday: teamTasks.filter((t) => {
if (!t.completed_at) return false;
const completed = new Date(t.completed_at);
const c = new Date(t.completed_at);
const today = new Date();
return completed.toDateString() === today.toDateString();
return c.toDateString() === today.toDateString();
}).length,
};
});
if (isOffline) {
return (
<OfflineState
title="Cannot Load Performance Metrics"
description="Start the RoboCo orchestrator to view performance analytics."
onRetry={refetch}
/>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Metrics</h1>
<p className="text-muted-foreground">
Performance analytics and operational insights
</p>
{/* Velocity Metrics */}
<div>
<h2 className="text-lg font-semibold mb-3">Velocity</h2>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-4 2xl:grid-cols-4">
<MetricCard
title="Completed Today"
value={completedToday}
subtitle="Tasks finished"
icon={<Zap className="h-4 w-4 text-green-500" />}
/>
<MetricCard
title="Completed This Week"
value={completedThisWeek}
subtitle="Rolling 7 days"
icon={<TrendingUp className="h-4 w-4 text-blue-500" />}
/>
<MetricCard
title="Total Completed"
value={completed}
subtitle="All time"
icon={<CheckCircle className="h-4 w-4 text-green-500" />}
/>
<MetricCard
title="Completion Rate"
value={taskList.length > 0 ? Math.round((completed / taskList.length) * 100) + "%" : "0%"}
subtitle="Of all tasks"
icon={<Activity className="h-4 w-4 text-purple-500" />}
/>
</div>
<Button variant="outline" onClick={refetch}>
<RefreshCw className="h-4 w-4 mr-2" />
Refresh
</Button>
</div>
{isOffline ? (
<OfflineState
title="Cannot Load Metrics"
description="Start the RoboCo orchestrator to view performance analytics."
onRetry={refetch}
/>
) : (
<>
{/* Velocity Metrics */}
<div>
<h2 className="text-lg font-semibold mb-3">Velocity</h2>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-4 2xl:grid-cols-4">
<MetricCard
title="Completed Today"
value={completedToday}
subtitle="Tasks finished"
icon={<Zap className="h-4 w-4 text-green-500" />}
/>
<MetricCard
title="Completed This Week"
value={completedThisWeek}
subtitle="Rolling 7 days"
icon={<TrendingUp className="h-4 w-4 text-blue-500" />}
/>
<MetricCard
title="Total Completed"
value={completed}
subtitle="All time"
icon={<CheckCircle className="h-4 w-4 text-green-500" />}
/>
<MetricCard
title="Completion Rate"
value={taskList.length > 0 ? Math.round((completed / taskList.length) * 100) + "%" : "0%"}
subtitle="Of all tasks"
icon={<Activity className="h-4 w-4 text-purple-500" />}
/>
</div>
</div>
{/* Task Status */}
<div>
<h2 className="text-lg font-semibold mb-3">Task Status</h2>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5 xl:grid-cols-5 2xl:grid-cols-5">
<MetricCard
title="Pending"
value={pending}
icon={<Clock className="h-4 w-4 text-gray-500" />}
/>
<MetricCard
title="In Progress"
value={inProgress}
icon={<Activity className="h-4 w-4 text-blue-500" />}
/>
<MetricCard
title="Blocked"
value={blocked}
icon={<AlertTriangle className="h-4 w-4 text-red-500" />}
/>
<MetricCard
title="Awaiting QA"
value={awaitingQa}
icon={<Timer className="h-4 w-4 text-yellow-500" />}
/>
<MetricCard
title="Completed"
value={completed}
icon={<CheckCircle className="h-4 w-4 text-green-500" />}
/>
</div>
</div>
{/* Task Status */}
<div>
<h2 className="text-lg font-semibold mb-3">Task Status</h2>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5 xl:grid-cols-5 2xl:grid-cols-5">
<MetricCard
title="Pending"
value={pending}
icon={<Clock className="h-4 w-4 text-gray-500" />}
/>
<MetricCard
title="In Progress"
value={inProgress}
icon={<Activity className="h-4 w-4 text-blue-500" />}
/>
<MetricCard
title="Blocked"
value={blocked}
icon={<AlertTriangle className="h-4 w-4 text-red-500" />}
/>
<MetricCard
title="Awaiting QA"
value={awaitingQa}
icon={<Timer className="h-4 w-4 text-yellow-500" />}
/>
<MetricCard
title="Completed"
value={completed}
icon={<CheckCircle className="h-4 w-4 text-green-500" />}
/>
</div>
</div>
{/* Agent Status */}
<div>
<h2 className="text-lg font-semibold mb-3">Agent Status</h2>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-4 2xl:grid-cols-4">
<MetricCard
title="Running"
value={runningAgents}
subtitle="Active agents"
icon={<Users className="h-4 w-4 text-green-500" />}
/>
<MetricCard
title="Idle"
value={idleAgents}
subtitle="Available"
icon={<Users className="h-4 w-4 text-gray-500" />}
/>
<MetricCard
title="Waiting"
value={waitingAgents}
subtitle="Needs input"
icon={<Clock className="h-4 w-4 text-yellow-500" />}
/>
<MetricCard
title="Errors"
value={errorAgents}
subtitle="Failed agents"
icon={<XCircle className="h-4 w-4 text-red-500" />}
/>
</div>
</div>
{/* Agent Status */}
<div>
<h2 className="text-lg font-semibold mb-3">Agent Status</h2>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-4 2xl:grid-cols-4">
<MetricCard
title="Running"
value={runningAgents}
subtitle="Active agents"
icon={<Users className="h-4 w-4 text-green-500" />}
/>
<MetricCard
title="Idle"
value={idleAgents}
subtitle="Available"
icon={<Users className="h-4 w-4 text-gray-500" />}
/>
<MetricCard
title="Waiting"
value={waitingAgents}
subtitle="Needs input"
icon={<Clock className="h-4 w-4 text-yellow-500" />}
/>
<MetricCard
title="Errors"
value={errorAgents}
subtitle="Failed agents"
icon={<XCircle className="h-4 w-4 text-red-500" />}
/>
</div>
</div>
{/* Team Health */}
<div>
<h2 className="text-lg font-semibold mb-3">Team Health</h2>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5 xl:grid-cols-5 2xl:grid-cols-6">
{teamMetrics.map((tm) => (
<TeamHealthCard
key={tm.team}
team={tm.team}
activeTasks={tm.activeTasks}
blockedTasks={tm.blockedTasks}
completedToday={tm.completedToday}
/>
))}
</div>
</div>
{/* ─── Token Usage & Costs ─────────────────────────────────── */}
<TokenUsageCostsSection />
</>
)}
{/* Team Health */}
<div>
<h2 className="text-lg font-semibold mb-3">Team Health</h2>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5 xl:grid-cols-5 2xl:grid-cols-6">
{teamMetrics.map((tm) => (
<TeamHealthCard
key={tm.team}
team={tm.team}
activeTasks={tm.activeTasks}
blockedTasks={tm.blockedTasks}
completedToday={tm.completedToday}
/>
))}
</div>
</div>
</div>
);
}
// =============================================================================
// TOKEN USAGE & COSTS SECTION
// =============================================================================
// ─── Token Usage & Costs tab content ─────────────────────────────────────────
function TokenUsageCostsSection() {
const { data: summary, isLoading: loadingSnap } = useUsageSummary("24h");
@@ -343,8 +346,6 @@ function TokenUsageCostsSection() {
return (
<div className="space-y-6">
<h2 className="text-lg font-semibold">Token Usage &amp; Costs</h2>
{/* Row 1 — Summary cards */}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 2xl:grid-cols-6">
<SummaryCard
@@ -411,7 +412,7 @@ function TokenUsageCostsSection() {
<CacheEfficiencyCard cacheStats={cacheStats} isLoading={loadingCache} />
</div>
{/* Row 5 — Sessions table (mock-mode only; empty in production) */}
{/* Row 5 — Sessions table */}
<SessionsTable data={sessions} isLoading={loadingSessions} />
</div>
);
@@ -419,12 +420,6 @@ function TokenUsageCostsSection() {
// ─── Helper sub-components ────────────────────────────────────────────────────
function fmtTokens(n: number): string {
if (n >= 1_000_000) return (n / 1_000_000).toFixed(2) + "M";
if (n >= 1_000) return (n / 1_000).toFixed(1) + "K";
return String(n);
}
interface SummaryCardProps {
title: string;
value: string | undefined;
@@ -463,8 +458,6 @@ function SummaryCard({ title, value, icon, trend, isLoading }: SummaryCardProps)
);
}
import type { UsageProjection as UP, CacheEfficiencyResponse as CER } from "@/types";
interface ProjectionCardProps {
projection: UP | undefined;
isLoading: boolean;
@@ -531,3 +524,93 @@ function CacheEfficiencyCard({ cacheStats, isLoading }: CacheEfficiencyCardProps
</Card>
);
}
// ─── Tab types ────────────────────────────────────────────────────────────────
type MetricsTab = "performance" | "token-usage";
const VALID_METRICS_TABS: MetricsTab[] = ["performance", "token-usage"];
function isValidMetricsTab(value: string | null): value is MetricsTab {
return VALID_METRICS_TABS.includes(value as MetricsTab);
}
// ─── Main page content (uses useSearchParams) ─────────────────────────────────
function MetricsPageContent() {
const router = useRouter();
const searchParams = useSearchParams();
// Read ?tab= from URL, default to "performance"
const rawTab = searchParams.get("tab");
const activeTab: MetricsTab = isValidMetricsTab(rawTab) ? rawTab : "performance";
function handleTabChange(value: string) {
const params = new URLSearchParams(searchParams.toString());
params.set("tab", value);
router.push(`?${params.toString()}`);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Metrics</h1>
<p className="text-muted-foreground">
Performance analytics and operational insights
</p>
</div>
</div>
<Tabs value={activeTab} onValueChange={handleTabChange}>
<TabsList>
<TabsTrigger value="performance">Performance</TabsTrigger>
<TabsTrigger value="token-usage">Token Usage</TabsTrigger>
</TabsList>
<TabsContent value="performance" className="mt-6">
<PerformanceTabContent />
</TabsContent>
<TabsContent value="token-usage" className="mt-6">
<TokenUsageCostsSection />
</TabsContent>
</Tabs>
</div>
);
}
// Wrap in Suspense for useSearchParams
export default function MetricsPage() {
return (
<Suspense fallback={
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<Skeleton className="h-9 w-32 mb-2" />
<Skeleton className="h-5 w-72" />
</div>
</div>
<div className="flex gap-2">
<Skeleton className="h-9 w-28" />
<Skeleton className="h-9 w-28" />
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<Card key={i}>
<CardHeader className="pb-2">
<Skeleton className="h-4 w-24" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-16" />
</CardContent>
</Card>
))}
</div>
</div>
}>
<MetricsPageContent />
</Suspense>
);
}
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import { Suspense } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import {
useNotifications,
useMarkNotificationRead,
@@ -105,17 +106,34 @@ function NotificationCard({ notification, onMarkRead, onAcknowledge }: Notificat
);
}
export default function NotificationsPage() {
// Default to Unread: the actionable view. Landing on "All" buries new
// notifications under everything already seen.
const [activeTab, setActiveTab] = useState<"all" | "unread" | "pending">("unread");
type TabValue = "all" | "unread" | "pending";
const VALID_TABS: TabValue[] = ["all", "unread", "pending"];
function isValidTab(value: string | null): value is TabValue {
return VALID_TABS.includes(value as TabValue);
}
function NotificationsPageContent() {
const router = useRouter();
const searchParams = useSearchParams();
// Read ?tab= from URL, default to "unread" (most actionable view)
const rawTab = searchParams.get("tab");
const activeTab: TabValue = isValidTab(rawTab) ? rawTab : "unread";
function handleTabChange(value: string) {
const params = new URLSearchParams(searchParams.toString());
params.set("tab", value);
router.push(`?${params.toString()}`);
}
const { data, isLoading, error, refetch } = useNotifications(
activeTab === "unread" ? { unread_only: true } :
activeTab === "pending" ? { pending_ack_only: true } :
undefined
);
const markRead = useMarkNotificationRead();
const acknowledge = useAcknowledgeNotification();
const markAllRead = useMarkAllNotificationsRead();
@@ -219,7 +237,7 @@ export default function NotificationsPage() {
onRetry={() => refetch()}
/>
) : (
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as typeof activeTab)}>
<Tabs value={activeTab} onValueChange={handleTabChange}>
<TabsList>
<TabsTrigger value="all">All</TabsTrigger>
<TabsTrigger value="unread">
@@ -262,3 +280,46 @@ export default function NotificationsPage() {
</div>
);
}
// Wrap in Suspense for useSearchParams
export default function NotificationsPage() {
return (
<Suspense fallback={
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<Skeleton className="h-9 w-48 mb-2" />
<Skeleton className="h-5 w-72" />
</div>
<div className="flex items-center gap-2">
<Skeleton className="h-9 w-36" />
<Skeleton className="h-9 w-24" />
</div>
</div>
<div className="grid grid-cols-3 gap-4">
{Array.from({ length: 3 }).map((_, i) => (
<Card key={i}>
<CardHeader className="pb-2">
<Skeleton className="h-4 w-16" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-12" />
</CardContent>
</Card>
))}
</div>
<div className="space-y-3">
{Array.from({ length: 5 }).map((_, i) => (
<Card key={i}>
<CardContent className="p-4">
<Skeleton className="h-20 w-full" />
</CardContent>
</Card>
))}
</div>
</div>
}>
<NotificationsPageContent />
</Suspense>
);
}
@@ -2,7 +2,7 @@ import { AIRoutingCard } from "@/components/settings/ai-routing-card";
export default function AIProvidersPage() {
return (
<div className="space-y-6 max-w-5xl">
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold tracking-tight">AI Providers</h1>
<p className="text-muted-foreground">
+175 -172
View File
@@ -43,7 +43,7 @@ export default function SettingsPage() {
};
return (
<div className="space-y-6 max-w-3xl">
<div className="space-y-6">
{/* Header */}
<div>
<h1 className="text-3xl font-bold tracking-tight">Settings</h1>
@@ -52,189 +52,192 @@ export default function SettingsPage() {
</p>
</div>
{/* Appearance */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Palette className="h-5 w-5" />
Appearance
</CardTitle>
<CardDescription>Customize the look and feel of the panel</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label>Theme</Label>
<p className="text-sm text-muted-foreground">
Select your preferred color scheme
</p>
{/* Cards grid — two columns on large screens */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Appearance */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Palette className="h-5 w-5" />
Appearance
</CardTitle>
<CardDescription>Customize the look and feel of the panel</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label>Theme</Label>
<p className="text-sm text-muted-foreground">
Select your preferred color scheme
</p>
</div>
<Select value={theme} onValueChange={setTheme}>
<SelectTrigger className="w-auto min-w-28">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
</SelectContent>
</Select>
</div>
<Select value={theme} onValueChange={setTheme}>
<SelectTrigger className="w-auto min-w-28">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
</SelectContent>
</Select>
</div>
<Separator />
<div className="flex items-center justify-between">
<div>
<Label>Collapsed Sidebar</Label>
<p className="text-sm text-muted-foreground">
Show icons only in the sidebar
</p>
<Separator />
<div className="flex items-center justify-between">
<div>
<Label>Collapsed Sidebar</Label>
<p className="text-sm text-muted-foreground">
Show icons only in the sidebar
</p>
</div>
<Switch
checked={sidebarCollapsed}
onCheckedChange={setSidebarCollapsed}
/>
</div>
<Switch
checked={sidebarCollapsed}
onCheckedChange={setSidebarCollapsed}
/>
</div>
</CardContent>
</Card>
</CardContent>
</Card>
{/* Notifications */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Bell className="h-5 w-5" />
Notifications
</CardTitle>
<CardDescription>Configure how you receive updates</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label>Enable Notifications</Label>
<p className="text-sm text-muted-foreground">
Receive real-time notifications from agents
</p>
{/* Notifications */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Bell className="h-5 w-5" />
Notifications
</CardTitle>
<CardDescription>Configure how you receive updates</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label>Enable Notifications</Label>
<p className="text-sm text-muted-foreground">
Receive real-time notifications from agents
</p>
</div>
<Switch
checked={notificationsEnabled}
onCheckedChange={setNotificationsEnabled}
/>
</div>
<Switch
checked={notificationsEnabled}
onCheckedChange={setNotificationsEnabled}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<div>
<Label>Sound Alerts</Label>
<p className="text-sm text-muted-foreground">
Play sound for important notifications
</p>
<Separator />
<div className="flex items-center justify-between">
<div>
<Label>Sound Alerts</Label>
<p className="text-sm text-muted-foreground">
Play sound for important notifications
</p>
</div>
<Switch
checked={soundEnabled}
onCheckedChange={setSoundEnabled}
disabled={!notificationsEnabled}
/>
</div>
<Switch
checked={soundEnabled}
onCheckedChange={setSoundEnabled}
disabled={!notificationsEnabled}
/>
</div>
</CardContent>
</Card>
</CardContent>
</Card>
{/* Data & Refresh */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Server className="h-5 w-5" />
Data & Refresh
</CardTitle>
<CardDescription>Configure data fetching behavior</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label>Auto Refresh</Label>
<p className="text-sm text-muted-foreground">
Automatically refresh data periodically
</p>
{/* Data & Refresh */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Server className="h-5 w-5" />
Data & Refresh
</CardTitle>
<CardDescription>Configure data fetching behavior</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label>Auto Refresh</Label>
<p className="text-sm text-muted-foreground">
Automatically refresh data periodically
</p>
</div>
<Switch
checked={autoRefresh}
onCheckedChange={setAutoRefresh}
/>
</div>
<Switch
checked={autoRefresh}
onCheckedChange={setAutoRefresh}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<div>
<Label>Refresh Interval</Label>
<p className="text-sm text-muted-foreground">
How often to fetch new data (seconds)
</p>
<Separator />
<div className="flex items-center justify-between">
<div>
<Label>Refresh Interval</Label>
<p className="text-sm text-muted-foreground">
How often to fetch new data (seconds)
</p>
</div>
<Select
value={refreshInterval}
onValueChange={setRefreshInterval}
disabled={!autoRefresh}
>
<SelectTrigger className="w-auto min-w-20">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="10">10s</SelectItem>
<SelectItem value="30">30s</SelectItem>
<SelectItem value="60">1m</SelectItem>
<SelectItem value="300">5m</SelectItem>
</SelectContent>
</Select>
</div>
<Select
value={refreshInterval}
onValueChange={setRefreshInterval}
disabled={!autoRefresh}
>
<SelectTrigger className="w-auto min-w-20">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="10">10s</SelectItem>
<SelectItem value="30">30s</SelectItem>
<SelectItem value="60">1m</SelectItem>
<SelectItem value="300">5m</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
</CardContent>
</Card>
{/* Transcript Retention (panel-tunable; persisted server-side) */}
<TranscriptRetentionCard />
{/* Transcript Retention (panel-tunable; persisted server-side) */}
<TranscriptRetentionCard />
{/* Connection Info */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Settings className="h-5 w-5" />
Connection Info
</CardTitle>
<CardDescription>Backend API configuration (read-only)</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>API URL</Label>
<Input value={API_URL} readOnly className="bg-muted" />
</div>
<div className="space-y-2">
<Label>WebSocket URL</Label>
<Input value={WS_URL} readOnly className="bg-muted" />
</div>
<p className="text-xs text-muted-foreground">
These values are configured via environment variables (NEXT_PUBLIC_API_URL, NEXT_PUBLIC_WS_URL)
</p>
</CardContent>
</Card>
{/* Connection Info */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Settings className="h-5 w-5" />
Connection Info
</CardTitle>
<CardDescription>Backend API configuration (read-only)</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>API URL</Label>
<Input value={API_URL} readOnly className="bg-muted" />
</div>
<div className="space-y-2">
<Label>WebSocket URL</Label>
<Input value={WS_URL} readOnly className="bg-muted" />
</div>
<p className="text-xs text-muted-foreground">
These values are configured via environment variables (NEXT_PUBLIC_API_URL, NEXT_PUBLIC_WS_URL)
</p>
</CardContent>
</Card>
{/* User Info */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User className="h-5 w-5" />
User Info
</CardTitle>
<CardDescription>Your account information</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-4">
<div className="h-16 w-16 rounded-full bg-primary flex items-center justify-center">
<span className="text-primary-foreground font-bold text-2xl">CEO</span>
{/* User Info */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User className="h-5 w-5" />
User Info
</CardTitle>
<CardDescription>Your account information</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-4">
<div className="h-16 w-16 rounded-full bg-primary flex items-center justify-center">
<span className="text-primary-foreground font-bold text-2xl">CEO</span>
</div>
<div>
<p className="font-semibold text-lg">Renzo</p>
<p className="text-sm text-muted-foreground">Chief Executive Officer</p>
<p className="text-xs text-muted-foreground mt-1">
Agent ID: 00000000-0000-0000-0000-000000000001
</p>
</div>
</div>
<div>
<p className="font-semibold text-lg">Renzo</p>
<p className="text-sm text-muted-foreground">Chief Executive Officer</p>
<p className="text-xs text-muted-foreground mt-1">
Agent ID: 00000000-0000-0000-0000-000000000001
</p>
</div>
</div>
</CardContent>
</Card>
</CardContent>
</Card>
</div>
{/* Save Button */}
<div className="flex justify-end">