fix(.gitignore): anchor Python build artifacts; recover panel/src/lib (28 files)

The 'lib/' rule (intended for Python virtualenv at repo root) was matching panel/src/lib/, hiding the entire panel API client + utility tree from git. Anchored Python build-artifact rules to the repo root with a leading slash so they only match at the top level. Adds 28 panel/src/lib files that should have been tracked from day one.
This commit is contained in:
Renn F
2026-05-02 03:18:47 +02:00
parent 62bda0c497
commit a82a4f9fd4
44 changed files with 5875 additions and 51 deletions
+43
View File
@@ -0,0 +1,43 @@
/**
* Agent Definitions
*
* Helper functions to filter agents by organizational group.
* Agent data is now fetched from API via useAgentDefinitions() hook.
*/
import { AgentRole, Team } from "@/types";
export interface AgentDefinition {
id: string;
name: string;
role: AgentRole | null;
team: Team | null;
}
// Helper functions to filter agents by group
// These now accept agents as a parameter instead of using static data
// All functions handle undefined/null gracefully
export const getBoardAgents = (agents: AgentDefinition[] | undefined | null) =>
(agents ?? []).filter(
(a) =>
a.team === Team.BOARD ||
a.role === AgentRole.HEAD_MARKETING ||
a.role === AgentRole.AUDITOR ||
a.role === AgentRole.PRODUCT_OWNER
);
export const getMainPm = (agents: AgentDefinition[] | undefined | null) =>
(agents ?? []).filter((a) => a.role === AgentRole.MAIN_PM);
export const getBackendAgents = (agents: AgentDefinition[] | undefined | null) =>
(agents ?? []).filter((a) => a.team === Team.BACKEND);
export const getFrontendAgents = (agents: AgentDefinition[] | undefined | null) =>
(agents ?? []).filter((a) => a.team === Team.FRONTEND);
export const getUxAgents = (agents: AgentDefinition[] | undefined | null) =>
(agents ?? []).filter((a) => a.team === Team.UX_UI);
export const getMarketingAgents = (agents: AgentDefinition[] | undefined | null) =>
(agents ?? []).filter((a) => a.team === Team.MARKETING);
+167
View File
@@ -0,0 +1,167 @@
/**
* Agent Display Utilities
*
* Utilities for resolving agent IDs (slugs or UUIDs) to human-readable names.
*/
// Static UUID → slug mapping (from backend seeds/initial_data.py)
// NEVER change these after initial deployment
const AGENT_UUIDS: Record<string, string> = {
// CEO (Human)
"00000000-0000-0000-0000-000000000001": "ceo",
// Backend Cell
"00000000-0000-0000-0001-000000000001": "be-dev-1",
"00000000-0000-0000-0001-000000000002": "be-dev-2",
"00000000-0000-0000-0001-000000000003": "be-qa",
"00000000-0000-0000-0001-000000000004": "be-pm",
"00000000-0000-0000-0001-000000000005": "be-doc",
// Frontend Cell
"00000000-0000-0000-0002-000000000001": "fe-dev-1",
"00000000-0000-0000-0002-000000000002": "fe-dev-2",
"00000000-0000-0000-0002-000000000003": "fe-qa",
"00000000-0000-0000-0002-000000000004": "fe-pm",
"00000000-0000-0000-0002-000000000005": "fe-doc",
// UX/UI Cell
"00000000-0000-0000-0003-000000000001": "ux-dev-1",
"00000000-0000-0000-0003-000000000002": "ux-dev-2",
"00000000-0000-0000-0003-000000000003": "ux-qa",
"00000000-0000-0000-0003-000000000004": "ux-pm",
"00000000-0000-0000-0003-000000000005": "ux-doc",
// Board / Management
"00000000-0000-0000-0004-000000000001": "main-pm",
"00000000-0000-0000-0004-000000000002": "product-owner",
"00000000-0000-0000-0004-000000000003": "head-marketing",
"00000000-0000-0000-0004-000000000004": "auditor",
};
// Static agent name mapping (slug -> display name)
// This matches the backend seed data
const AGENT_NAMES: Record<string, string> = {
// Board / Management
"main-pm": "Main PM",
"product-owner": "Product Owner",
"head-marketing": "Head Marketing",
"auditor": "Auditor",
// Backend Cell
"be-pm": "Backend PM",
"be-dev-1": "Backend Dev 1",
"be-dev-2": "Backend Dev 2",
"be-qa": "Backend QA",
"be-doc": "Backend Doc",
// Frontend Cell
"fe-pm": "Frontend PM",
"fe-dev-1": "Frontend Dev 1",
"fe-dev-2": "Frontend Dev 2",
"fe-qa": "Frontend QA",
"fe-doc": "Frontend Doc",
// UX/UI Cell
"ux-pm": "UX/UI PM",
"ux-dev-1": "UX/UI Dev 1",
"ux-dev-2": "UX/UI Dev 2",
"ux-qa": "UX/UI QA",
"ux-doc": "UX/UI Doc",
// CEO (human)
"ceo": "CEO",
"CEO": "CEO",
};
/**
* Resolve agent ID (UUID or slug) to slug.
*/
export function resolveToSlug(agentId: string | null | undefined): string {
if (!agentId) return "";
// If it's a known UUID, return the slug
if (AGENT_UUIDS[agentId]) {
return AGENT_UUIDS[agentId];
}
// Already a slug or unknown
return agentId;
}
/**
* Get display name for an agent ID.
* Works with both slugs (be-pm) and UUIDs.
*
* @param agentId - The agent identifier (slug or UUID)
* @returns The human-readable name, or the slug, or "Unknown Agent" for unrecognized UUIDs
*/
export function getAgentDisplayName(agentId: string | null | undefined): string {
if (!agentId) return "Unassigned";
// First resolve UUID to slug if applicable
const slug = resolveToSlug(agentId);
// Check if it's a known slug
if (AGENT_NAMES[slug]) {
return AGENT_NAMES[slug];
}
// Check if the original was a UUID that we couldn't resolve
if (agentId.length === 36 && agentId.includes("-")) {
// Unknown UUID - show first 8 chars
return agentId.slice(0, 8);
}
// It's an unknown slug - return as-is
return slug;
}
// 3-letter codes for agents (slug -> code)
const AGENT_CODES: Record<string, string> = {
// Board / Management
"main-pm": "MPM",
"product-owner": "PO",
"head-marketing": "MKT",
"auditor": "AUD",
// Backend Cell
"be-pm": "BPM",
"be-dev-1": "BD1",
"be-dev-2": "BD2",
"be-qa": "BQA",
"be-doc": "BDC",
// Frontend Cell
"fe-pm": "FPM",
"fe-dev-1": "FD1",
"fe-dev-2": "FD2",
"fe-qa": "FQA",
"fe-doc": "FDC",
// UX/UI Cell
"ux-pm": "UPM",
"ux-dev-1": "UD1",
"ux-dev-2": "UD2",
"ux-qa": "UQA",
"ux-doc": "UDC",
// CEO
"ceo": "CEO",
"CEO": "CEO",
};
/**
* Get initials for an agent (for avatars).
*
* @param agentId - The agent identifier
* @returns 3-character code
*/
export function getAgentInitials(agentId: string | null | undefined): string {
if (!agentId) return "???";
// First resolve UUID to slug if applicable
const slug = resolveToSlug(agentId);
// Check for known code
if (AGENT_CODES[slug]) {
return AGENT_CODES[slug];
}
// Fallback: first 3 chars of display name
const displayName = getAgentDisplayName(agentId);
return displayName.slice(0, 3).toUpperCase();
}
/**
* Check if an agent ID is a known agent slug.
*/
export function isKnownAgent(agentId: string | null | undefined): boolean {
if (!agentId) return false;
return agentId in AGENT_NAMES;
}
+186
View File
@@ -0,0 +1,186 @@
/**
* A2A (Agent-to-Agent) Protocol API Client
*
* API functions for agent-to-agent communication protocol.
*/
import api from "./client";
import { isMockMode } from "@/lib/mock-data";
// =============================================================================
// Types
// =============================================================================
export interface A2AMessage {
id: string;
from_agent_id: string;
to_agent_id: string;
content: string;
message_type: string;
timestamp: string;
metadata?: Record<string, unknown>;
}
export interface A2AMessageSendRequest {
to_agent_id: string;
content: string;
message_type?: string;
task_id?: string;
metadata?: Record<string, unknown>;
}
export interface A2AMessageResponse {
message_id: string;
status: string;
delivered_at?: string;
}
export interface A2ATask {
id: string;
title: string;
status: string;
assigned_to: string | null;
created_at: string;
}
export interface A2AAgentCard {
agent_id: string;
name: string;
role: string;
capabilities: string[];
status: string;
current_task_id: string | null;
}
export interface A2AStreamChunk {
chunk_id: string;
content: string;
is_final: boolean;
}
// =============================================================================
// API Client
// =============================================================================
export const a2aApi = {
// ===========================================================================
// MESSAGE ENDPOINTS
// ===========================================================================
/**
* Send a message to another agent
*/
sendMessage: async (request: A2AMessageSendRequest): Promise<A2AMessageResponse> => {
if (isMockMode()) {
return {
message_id: `msg-${Date.now()}`,
status: "delivered",
delivered_at: new Date().toISOString(),
};
}
const { data } = await api.post<A2AMessageResponse>("/a2a/message/send", request);
return data;
},
/**
* Stream a message to another agent (for long content)
*/
streamMessage: async (request: A2AMessageSendRequest): Promise<A2AMessageResponse> => {
if (isMockMode()) {
return {
message_id: `msg-${Date.now()}`,
status: "streaming",
};
}
const { data } = await api.post<A2AMessageResponse>("/a2a/message/stream", request);
return data;
},
// ===========================================================================
// TASK ENDPOINTS
// ===========================================================================
/**
* List tasks visible via A2A protocol
*/
listTasks: async (): Promise<A2ATask[]> => {
if (isMockMode()) {
return [];
}
const { data } = await api.get<A2ATask[]>("/a2a/tasks");
return data;
},
/**
* Get a specific task via A2A protocol
*/
getTask: async (taskId: string): Promise<A2ATask> => {
if (isMockMode()) {
return {
id: taskId,
title: "Mock Task",
status: "in_progress",
assigned_to: null,
created_at: new Date().toISOString(),
};
}
const { data } = await api.get<A2ATask>(`/a2a/tasks/${taskId}`);
return data;
},
/**
* Subscribe to task updates (returns SSE stream URL)
* Note: This endpoint returns Server-Sent Events, handle appropriately
*/
subscribeToTask: (taskId: string): string => {
// Returns the URL for SSE subscription
return `/a2a/tasks/${taskId}/subscribe`;
},
/**
* Cancel a task via A2A protocol
*/
cancelTask: async (taskId: string): Promise<{ status: string; task_id: string }> => {
if (isMockMode()) {
return {
status: "cancelled",
task_id: taskId,
};
}
const { data } = await api.post<{ status: string; task_id: string }>(`/a2a/tasks/${taskId}/cancel`);
return data;
},
// ===========================================================================
// AGENT ENDPOINTS
// ===========================================================================
/**
* List all agents available via A2A protocol
*/
listAgents: async (): Promise<A2AAgentCard[]> => {
if (isMockMode()) {
return [];
}
const { data } = await api.get<A2AAgentCard[]>("/a2a/agents");
return data;
},
/**
* Get agent card (profile/capabilities)
*/
getAgentCard: async (agentId: string): Promise<A2AAgentCard> => {
if (isMockMode()) {
return {
agent_id: agentId,
name: "Mock Agent",
role: "developer",
capabilities: ["coding", "testing"],
status: "idle",
current_task_id: null,
};
}
const { data } = await api.get<A2AAgentCard>(`/a2a/agents/${agentId}/card`);
return data;
},
};
+70
View File
@@ -0,0 +1,70 @@
/**
* Agents API Client
*
* Fetches agent data from the backend API.
*/
import api from "./client";
import type { AgentRole, Team } from "@/types";
import { isMockMode, mockAgents } from "@/lib/mock-data";
export interface AgentDefinition {
id: string; // slug from backend (e.g., "be-dev-1")
name: string;
role: AgentRole | null;
team: Team | null;
}
interface AgentApiResponse {
id: string; // UUID (not used for definitions)
name: string;
slug: string;
role: string;
team: string | null;
}
export const agentsApi = {
/**
* Get all agents from the API
*/
getAll: async (): Promise<AgentDefinition[]> => {
if (isMockMode()) {
return mockAgents.map((a) => ({
id: a.slug || a.id,
name: a.name,
role: a.role,
team: a.team,
}));
}
const response = await api.get<AgentApiResponse[]>("/agents");
const data = response.data;
// Handle unexpected response formats
if (!data || !Array.isArray(data)) {
console.warn("Unexpected agents API response:", data);
return [];
}
return data.map((a) => ({
id: a.slug || a.id, // Use slug as ID, fallback to UUID
name: a.name || "Unknown",
role: (a.role as AgentRole) || null,
team: (a.team as Team) || null,
}));
},
/**
* Get a single agent by ID or slug
*/
getOne: async (idOrSlug: string): Promise<AgentDefinition> => {
const response = await api.get<AgentApiResponse>(`/agents/${idOrSlug}`);
const a = response.data;
return {
id: a.slug,
name: a.name,
role: a.role as AgentRole,
team: a.team as Team | null,
};
},
};
+135
View File
@@ -0,0 +1,135 @@
import api from "./client";
import type { Channel, PaginatedResponse, Group } from "@/types";
import { ChannelType } from "@/types";
import { isMockMode, mockChannels, mockGroups } from "@/lib/mock-data";
export interface ChannelFilters {
type?: string;
is_private?: boolean;
}
export interface ChannelCreate {
name: string;
slug: string;
type?: string;
description?: string;
topic?: string;
is_private?: boolean;
}
export interface ChannelUpdate {
name?: string;
description?: string;
topic?: string;
is_archived?: boolean;
}
export const channelsApi = {
// List all channels
list: async (filters?: ChannelFilters): Promise<Channel[]> => {
if (isMockMode()) {
let channels = [...mockChannels] as Channel[];
if (filters?.type) {
channels = channels.filter((c) => c.type === filters.type);
}
if (filters?.is_private !== undefined) {
channels = channels.filter((c) => c.is_private === filters.is_private);
}
return channels;
}
const { data } = await api.get<PaginatedResponse<Channel>>("/channels", { params: filters });
return data.items;
},
// Get channel by ID
get: async (channelId: string): Promise<Channel> => {
if (isMockMode()) {
const channel = mockChannels.find((c) => c.id === channelId);
if (channel) return channel as Channel;
throw new Error("Channel not found");
}
const { data } = await api.get<Channel>("/channels/" + channelId);
return data;
},
// Get channel by slug
getBySlug: async (slug: string): Promise<Channel> => {
if (isMockMode()) {
const channel = mockChannels.find((c) => c.slug === slug);
if (channel) return channel as Channel;
throw new Error("Channel not found");
}
// Backend uses query param filter, not path segment
const { data } = await api.get<PaginatedResponse<Channel>>("/channels", {
params: { slug },
});
if (!data.items.length) {
throw new Error("Channel not found");
}
return data.items[0];
},
// Get groups for a channel
getGroups: async (channelId: string): Promise<Group[]> => {
if (isMockMode()) {
return mockGroups as Group[];
}
const { data } = await api.get<Group[]>("/channels/" + channelId + "/groups");
return data;
},
// Create a new channel (PM/CEO only)
create: async (channel: ChannelCreate): Promise<Channel> => {
if (isMockMode()) {
const newChannel: Channel = {
id: `channel-${Date.now()}`,
name: channel.name,
slug: channel.slug,
type: (channel.type as ChannelType) || ChannelType.CELL,
description: channel.description || null,
topic: channel.topic || null,
is_private: channel.is_private || false,
is_archived: false,
member_count: 0,
message_count: 0,
group_count: 0,
can_write: true,
};
(mockChannels as Channel[]).push(newChannel);
return newChannel;
}
const { data } = await api.post<Channel>("/channels", channel);
return data;
},
// Update a channel (PM/CEO only)
update: async (channelId: string, updates: ChannelUpdate): Promise<Channel> => {
if (isMockMode()) {
const idx = mockChannels.findIndex((c) => c.id === channelId);
if (idx === -1) throw new Error("Channel not found");
const updated = { ...mockChannels[idx], ...updates } as Channel;
(mockChannels as Channel[])[idx] = updated;
return updated;
}
const { data } = await api.patch<Channel>("/channels/" + channelId, updates);
return data;
},
// Add a member to a channel (PM/CEO only)
addMember: async (channelId: string, agentId: string): Promise<void> => {
if (isMockMode()) {
return;
}
await api.post("/channels/" + channelId + "/add-member", { agent_id: agentId });
},
// Remove a member from a channel (PM/CEO only)
removeMember: async (channelId: string, agentId: string): Promise<void> => {
if (isMockMode()) {
return;
}
await api.delete("/channels/" + channelId + "/remove-member", {
data: { agent_id: agentId },
});
},
};
+112
View File
@@ -0,0 +1,112 @@
import axios, { AxiosInstance, AxiosError } from "axios";
import { API_URL, CEO_AGENT_ID, CEO_ROLE } from "@/lib/constants";
// Create axios instance with default config
const api: AxiosInstance = axios.create({
baseURL: API_URL,
headers: {
"Content-Type": "application/json",
},
timeout: 60000, // Increased to 60s for long operations like reindexing
});
// Request interceptor to add auth headers and logging
api.interceptors.request.use(
(config) => {
// Add agent context headers for API authorization
config.headers["X-Agent-ID"] = CEO_AGENT_ID;
config.headers["X-Agent-Role"] = CEO_ROLE;
// Log request in development
if (process.env.NODE_ENV === "development") {
console.log(`[API] ${config.method?.toUpperCase()} ${config.url}`);
}
return config;
},
(error) => {
console.error("[API] Request setup error:", error);
return Promise.reject(error);
}
);
// Response interceptor for comprehensive error handling
api.interceptors.response.use(
(response) => {
// Log successful responses in development
if (process.env.NODE_ENV === "development") {
console.log(`[API] ✓ ${response.config.method?.toUpperCase()} ${response.config.url}`);
}
return response;
},
(error: AxiosError) => {
// Extract error details
const status = error.response?.status;
const url = error.config?.url;
const method = error.config?.method?.toUpperCase();
const errorData = error.response?.data as Record<string, unknown> | undefined;
const errorDetail = errorData?.detail || error.message;
// Log comprehensive error info
console.error(`[API] ✗ ${method} ${url}`, {
status,
detail: errorDetail,
error: error.message,
});
// Specific error handling with helpful messages
if (error.code === "ECONNABORTED") {
console.error("[API] Request timed out - backend may be overloaded or unavailable");
} else if (error.code === "ERR_NETWORK") {
console.error("[API] Network error - check if backend is running at", API_URL);
} else if (status === 401) {
console.error("[API] Unauthorized - check API authentication headers");
} else if (status === 403) {
console.error("[API] Forbidden - insufficient permissions for this action");
} else if (status === 404) {
console.error("[API] Not found - endpoint may not exist:", url);
} else if (status === 422) {
console.error("[API] Validation error - request data is invalid:", errorDetail);
} else if (status && status >= 500) {
console.error("[API] Server error - backend encountered an internal error");
}
return Promise.reject(error);
}
);
/**
* Helper to extract user-friendly error message from API error
*/
export function getErrorMessage(error: unknown): string {
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError<{ detail?: string }>;
// Check for specific error codes
if (error.code === "ECONNABORTED") {
return "Request timed out. The server may be busy.";
}
if (error.code === "ERR_NETWORK") {
return "Cannot connect to server. Check if the backend is running.";
}
// Check for API error response
if (axiosError.response?.data?.detail) {
return String(axiosError.response.data.detail);
}
// Check for HTTP status
const status = axiosError.response?.status;
if (status === 401) return "Authentication required. Please refresh the page.";
if (status === 403) return "Permission denied for this action.";
if (status === 404) return "The requested resource was not found.";
if (status === 422) return "Invalid request data.";
if (status && status >= 500) return "Server error. Please try again later.";
}
// Generic error
return error instanceof Error ? error.message : "An unexpected error occurred";
}
export { api, API_URL };
export default api;
+568
View File
@@ -0,0 +1,568 @@
import api from "./client";
import { Team, TaskStatus } from "@/types";
import type {
KanbanBoard,
AuditorDashboard,
AuditorFlag,
AuditorReport,
FlagSeverity,
CEOOverview as CEOOverviewType,
Task,
} from "@/types";
import {
isMockMode,
mockDashboardStats,
mockTeamHealth,
getMockRecentActivity,
mockAuditorDashboard,
mockAuditorFlags,
mockAuditorReports,
mockTasks,
mockOrchestratorStatus,
} from "@/lib/mock-data";
// Re-export types from @/types for backward compatibility
export type { CEOOverviewType as CEOOverview };
export interface TeamHealth {
team: Team;
health_score: number;
active_tasks: number;
blocked_tasks: number;
completed_today: number;
}
export interface MetricsSummary {
velocity: VelocityMetric;
blockers: BlockerMetric;
communication: CommunicationMetric;
agents: AgentMetric;
}
export interface VelocityMetric {
tasks_completed_today: number;
tasks_completed_week: number;
average_completion_time_hours: number;
}
export interface BlockerMetric {
total_blocked: number;
blocked_by_team: Record<string, number>;
longest_blocked_hours: number;
}
export interface CommunicationMetric {
messages_today: number;
active_channels: number;
notifications_pending: number;
}
export interface AgentMetric {
total_agents: number;
running: number;
idle: number;
waiting: number;
errors: number;
}
// =============================================================================
// AUDITOR API TYPES
// =============================================================================
export interface CreateFlagRequest {
severity: FlagSeverity;
category: string;
title: string;
description: string;
related_task_id?: string;
related_agent_id?: string;
}
export interface CreateReportRequest {
report_type: string;
title: string;
summary: string;
sections: Array<Record<string, unknown>>;
}
// Helper to create mock kanban board from tasks
function createMockKanbanBoard(tasks: Task[], team?: Team): KanbanBoard {
const filteredTasks = team ? tasks.filter((t) => t.team === team) : tasks;
const pendingTasks = filteredTasks.filter((t) => t.status === TaskStatus.PENDING);
const inProgressTasks = filteredTasks.filter((t) => t.status === TaskStatus.IN_PROGRESS);
const blockedTasks = filteredTasks.filter((t) => t.status === TaskStatus.BLOCKED);
const awaitingQaTasks = filteredTasks.filter((t) => t.status === TaskStatus.AWAITING_QA);
const completedTasks = filteredTasks.filter((t) => t.status === TaskStatus.COMPLETED);
return {
columns: [
{
id: "pending",
title: "Pending",
status: TaskStatus.PENDING,
tasks: pendingTasks,
count: pendingTasks.length,
},
{
id: "in_progress",
title: "In Progress",
status: TaskStatus.IN_PROGRESS,
tasks: inProgressTasks,
count: inProgressTasks.length,
},
{
id: "blocked",
title: "Blocked",
status: TaskStatus.BLOCKED,
tasks: blockedTasks,
count: blockedTasks.length,
},
{
id: "awaiting_qa",
title: "Awaiting QA",
status: TaskStatus.AWAITING_QA,
tasks: awaitingQaTasks,
count: awaitingQaTasks.length,
},
{
id: "completed",
title: "Completed",
status: TaskStatus.COMPLETED,
tasks: completedTasks,
count: completedTasks.length,
},
],
total_tasks: filteredTasks.length,
};
}
export const dashboardApi = {
// Get CEO overview
getCeoOverview: async (): Promise<CEOOverviewType> => {
if (isMockMode()) {
return {
health_status: mockTeamHealth,
key_metrics: {
total_tasks: mockDashboardStats.total_tasks,
tasks_in_progress: mockDashboardStats.tasks_in_progress,
tasks_blocked: mockDashboardStats.tasks_blocked,
tasks_completed_today: mockDashboardStats.tasks_completed_today,
active_agents: mockDashboardStats.active_agents,
},
auditor_alerts: {},
roadmap_progress: {
phase_1: 100,
phase_2: 75,
phase_3: 25,
},
};
}
const { data } = await api.get<CEOOverviewType>("/dashboard/ceo");
return data;
},
// Get metrics - individual endpoints
getVelocityMetrics: async (): Promise<VelocityMetric> => {
if (isMockMode()) {
return {
tasks_completed_today: mockDashboardStats.tasks_completed_today,
tasks_completed_week: 15,
average_completion_time_hours: 4.5,
};
}
const { data } = await api.get<VelocityMetric>("/dashboard/metrics/velocity");
return data;
},
getBlockerMetrics: async (): Promise<BlockerMetric> => {
if (isMockMode()) {
const blockedTasks = mockTasks.filter((t) => t.status === TaskStatus.BLOCKED);
return {
total_blocked: blockedTasks.length,
blocked_by_team: {
backend: blockedTasks.filter((t) => t.team === Team.BACKEND).length,
frontend: blockedTasks.filter((t) => t.team === Team.FRONTEND).length,
ux_ui: blockedTasks.filter((t) => t.team === Team.UX_UI).length,
marketing: blockedTasks.filter((t) => t.team === Team.MARKETING).length,
},
longest_blocked_hours: 24,
};
}
const { data } = await api.get<BlockerMetric>("/dashboard/metrics/blockers");
return data;
},
getCommunicationMetrics: async (): Promise<CommunicationMetric> => {
if (isMockMode()) {
return {
messages_today: 45,
active_channels: 5,
notifications_pending: 3,
};
}
const { data } = await api.get<CommunicationMetric>("/dashboard/metrics/communication");
return data;
},
getHealthMetrics: async () => {
if (isMockMode()) {
return mockTeamHealth;
}
const { data } = await api.get("/dashboard/metrics/health");
return data;
},
// Get kanban board for a team (dev view)
getKanbanDev: async (team: Team): Promise<KanbanBoard> => {
if (isMockMode()) {
return createMockKanbanBoard(mockTasks, team);
}
const { data } = await api.get<KanbanBoard>("/kanban/dev/" + team);
return data;
},
// Get QA kanban board
getKanbanQa: async (team: Team): Promise<KanbanBoard> => {
if (isMockMode()) {
return createMockKanbanBoard(mockTasks, team);
}
const { data } = await api.get<KanbanBoard>("/kanban/qa/" + team);
return data;
},
// Get PM kanban board (cross-team view)
getKanbanPm: async (): Promise<KanbanBoard> => {
if (isMockMode()) {
return createMockKanbanBoard(mockTasks);
}
const { data } = await api.get<KanbanBoard>("/kanban/main-pm");
return data;
},
// Get agent status
getAgentStatus: async () => {
if (isMockMode()) {
return mockOrchestratorStatus;
}
const { data } = await api.get("/dashboard/agents/status");
return data;
},
// Get recent activity
getRecentActivity: async (hours: number = 24, limit: number = 50) => {
if (isMockMode()) {
return getMockRecentActivity().slice(0, limit);
}
const { data } = await api.get<{ period_hours: number; activity: unknown[] }>("/dashboard/activity/recent", {
params: { hours, limit },
});
// Backend returns { period_hours, activity }, extract the activity array
return data.activity ?? [];
},
// =============================================================================
// AUDITOR DASHBOARD ENDPOINTS
// =============================================================================
// Get complete auditor dashboard
getAuditorDashboard: async (): Promise<AuditorDashboard> => {
if (isMockMode()) {
return mockAuditorDashboard as AuditorDashboard;
}
const { data } = await api.get<AuditorDashboard>("/dashboard/auditor");
return data;
},
// Get auditor flags
getAuditorFlags: async (params?: {
severity?: FlagSeverity;
resolved?: boolean;
}): Promise<AuditorFlag[]> => {
if (isMockMode()) {
let flags = [...mockAuditorFlags] as AuditorFlag[];
if (params?.severity) {
flags = flags.filter((f) => f.severity === params.severity);
}
if (params?.resolved !== undefined) {
flags = flags.filter((f) => (params.resolved ? f.resolved_at : !f.resolved_at));
}
return flags;
}
const { data } = await api.get<AuditorFlag[]>("/dashboard/auditor/flags", {
params,
});
return data;
},
// Create an auditor flag
createAuditorFlag: async (
request: CreateFlagRequest
): Promise<AuditorFlag> => {
if (isMockMode()) {
const newFlag: AuditorFlag = {
id: `flag-${Date.now()}`,
severity: request.severity,
category: request.category,
title: request.title,
description: request.description,
related_task_id: request.related_task_id ?? null,
related_agent_id: request.related_agent_id ?? null,
created_at: new Date().toISOString(),
resolved_at: null,
notes: null,
};
(mockAuditorFlags as AuditorFlag[]).push(newFlag);
return newFlag;
}
const { data } = await api.post<AuditorFlag>(
"/dashboard/auditor/flags",
request
);
return data;
},
// Resolve an auditor flag
resolveAuditorFlag: async (
flagId: string,
notes?: string
): Promise<{ status: string; flag_id: string }> => {
if (isMockMode()) {
const flags = mockAuditorFlags as AuditorFlag[];
const idx = flags.findIndex((f) => f.id === flagId);
if (idx !== -1) {
flags[idx] = {
...flags[idx],
resolved_at: new Date().toISOString(),
notes: notes ?? null,
};
return { status: "resolved", flag_id: flagId };
}
throw new Error("Flag not found");
}
const { data } = await api.put(`/dashboard/auditor/flags/${flagId}/resolve`, null, {
params: { notes },
});
return data;
},
// Get auditor reports
getAuditorReports: async (params?: {
report_type?: string;
limit?: number;
}): Promise<AuditorReport[]> => {
if (isMockMode()) {
let reports = [...mockAuditorReports] as AuditorReport[];
if (params?.report_type) {
reports = reports.filter((r) => r.report_type === params.report_type);
}
if (params?.limit) {
reports = reports.slice(0, params.limit);
}
return reports;
}
const { data } = await api.get<AuditorReport[]>(
"/dashboard/auditor/reports",
{ params }
);
return data;
},
// Create an auditor report
createAuditorReport: async (
request: CreateReportRequest
): Promise<AuditorReport> => {
if (isMockMode()) {
const newReport: AuditorReport = {
id: `report-${Date.now()}`,
report_type: request.report_type,
title: request.title,
summary: request.summary,
sections: request.sections,
created_at: new Date().toISOString(),
sent_at: null,
};
(mockAuditorReports as AuditorReport[]).push(newReport);
return newReport;
}
const { data } = await api.post<AuditorReport>(
"/dashboard/auditor/reports",
request
);
return data;
},
// Send a report to CEO
sendAuditorReport: async (
reportId: string
): Promise<{ status: string; report_id: string }> => {
if (isMockMode()) {
const reports = mockAuditorReports as AuditorReport[];
const idx = reports.findIndex((r) => r.id === reportId);
if (idx !== -1) {
reports[idx] = {
...reports[idx],
sent_at: new Date().toISOString(),
};
return { status: "sent", report_id: reportId };
}
throw new Error("Report not found");
}
const { data } = await api.post(
`/dashboard/auditor/reports/${reportId}/send`
);
return data;
},
// =============================================================================
// CEO DETAIL ENDPOINTS
// =============================================================================
// Get detailed team metrics
getCeoTeamDetails: async () => {
if (isMockMode()) {
return mockTeamHealth.map((th) => ({
...th,
velocity_7d: 10,
avg_completion_time: 4.5,
agent_count: 5,
}));
}
const { data } = await api.get("/dashboard/ceo/teams");
return data;
},
// Get blocker details for CEO
getCeoBlockerDetails: async () => {
if (isMockMode()) {
const blockedTasks = mockTasks.filter((t) => t.status === TaskStatus.BLOCKED);
return {
total_blocked: blockedTasks.length,
blockers: blockedTasks.map((t) => ({
task_id: t.id,
title: t.title,
team: t.team,
blocked_hours: 24,
blocker_reason: "Dependency not resolved",
})),
};
}
const { data } = await api.get("/dashboard/ceo/blockers");
return data;
},
// Get velocity metrics for CEO
getCeoVelocity: async (days: number = 7) => {
if (isMockMode()) {
return {
period_days: days,
total_completed: 15,
by_team: {
backend: 6,
frontend: 5,
ux_ui: 4,
marketing: 0,
},
daily_breakdown: Array.from({ length: days }, (_, i) => ({
date: new Date(Date.now() - i * 24 * 60 * 60 * 1000).toISOString().split("T")[0],
completed: Math.floor(Math.random() * 5) + 1,
})),
};
}
const { data } = await api.get("/dashboard/ceo/velocity", {
params: { days },
});
return data;
},
// =============================================================================
// KANBAN ENDPOINTS (Role-specific views)
// =============================================================================
// Get documenter kanban board
getKanbanDocumenter: async (team: Team): Promise<KanbanBoard> => {
if (isMockMode()) {
return createMockKanbanBoard(mockTasks, team);
}
const { data } = await api.get<KanbanBoard>(`/kanban/documenter/${team}`);
return data;
},
// Get cell PM kanban board
getKanbanCellPm: async (team: Team): Promise<KanbanBoard> => {
if (isMockMode()) {
return createMockKanbanBoard(mockTasks, team);
}
const { data } = await api.get<KanbanBoard>(`/kanban/pm/${team}`);
return data;
},
// Get board-level roadmap kanban
getKanbanBoard: async (): Promise<KanbanBoard> => {
if (isMockMode()) {
return createMockKanbanBoard(mockTasks);
}
const { data } = await api.get<KanbanBoard>("/kanban/board");
return data;
},
// Get kanban statistics
getKanbanStats: async (team?: Team) => {
if (isMockMode()) {
const tasks = team ? mockTasks.filter((t) => t.team === team) : mockTasks;
return {
total: tasks.length,
by_status: {
pending: tasks.filter((t) => t.status === TaskStatus.PENDING).length,
in_progress: tasks.filter((t) => t.status === TaskStatus.IN_PROGRESS).length,
blocked: tasks.filter((t) => t.status === TaskStatus.BLOCKED).length,
awaiting_qa: tasks.filter((t) => t.status === TaskStatus.AWAITING_QA).length,
completed: tasks.filter((t) => t.status === TaskStatus.COMPLETED).length,
},
};
}
const { data } = await api.get("/kanban/stats", { params: { team } });
return data;
},
// Get metrics for a specific agent
getAgentMetrics: async (agentId: string) => {
if (isMockMode()) {
return {
agent_id: agentId,
tasks_completed: 10,
tasks_in_progress: 2,
avg_completion_time_hours: 8.5,
quality_score: 0.92,
};
}
const { data } = await api.get(`/dashboard/metrics/agent/${agentId}`);
return data;
},
// Get metrics for a specific team
getTeamMetrics: async (team: Team) => {
if (isMockMode()) {
return {
team,
tasks_total: 25,
tasks_completed: 15,
tasks_in_progress: 5,
tasks_blocked: 2,
velocity: 8.5,
quality_score: 0.88,
};
}
const { data } = await api.get(`/dashboard/metrics/team/${team}`);
return data;
},
// Get kanban board for any team (generic endpoint)
getKanbanForTeam: async (team: Team): Promise<KanbanBoard> => {
if (isMockMode()) {
const teamTasks = mockTasks.filter((t) => t.team === team);
return createMockKanbanBoard(teamTasks);
}
const { data } = await api.get<KanbanBoard>(`/dashboard/kanban/${team}`);
return data;
},
};
+242
View File
@@ -0,0 +1,242 @@
/**
* Git API Client
*
* API functions for Git operations (branches, commits, PRs).
*/
import api from "./client";
import { isMockMode } from "@/lib/mock-data";
import type {
GitStatusResponse,
GitLogResponse,
GitBranchListResponse,
GitDiffResponse,
GitCommitRequest,
GitCommitResponse,
GitPushRequest,
GitPushResponse,
GitCreateBranchRequest,
GitCreateBranchResponse,
GitCheckoutRequest,
GitCheckoutResponse,
GitCreatePRRequest,
GitCreatePRResponse,
GitMergePRRequest,
GitMergePRResponse,
} from "@/types/git";
// =============================================================================
// Mock Data
// =============================================================================
const mockGitStatus: GitStatusResponse = {
project_slug: "roboco",
current_branch: "feature/backend/abc12345",
has_changes: true,
staged_files: ["src/api/routes/tasks.py"],
unstaged_files: ["src/services/task_service.py"],
untracked_files: [],
ahead: 2,
behind: 0,
};
const mockGitLog: GitLogResponse = {
project_slug: "roboco",
branch: "feature/backend/abc12345",
commits: [
{
hash: "abc123456789abcdef",
short_hash: "abc1234",
message: "[abc12345] Add task status endpoint",
author: "backend-dev",
date: new Date().toISOString(),
},
],
};
const mockBranches: GitBranchListResponse = {
project_slug: "roboco",
current_branch: "feature/backend/abc12345",
branches: [
{ name: "main", is_current: false, is_remote: false, last_commit: "def456" },
{ name: "feature/backend/abc12345", is_current: true, is_remote: false, last_commit: "abc123" },
],
};
// =============================================================================
// API Client
// =============================================================================
export const gitApi = {
// ===========================================================================
// READ-ONLY OPERATIONS
// ===========================================================================
/**
* Get git status for a project
*/
getStatus: async (projectSlug: string, taskId?: string): Promise<GitStatusResponse> => {
if (isMockMode()) {
return { ...mockGitStatus, project_slug: projectSlug };
}
const { data } = await api.get<GitStatusResponse>("/git/status", {
params: { project_slug: projectSlug, _task_id: taskId },
});
return data;
},
/**
* Get git log for a project
*/
getLog: async (
projectSlug: string,
limit: number = 10,
branch?: string
): Promise<GitLogResponse> => {
if (isMockMode()) {
return { ...mockGitLog, project_slug: projectSlug };
}
const { data } = await api.get<GitLogResponse>("/git/log", {
params: { project_slug: projectSlug, limit, branch },
});
return data;
},
/**
* Get git branches for a project
*/
getBranches: async (
projectSlug: string,
includeRemote: boolean = false
): Promise<GitBranchListResponse> => {
if (isMockMode()) {
return { ...mockBranches, project_slug: projectSlug };
}
const { data } = await api.get<GitBranchListResponse>("/git/branches", {
params: { project_slug: projectSlug, include_remote: includeRemote },
});
return data;
},
/**
* Get git diff for a project
*/
getDiff: async (
projectSlug: string,
staged: boolean = false,
filePath?: string
): Promise<GitDiffResponse> => {
if (isMockMode()) {
return {
project_slug: projectSlug,
staged,
file_path: filePath || null,
diff: "- old line\n+ new line",
files_changed: 1,
};
}
const { data } = await api.get<GitDiffResponse>("/git/diff", {
params: { project_slug: projectSlug, staged, file_path: filePath },
});
return data;
},
// ===========================================================================
// WRITE OPERATIONS
// ===========================================================================
/**
* Create a git commit
*/
commit: async (request: GitCommitRequest): Promise<GitCommitResponse> => {
if (isMockMode()) {
return {
commit_hash: "abc123456789abcdef",
message: `[${request.task_id.slice(0, 8)}] ${request.message}`,
files_changed: request.files?.length || 1,
insertions: 10,
deletions: 5,
};
}
const { data } = await api.post<GitCommitResponse>("/git/commit", request);
return data;
},
/**
* Push commits to remote
*/
push: async (request: GitPushRequest): Promise<GitPushResponse> => {
if (isMockMode()) {
return {
branch: "feature/backend/abc12345",
commits_pushed: 2,
remote: "origin",
ready_for_pr: true,
};
}
const { data } = await api.post<GitPushResponse>("/git/push", request);
return data;
},
/**
* Create a task branch (PM only)
*/
createBranch: async (request: GitCreateBranchRequest): Promise<GitCreateBranchResponse> => {
if (isMockMode()) {
return {
branch_name: `${request.branch_type}/backend/${request.task_id.slice(0, 8)}`,
created_from: request.parent_branch || "main",
project_slug: request.project_slug,
};
}
const { data } = await api.post<GitCreateBranchResponse>("/git/branch/create", request);
return data;
},
/**
* Checkout a branch
*/
checkout: async (request: GitCheckoutRequest): Promise<GitCheckoutResponse> => {
if (isMockMode()) {
return {
branch: request.branch,
project_slug: request.project_slug,
};
}
const { data } = await api.post<GitCheckoutResponse>("/git/checkout", request);
return data;
},
/**
* Create a pull request
*/
createPR: async (request: GitCreatePRRequest): Promise<GitCreatePRResponse> => {
if (isMockMode()) {
return {
pr_number: 42,
pr_url: "https://github.com/org/repo/pull/42",
title: request.title,
source_branch: "feature/backend/abc12345",
target_branch: "main",
};
}
const { data } = await api.post<GitCreatePRResponse>("/git/pr/create", request);
return data;
},
/**
* Merge a pull request (PM only)
*/
mergePR: async (request: GitMergePRRequest): Promise<GitMergePRResponse> => {
if (isMockMode()) {
return {
pr_number: request.pr_number,
merged: true,
merge_commit: "def456789abcdef",
target_branch: "main",
};
}
const { data } = await api.post<GitMergePRResponse>("/git/pr/merge", request);
return data;
},
};
+61
View File
@@ -0,0 +1,61 @@
/**
* Groups API Client
*
* API functions for group management within channels.
*/
import api from "./client";
import { isMockMode } from "@/lib/mock-data";
import type { Group } from "@/types";
// =============================================================================
// Types
// =============================================================================
export interface GroupCreate {
channel_id: string;
name: string;
hierarchy_level?: number;
}
// =============================================================================
// API Client
// =============================================================================
export const groupsApi = {
/**
* Create a new group within a channel
*/
create: async (group: GroupCreate): Promise<Group> => {
if (isMockMode()) {
return {
id: `group-${Date.now()}`,
name: group.name,
hierarchy_level: group.hierarchy_level ?? 0,
is_active: true,
total_messages: 0,
active_session_id: null,
};
}
const { data } = await api.post<Group>("/groups", group);
return data;
},
/**
* Get a group by ID
*/
get: async (groupId: string): Promise<Group> => {
if (isMockMode()) {
return {
id: groupId,
name: "Mock Group",
hierarchy_level: 0,
is_active: true,
total_messages: 0,
active_session_id: null,
};
}
const { data } = await api.get<Group>(`/groups/${groupId}`);
return data;
},
};
+13
View File
@@ -0,0 +1,13 @@
export { api, API_URL } from "./client";
export { tasksApi } from "./tasks";
export { orchestratorApi } from "./orchestrator";
export { channelsApi } from "./channels";
export { notificationsApi } from "./notifications";
export { dashboardApi } from "./dashboard";
export { knowledgeBaseApi } from "./knowledge-base";
export { projectsApi } from "./projects";
export { workSessionsApi } from "./work-sessions";
export { gitApi } from "./git";
export { a2aApi } from "./a2a";
export { streamApi } from "./stream";
export { groupsApi } from "./groups";
+426
View File
@@ -0,0 +1,426 @@
/**
* Journals API Client
*
* Agent personal journals for reflection, growth tracking, and debugging.
* Matches backend: roboco/api/routes/journals.py
*/
import api from "./client";
import {
Journal,
JournalEntry,
JournalEntryCreate,
JournalEntryType,
JournalStats,
GrowthMetrics,
} from "@/types";
import {
isMockMode,
mockJournals,
mockJournalEntries,
} from "@/lib/mock-data";
// =============================================================================
// JOURNAL ENDPOINTS
// =============================================================================
/**
* Get the current agent's journal (CEO's journal in this case)
*/
async function getMyJournal(): Promise<Journal> {
if (isMockMode()) {
return mockJournals[0] as Journal;
}
const response = await api.get<Journal>("/journals/me");
return response.data;
}
/**
* Get a journal by agent ID or slug (e.g., "be-dev-1")
*/
async function getJournalByAgent(agentIdOrSlug: string): Promise<Journal> {
if (isMockMode()) {
const journal = mockJournals.find((j) => j.agent_id === agentIdOrSlug);
if (journal) return journal as Journal;
// Return first journal as fallback
return mockJournals[0] as Journal;
}
const response = await api.get<Journal>(`/journals/${agentIdOrSlug}`);
return response.data;
}
/**
* List entries for a specific agent by ID or slug
*/
async function listAgentEntries(
agentIdOrSlug: string,
params?: {
entry_type?: JournalEntryType;
task_id?: string;
limit?: number;
offset?: number;
}
): Promise<JournalEntry[]> {
if (isMockMode()) {
let entries = [...mockJournalEntries] as JournalEntry[];
if (params?.entry_type) {
entries = entries.filter((e) => e.type === params.entry_type);
}
if (params?.task_id) {
entries = entries.filter((e) => e.task_id === params.task_id);
}
const offset = params?.offset ?? 0;
const limit = params?.limit ?? 50;
return entries.slice(offset, offset + limit);
}
const response = await api.get<JournalEntry[]>(
`/journals/${agentIdOrSlug}/entries`,
{ params }
);
return response.data;
}
// =============================================================================
// ENTRY CRUD ENDPOINTS
// =============================================================================
/**
* Create a new journal entry
*/
async function createEntry(data: JournalEntryCreate): Promise<JournalEntry> {
if (isMockMode()) {
const newEntry: JournalEntry = {
id: `entry-${Date.now()}`,
journal_id: mockJournals[0].id,
type: data.type,
title: data.title,
content: data.content,
task_id: data.task_id ?? null,
session_id: data.session_id ?? null,
timestamp: new Date().toISOString(),
tags: data.tags ?? [],
sentiment: data.sentiment ?? null,
is_private: data.is_private ?? false,
created_at: new Date().toISOString(),
updated_at: null,
};
(mockJournalEntries as JournalEntry[]).push(newEntry);
return newEntry;
}
const response = await api.post<JournalEntry>("/journals/me/entries", data);
return response.data;
}
/**
* List current agent's journal entries
*/
async function listMyEntries(params?: {
entry_type?: JournalEntryType;
task_id?: string;
limit?: number;
offset?: number;
}): Promise<JournalEntry[]> {
if (isMockMode()) {
let entries = [...mockJournalEntries] as JournalEntry[];
if (params?.entry_type) {
entries = entries.filter((e) => e.type === params.entry_type);
}
if (params?.task_id) {
entries = entries.filter((e) => e.task_id === params.task_id);
}
const offset = params?.offset ?? 0;
const limit = params?.limit ?? 50;
return entries.slice(offset, offset + limit);
}
const response = await api.get<JournalEntry[]>("/journals/me/entries", {
params,
});
return response.data;
}
/**
* Get a specific journal entry by ID
*/
async function getEntry(entryId: string): Promise<JournalEntry> {
if (isMockMode()) {
const entry = mockJournalEntries.find((e) => e.id === entryId);
if (entry) return entry as JournalEntry;
throw new Error("Entry not found");
}
const response = await api.get<JournalEntry>(`/journals/entries/${entryId}`);
return response.data;
}
/**
* Delete a journal entry
*/
async function deleteEntry(entryId: string): Promise<void> {
if (isMockMode()) {
const idx = mockJournalEntries.findIndex((e) => e.id === entryId);
if (idx !== -1) mockJournalEntries.splice(idx, 1);
return;
}
await api.delete(`/journals/entries/${entryId}`);
}
// =============================================================================
// CONVENIENCE ENTRY ENDPOINTS
// =============================================================================
/**
* Add a task reflection entry
*/
async function addTaskReflection(data: {
task_id: string;
title: string;
what_done: string;
what_learned: string;
what_struggled?: string;
next_steps?: string[];
tags?: string[];
}): Promise<JournalEntry> {
if (isMockMode()) {
return createEntry({
type: JournalEntryType.TASK_REFLECTION,
title: data.title,
content: JSON.stringify({
what_done: data.what_done,
what_learned: data.what_learned,
what_struggled: data.what_struggled,
next_steps: data.next_steps,
}),
task_id: data.task_id,
tags: data.tags,
});
}
const response = await api.post<JournalEntry>(
"/journals/me/reflections",
data
);
return response.data;
}
/**
* Add a decision log entry
*/
async function addDecisionLog(data: {
title: string;
context: string;
options: string[];
chosen: string;
rationale: string;
consequences?: string;
task_id?: string;
tags?: string[];
}): Promise<JournalEntry> {
if (isMockMode()) {
return createEntry({
type: JournalEntryType.DECISION_LOG,
title: data.title,
content: JSON.stringify({
context: data.context,
options: data.options,
chosen: data.chosen,
rationale: data.rationale,
consequences: data.consequences,
}),
task_id: data.task_id,
tags: data.tags,
});
}
const response = await api.post<JournalEntry>("/journals/me/decisions", data);
return response.data;
}
/**
* Add a learning entry
*/
async function addLearning(data: {
title: string;
what_learned: string;
how_applied?: string;
source?: string;
task_id?: string;
tags?: string[];
}): Promise<JournalEntry> {
if (isMockMode()) {
return createEntry({
type: JournalEntryType.LEARNING,
title: data.title,
content: JSON.stringify({
what_learned: data.what_learned,
how_applied: data.how_applied,
source: data.source,
}),
task_id: data.task_id,
tags: data.tags,
});
}
const response = await api.post<JournalEntry>("/journals/me/learnings", data);
return response.data;
}
/**
* Add a struggle entry
*/
async function addStruggle(data: {
title: string;
what_struggled: string;
attempted_solutions: string[];
resolution?: string;
help_needed?: string;
task_id?: string;
tags?: string[];
}): Promise<JournalEntry> {
if (isMockMode()) {
return createEntry({
type: JournalEntryType.STRUGGLE,
title: data.title,
content: JSON.stringify({
what_struggled: data.what_struggled,
attempted_solutions: data.attempted_solutions,
resolution: data.resolution,
help_needed: data.help_needed,
}),
task_id: data.task_id,
tags: data.tags,
});
}
const response = await api.post<JournalEntry>("/journals/me/struggles", data);
return response.data;
}
/**
* Add a general journal entry
*/
async function addNote(data: {
title: string;
content: string;
task_id?: string;
session_id?: string;
tags?: string[];
is_private?: boolean;
}): Promise<JournalEntry> {
if (isMockMode()) {
return createEntry({
type: JournalEntryType.GENERAL,
title: data.title,
content: data.content,
task_id: data.task_id,
session_id: data.session_id,
tags: data.tags,
is_private: data.is_private,
});
}
const response = await api.post<JournalEntry>("/journals/me/notes", data);
return response.data;
}
// =============================================================================
// ANALYTICS ENDPOINTS
// =============================================================================
/**
* Get statistics for the current agent's journal
*/
async function getMyStats(): Promise<JournalStats> {
if (isMockMode()) {
const entries = mockJournalEntries as JournalEntry[];
return {
total_entries: entries.length,
entries_by_type: {
task_reflection: entries.filter((e) => e.type === JournalEntryType.TASK_REFLECTION).length,
decision_log: entries.filter((e) => e.type === JournalEntryType.DECISION_LOG).length,
learning: entries.filter((e) => e.type === JournalEntryType.LEARNING).length,
struggle: entries.filter((e) => e.type === JournalEntryType.STRUGGLE).length,
general: entries.filter((e) => e.type === JournalEntryType.GENERAL).length,
},
last_entry_at: entries[0]?.created_at ?? null,
has_summary: false,
};
}
const response = await api.get<JournalStats>("/journals/me/stats");
return response.data;
}
/**
* Get growth metrics for the current agent
*/
async function getMyGrowthMetrics(): Promise<GrowthMetrics> {
if (isMockMode()) {
const entries = mockJournalEntries as JournalEntry[];
return {
total_reflections: entries.filter((e) => e.type === JournalEntryType.TASK_REFLECTION).length,
total_learnings: entries.filter((e) => e.type === JournalEntryType.LEARNING).length,
total_struggles: entries.filter((e) => e.type === JournalEntryType.STRUGGLE).length,
total_decisions: entries.filter((e) => e.type === JournalEntryType.DECISION_LOG).length,
struggle_resolution_rate: 0.7,
learning_frequency: 3.5,
sentiment_trend: "improving",
};
}
const response = await api.get<GrowthMetrics>("/journals/me/growth");
return response.data;
}
// =============================================================================
// SEARCH ENDPOINTS
// =============================================================================
/**
* Semantic search through journal entries
*/
async function searchEntries(
query: string,
topK: number = 10
): Promise<JournalEntry[]> {
if (isMockMode()) {
// Simple text search for mock mode
const queryLower = query.toLowerCase();
return (mockJournalEntries as JournalEntry[])
.filter(
(e) =>
e.title.toLowerCase().includes(queryLower) ||
e.content.toLowerCase().includes(queryLower)
)
.slice(0, topK);
}
const response = await api.post<JournalEntry[]>("/journals/me/search", {
query,
top_k: topK,
});
return response.data;
}
// =============================================================================
// EXPORT API
// =============================================================================
export const journalsApi = {
// Journal
getMyJournal,
getJournalByAgent,
// Entry CRUD
createEntry,
listMyEntries,
listAgentEntries,
getEntry,
deleteEntry,
// Convenience entry creators
addTaskReflection,
addDecisionLog,
addLearning,
addStruggle,
addNote,
// Analytics
getMyStats,
getMyGrowthMetrics,
// Search
searchEntries,
};
export default journalsApi;
+653
View File
@@ -0,0 +1,653 @@
/**
* Knowledge Base API Client
*
* Semantic search and RAG queries against indexed knowledge.
* Matches backend: roboco/api/routes/optimal.py
*/
import api from "./client";
import {
KBIndexType,
type KBSearchRequest,
type KBSearchResponse,
type KBSearchResult,
type RAGQueryRequest,
type RAGQueryResponse,
type KBStats,
type KBIndexStats,
type RAGHealthResponse,
type MentorAskRequest,
type MentorAskResponse,
type ErrorSearchRequest,
type ErrorSearchResponse,
type ErrorRecordRequest,
type ErrorRecordResponse,
type DecisionCheckRequest,
type DecisionCheckResponse,
type DecisionRecordRequest,
type DecisionRecordResponse,
type StandardsGetRequest,
type StandardsGetResponse,
type ValidateActionRequest,
type ValidateActionResponse,
type CodeReviewRequest,
type CodeReviewResponse,
type LearningRecordRequest,
type LearningRecordResponse,
type LearningSearchRequest,
type ProactiveContextRequest,
type ProactiveContextResponse,
type TokenEstimateRequest,
type TokenEstimateResponse,
type RefreshIndexRequest,
type RefreshIndexResponse,
type ClearIndexResponse,
type ReindexResponse,
type ReindexRequest,
type IndexStalenessResponse,
} from "@/types";
import { isMockMode } from "@/lib/mock-data";
// =============================================================================
// MOCK DATA
// =============================================================================
const mockSearchResults: KBSearchResult[] = [
{
content: "The TaskService handles all task lifecycle operations including creation, assignment, status transitions, and completion tracking...",
source: "roboco/services/tasks.py",
score: 0.92,
index_type: KBIndexType.CODE,
metadata: { language: "python", lines: "45-120" },
},
{
content: "## Task Lifecycle\n\nTasks follow a strict state machine from PENDING through IN_PROGRESS to COMPLETED. Each transition requires specific conditions...",
source: "docs/architecture/task-lifecycle.md",
score: 0.88,
index_type: KBIndexType.DOCUMENTATION,
metadata: { section: "Architecture" },
},
{
content: "BE-DEV-1: I've completed the task validation logic. The acceptance criteria now require at least one item before a task can be created.",
source: "channel:backend/session:abc123",
score: 0.75,
index_type: KBIndexType.CONVERSATIONS,
metadata: { agent: "be-dev-1", timestamp: "2024-01-15T10:30:00Z" },
},
{
content: "Learned that proper error boundaries in the task form prevent cascading failures. Applied this pattern to all form components.",
source: "journal:be-dev-1/entry:xyz789",
score: 0.71,
index_type: KBIndexType.JOURNALS,
metadata: { agent: "be-dev-1", type: "learning" },
},
];
const mockStats: KBStats = {
indexes: [
{ index_type: KBIndexType.CODE, document_count: 1250, chunk_count: 8500, last_updated: "2024-01-15T12:00:00Z" },
{ index_type: KBIndexType.DOCUMENTATION, document_count: 45, chunk_count: 320, last_updated: "2024-01-15T11:30:00Z" },
{ index_type: KBIndexType.CONVERSATIONS, document_count: 890, chunk_count: 4200, last_updated: "2024-01-15T12:15:00Z" },
{ index_type: KBIndexType.JOURNALS, document_count: 156, chunk_count: 780, last_updated: "2024-01-15T10:00:00Z" },
{ index_type: KBIndexType.ERRORS, document_count: 45, chunk_count: 180, last_updated: "2024-01-15T10:00:00Z" },
{ index_type: KBIndexType.STANDARDS, document_count: 12, chunk_count: 60, last_updated: "2024-01-15T10:00:00Z" },
{ index_type: KBIndexType.DECISIONS, document_count: 78, chunk_count: 390, last_updated: "2024-01-15T10:00:00Z" },
{ index_type: KBIndexType.REVIEWS, document_count: 234, chunk_count: 1170, last_updated: "2024-01-15T10:00:00Z" },
{ index_type: KBIndexType.LEARNINGS, document_count: 89, chunk_count: 445, last_updated: "2024-01-15T10:00:00Z" },
],
total_documents: 2799,
total_chunks: 15245,
};
// Backend returns stats as dict, we need to transform to array
interface BackendIndexStats {
initialized: boolean;
indexes: Record<string, { document_count: number; chunk_count: number; last_updated: string | null }>;
}
function transformStatsResponse(backendStats: BackendIndexStats): KBStats {
const indexes: KBIndexStats[] = Object.entries(backendStats.indexes || {}).map(([indexType, stats]) => ({
index_type: indexType as KBIndexType,
document_count: stats.document_count ?? 0,
chunk_count: stats.chunk_count ?? 0,
last_updated: stats.last_updated ?? null,
}));
const total_documents = indexes.reduce((sum, idx) => sum + idx.document_count, 0);
const total_chunks = indexes.reduce((sum, idx) => sum + idx.chunk_count, 0);
return { indexes, total_documents, total_chunks };
}
// =============================================================================
// SEARCH ENDPOINTS
// =============================================================================
/**
* Semantic search across indexed content
*/
async function search(params: KBSearchRequest): Promise<KBSearchResponse> {
if (isMockMode()) {
// Filter by index types if specified
let results = [...mockSearchResults];
if (params.index_types && params.index_types.length > 0) {
results = results.filter((r) => params.index_types!.includes(r.index_type));
}
// Filter by min score
if (params.min_score) {
results = results.filter((r) => r.score >= params.min_score!);
}
// Limit results
const topK = params.top_k ?? 10;
results = results.slice(0, topK);
return {
results,
total: results.length,
query: params.query,
};
}
const response = await api.post<KBSearchResponse>("/optimal/kb/search", params);
return response.data;
}
// =============================================================================
// RAG ENDPOINTS
// =============================================================================
/**
* RAG query - ask a question, get AI answer with citations
*/
async function ragQuery(params: RAGQueryRequest): Promise<RAGQueryResponse> {
if (isMockMode()) {
// Simulate RAG response with mock data
return {
answer: `Based on the indexed knowledge, here's what I found about "${params.question}":\n\nThe system uses a structured approach where tasks follow a defined lifecycle. Each task starts in PENDING state and can transition through various states like IN_PROGRESS, BLOCKED, and eventually COMPLETED or CANCELLED.\n\nKey points:\n- Tasks require acceptance criteria before creation\n- State transitions are validated by the TaskService\n- Agents can claim and work on tasks based on their role and team`,
citations: mockSearchResults.slice(0, 3).map((r) => ({
content: r.content,
source: r.source,
score: r.score,
index_type: r.index_type,
metadata: r.metadata,
})),
query: params.question,
context_used: 3,
};
}
// Transform frontend params to backend format
const backendParams = {
query: params.question,
index_types: params.index_types,
top_k: params.max_context_chunks ?? 5,
};
const response = await api.post<RAGQueryResponse>("/optimal/rag/query", backendParams);
return response.data;
}
/**
* Get context for a query without generating an answer
*/
async function getContext(params: RAGQueryRequest): Promise<KBSearchResult[]> {
if (isMockMode()) {
return mockSearchResults.slice(0, params.max_context_chunks ?? 5);
}
// Transform frontend params to backend format
const backendParams = {
query: params.question,
index_types: params.index_types,
top_k: params.max_context_chunks ?? 5,
};
const response = await api.post<{ context: KBSearchResult[] }>("/optimal/rag/context", backendParams);
return response.data.context;
}
// =============================================================================
// STATS ENDPOINTS
// =============================================================================
/**
* Get index statistics
*/
async function getStats(): Promise<KBStats> {
if (isMockMode()) {
return mockStats;
}
const response = await api.get<BackendIndexStats>("/optimal/stats");
return transformStatsResponse(response.data);
}
/**
* Get stats for a specific index type
*/
async function getIndexStats(indexType: KBIndexType): Promise<KBIndexStats> {
if (isMockMode()) {
const stats = mockStats.indexes.find((i) => i.index_type === indexType);
if (stats) return stats;
return {
index_type: indexType,
document_count: 0,
chunk_count: 0,
last_updated: null,
};
}
const response = await api.get<KBIndexStats>(`/optimal/stats/${indexType}`);
return response.data;
}
// =============================================================================
// BROWSE ENDPOINTS
// =============================================================================
/**
* List documents in a specific index (for browsing)
*/
async function listDocuments(
indexType: KBIndexType,
params?: { limit?: number; offset?: number }
): Promise<{ documents: Array<{ id: string; source: string; indexed_at: string; metadata?: Record<string, unknown> }>; total: number }> {
if (isMockMode()) {
// Generate mock document list
const docs = mockSearchResults
.filter((r) => r.index_type === indexType)
.map((r, idx) => ({
id: `doc-${indexType}-${idx}`,
source: r.source,
indexed_at: "2024-01-15T12:00:00Z",
}));
return { documents: docs, total: docs.length };
}
const response = await api.get<{ documents: Array<{ id: string; source: string; indexed_at: string; metadata?: Record<string, unknown> }>; total: number; index_type: string }>(
`/optimal/kb/${indexType}/documents`,
{ params }
);
return { documents: response.data.documents, total: response.data.total };
}
// =============================================================================
// HEALTH ENDPOINT
// =============================================================================
/**
* Get RAG system health status
*/
async function getHealth(): Promise<RAGHealthResponse> {
if (isMockMode()) {
return {
healthy: true,
embedding_status: "healthy",
llm_status: "healthy",
vector_store_status: "healthy",
details: {},
};
}
const response = await api.get<RAGHealthResponse>("/optimal/health");
return response.data;
}
// =============================================================================
// INDEX MANAGEMENT
// =============================================================================
/**
* Delete/clear an index
*/
async function deleteIndex(indexType: KBIndexType): Promise<ClearIndexResponse> {
if (isMockMode()) {
return { status: "cleared", index_type: indexType };
}
const response = await api.delete<ClearIndexResponse>(`/optimal/kb/${indexType}`);
return response.data;
}
/**
* Refresh an index with updated sources
*/
async function refreshIndex(request: RefreshIndexRequest): Promise<RefreshIndexResponse> {
if (isMockMode()) {
return { status: "refreshed", index_type: request.index_type, sources: request.sources };
}
const response = await api.post<RefreshIndexResponse>("/optimal/kb/refresh", request);
return response.data;
}
/**
* Trigger full reindex with detailed reporting
*
* @param request - Optional parameters for reindexing
* @param request.force - Force reindex even if indexes have content
* @param request.timeout_seconds - Max time to wait (default: 300s)
* @returns Detailed report of what was indexed, failed, and skipped
*/
async function reindexAll(request?: ReindexRequest): Promise<ReindexResponse> {
if (isMockMode()) {
return {
status: "reindexed",
code: {
index_type: "code",
total_attempted: 100,
successful: 98,
failed: 2,
skipped: 0,
success_rate: 98.0,
has_failures: true,
failed_sources: [
["/path/to/file1.py", "UTF-8 decode error"],
["/path/to/file2.py", "File too large"],
],
duration_seconds: 45.2,
},
documentation: {
index_type: "documentation",
total_attempted: 50,
successful: 50,
failed: 0,
skipped: 0,
success_rate: 100.0,
has_failures: false,
failed_sources: [],
duration_seconds: 12.5,
},
overall_success: true,
warnings: ["Code indexing had 2 failures"],
// Legacy fields
code_count: 98,
docs_count: 50,
};
}
const response = await api.post<ReindexResponse>("/optimal/kb/reindex", null, {
params: {
force: request?.force ?? false,
timeout_seconds: request?.timeout_seconds ?? 300,
},
});
return response.data;
}
/**
* Check if indexes are stale (source files modified after last indexing)
*
* @returns Staleness info for each index type
*/
async function checkStaleness(): Promise<IndexStalenessResponse> {
if (isMockMode()) {
return {
needs_reindex: true,
stale_indexes: ["code"],
details: {
code: {
status: "stale",
last_indexed: new Date(Date.now() - 86400000).toISOString(),
stale_file_count: 5,
stale_files_sample: [
"/roboco/services/task.py",
"/roboco/services/optimal.py",
],
recommendation: "Run /kb/reindex?force=true to update",
},
documentation: {
status: "current",
last_indexed: new Date().toISOString(),
indexed_sources_count: 50,
},
},
};
}
const response = await api.get<IndexStalenessResponse>(
"/optimal/stats/staleness"
);
return response.data;
}
// =============================================================================
// MENTOR ENDPOINTS
// =============================================================================
/**
* Ask the mentor for help
*/
async function askMentor(request: MentorAskRequest): Promise<MentorAskResponse> {
if (isMockMode()) {
return {
answer: `Here's what I found about "${request.question}":\n\nBased on our organizational knowledge, I recommend following the established patterns and consulting the relevant documentation.`,
sources: mockSearchResults.slice(0, 2),
conversation_id: "conv-" + Date.now(),
suggested_followups: ["Can you elaborate?", "What are the alternatives?"],
};
}
const response = await api.post<MentorAskResponse>("/optimal/mentor/ask", request);
return response.data;
}
// =============================================================================
// ERROR ENDPOINTS
// =============================================================================
/**
* Search for known error solutions
*/
async function searchErrors(request: ErrorSearchRequest): Promise<ErrorSearchResponse> {
if (isMockMode()) {
return { results: [], total: 0 };
}
const response = await api.post<ErrorSearchResponse>("/optimal/errors/search", request);
return response.data;
}
/**
* Record an error solution
*/
async function recordError(request: ErrorRecordRequest): Promise<ErrorRecordResponse> {
if (isMockMode()) {
return { error_id: "err-" + Date.now(), status: "recorded" };
}
const response = await api.post<ErrorRecordResponse>("/optimal/errors/record", request);
return response.data;
}
// =============================================================================
// DECISION ENDPOINTS
// =============================================================================
/**
* Check if a similar decision was made before
*/
async function checkDecision(request: DecisionCheckRequest): Promise<DecisionCheckResponse> {
if (isMockMode()) {
return { has_precedent: false, decisions: [], recommendation: "No similar decisions found" };
}
const response = await api.post<DecisionCheckResponse>("/optimal/decisions/check", request);
return response.data;
}
/**
* Record a decision for future reference
*/
async function recordDecision(request: DecisionRecordRequest): Promise<DecisionRecordResponse> {
if (isMockMode()) {
return { decision_id: "dec-" + Date.now(), status: "recorded" };
}
const response = await api.post<DecisionRecordResponse>("/optimal/decisions/record", request);
return response.data;
}
// =============================================================================
// STANDARDS ENDPOINTS
// =============================================================================
/**
* Get standards for a domain
*/
async function getStandards(request: StandardsGetRequest): Promise<StandardsGetResponse> {
if (isMockMode()) {
return { standards: [], total: 0 };
}
const response = await api.post<StandardsGetResponse>("/optimal/standards/get", request);
return response.data;
}
/**
* Validate an action against standards
*/
async function validateAction(request: ValidateActionRequest): Promise<ValidateActionResponse> {
if (isMockMode()) {
return { allowed: true, violations: [], warnings: [], relevant_standards: [] };
}
const response = await api.post<ValidateActionResponse>("/optimal/standards/validate", request);
return response.data;
}
// =============================================================================
// CODE REVIEW ENDPOINTS
// =============================================================================
/**
* Review code against standards
*/
async function reviewCode(request: CodeReviewRequest): Promise<CodeReviewResponse> {
if (isMockMode()) {
return {
file_path: request.file_path,
approved: true,
score: 85,
comments: [],
standards_checked: ["coding-standards"],
similar_reviews: [],
};
}
const response = await api.post<CodeReviewResponse>("/optimal/review/code", request);
return response.data;
}
// =============================================================================
// LEARNING ENDPOINTS
// =============================================================================
/**
* Record a learning for knowledge sharing
*/
async function recordLearning(request: LearningRecordRequest): Promise<LearningRecordResponse> {
if (isMockMode()) {
return { learning_id: "learn-" + Date.now(), status: "recorded" };
}
const response = await api.post<LearningRecordResponse>("/optimal/learnings/record", request);
return response.data;
}
/**
* Search for relevant learnings
*/
async function searchLearnings(request: LearningSearchRequest): Promise<KBSearchResponse> {
if (isMockMode()) {
return { results: [], total: 0, query: request.query };
}
const response = await api.post<KBSearchResponse>("/optimal/learnings/search", request);
return response.data;
}
// =============================================================================
// PROACTIVE CONTEXT ENDPOINTS
// =============================================================================
/**
* Get proactive context for a task
*/
async function getProactiveContext(request: ProactiveContextRequest): Promise<ProactiveContextResponse> {
if (isMockMode()) {
return {
task_id: request.task_id,
similar_tasks: [],
relevant_learnings: [],
code_patterns: [],
applicable_standards: [],
recent_decisions: [],
known_issues: [],
summary: "No relevant context found for this task.",
};
}
const response = await api.post<ProactiveContextResponse>("/optimal/context/proactive", request);
return response.data;
}
// =============================================================================
// TOKEN ESTIMATION
// =============================================================================
/**
* Estimate token count for content
*/
async function estimateTokens(request: TokenEstimateRequest): Promise<TokenEstimateResponse> {
if (isMockMode()) {
return {
token_count: Math.ceil(request.content.length / 4),
model: request.model || "claude-sonnet-4-20250514",
content_length: request.content.length,
};
}
const response = await api.post<TokenEstimateResponse>("/optimal/tokens/estimate", request);
return response.data;
}
// =============================================================================
// EXPORT API
// =============================================================================
export const knowledgeBaseApi = {
// Search
search,
// RAG
ragQuery,
getContext,
// Stats & Health
getStats,
getIndexStats,
getHealth,
// Browse
listDocuments,
// Index Management
deleteIndex,
refreshIndex,
reindexAll,
checkStaleness,
// Mentor
askMentor,
// Errors
searchErrors,
recordError,
// Decisions
checkDecision,
recordDecision,
// Standards
getStandards,
validateAction,
// Code Review
reviewCode,
// Learnings
recordLearning,
searchLearnings,
// Proactive Context
getProactiveContext,
// Token Estimation
estimateTokens,
};
export default knowledgeBaseApi;
+123
View File
@@ -0,0 +1,123 @@
import api from "./client";
import type { Message, MessageType } from "@/types";
import { isMockMode, getMockMessages, AGENT_IDS, CHANNEL_IDS } from "@/lib/mock-data";
// Store for mock messages (persists during session)
let mockMessagesStore: Message[] | null = null;
const getMessages = (): Message[] => {
if (!mockMessagesStore) {
mockMessagesStore = getMockMessages() as Message[];
}
return mockMessagesStore;
};
export const messagesApi = {
// List messages for a session
listBySession: async (
sessionId: string,
limit: number = 50,
before?: string,
after?: string
): Promise<{ items: Message[]; has_more: boolean }> => {
if (isMockMode()) {
let messages = getMessages().filter(
(m) => m.session_id === sessionId
);
if (before) {
messages = messages.filter(
(m) => new Date(m.timestamp) < new Date(before)
);
}
if (after) {
messages = messages.filter(
(m) => new Date(m.timestamp) > new Date(after)
);
}
return {
items: messages.slice(0, limit),
has_more: messages.length > limit,
};
}
const { data } = await api.get<{ items: Message[]; has_more: boolean }>("/messages", {
params: { session_id: sessionId, limit, before, after },
});
return data;
},
// Get message by ID
get: async (messageId: string): Promise<Message> => {
if (isMockMode()) {
const message = getMessages().find((m) => m.id === messageId);
if (message) return message;
throw new Error("Message not found");
}
const { data } = await api.get<Message>("/messages/" + messageId);
return data;
},
// Send a message
send: async (sessionId: string, content: string, type: string = "dialogue"): Promise<Message> => {
if (isMockMode()) {
const newMessage: Message = {
id: `msg-${Date.now()}`,
agent_id: AGENT_IDS.ceo,
channel_id: CHANNEL_IDS.backendCell,
group_id: `msg-${Date.now()}`,
session_id: sessionId,
type: type as MessageType,
content,
content_length: content.length,
is_reply: false,
reply_to: null,
mentions: [],
task_id: null,
commit_ref: null,
timestamp: new Date().toISOString(),
edited_at: null,
was_edited: false,
};
getMessages().push(newMessage);
return newMessage;
}
const { data } = await api.post<Message>("/messages", {
session_id: sessionId,
content,
type,
});
return data;
},
// Edit a message
edit: async (messageId: string, content: string): Promise<Message> => {
if (isMockMode()) {
const messages = getMessages();
const idx = messages.findIndex((m) => m.id === messageId);
if (idx !== -1) {
const message = messages[idx];
const editedMessage: Message = {
...message,
content,
content_length: content.length,
edited_at: new Date().toISOString(),
was_edited: true,
};
messages[idx] = editedMessage;
return editedMessage;
}
throw new Error("Message not found");
}
const { data } = await api.patch<Message>("/messages/" + messageId, { content });
return data;
},
// Delete a message
delete: async (messageId: string): Promise<void> => {
if (isMockMode()) {
const messages = getMessages();
const idx = messages.findIndex((m) => m.id === messageId);
if (idx !== -1) messages.splice(idx, 1);
return;
}
await api.delete("/messages/" + messageId);
},
};
+114
View File
@@ -0,0 +1,114 @@
import api from "./client";
import type { Notification, NotificationListResponse, NotificationType, NotificationPriority } from "@/types";
import { isMockMode, mockNotifications } from "@/lib/mock-data";
export interface NotificationFilters {
type?: NotificationType;
priority?: NotificationPriority;
unread_only?: boolean;
pending_ack_only?: boolean;
}
export const notificationsApi = {
// List notifications for CEO
list: async (filters?: NotificationFilters): Promise<NotificationListResponse> => {
if (isMockMode()) {
let notifications = [...mockNotifications] as Notification[];
if (filters?.type) {
notifications = notifications.filter((n) => n.type === filters.type);
}
if (filters?.priority) {
notifications = notifications.filter((n) => n.priority === filters.priority);
}
if (filters?.unread_only) {
notifications = notifications.filter((n) => !n.is_read);
}
if (filters?.pending_ack_only) {
notifications = notifications.filter((n) => n.requires_ack && !n.is_acknowledged);
}
return {
items: notifications,
total: notifications.length,
unread_count: notifications.filter((n) => !n.is_read).length,
pending_ack_count: notifications.filter((n) => n.requires_ack && !n.is_acknowledged).length,
};
}
// Backend uses type_filter, priority_filter parameter names
const params: Record<string, unknown> = {};
if (filters?.type) params.type_filter = filters.type;
if (filters?.priority) params.priority_filter = filters.priority;
if (filters?.unread_only) params.unread_only = filters.unread_only;
if (filters?.pending_ack_only) params.pending_ack_only = filters.pending_ack_only;
const { data } = await api.get<NotificationListResponse>("/notifications", { params });
return data;
},
// Get notification by ID
get: async (notificationId: string): Promise<Notification> => {
if (isMockMode()) {
const notification = mockNotifications.find((n) => n.id === notificationId);
if (notification) return notification as Notification;
throw new Error("Notification not found");
}
const { data } = await api.get<Notification>("/notifications/" + notificationId);
return data;
},
// Mark notification as read (backend returns 204 No Content)
markRead: async (notificationId: string): Promise<void> => {
if (isMockMode()) {
const idx = mockNotifications.findIndex((n) => n.id === notificationId);
if (idx !== -1) {
const notification = mockNotifications[idx];
mockNotifications[idx] = { ...notification, is_read: true };
}
return;
}
await api.post("/notifications/" + notificationId + "/read");
},
// Acknowledge notification (backend uses /ack)
acknowledge: async (notificationId: string): Promise<Notification> => {
if (isMockMode()) {
const idx = mockNotifications.findIndex((n) => n.id === notificationId);
if (idx !== -1) {
const notification = mockNotifications[idx];
const ackedNotification = {
...notification,
is_acknowledged: true,
is_fully_acknowledged: true,
};
mockNotifications[idx] = ackedNotification;
return ackedNotification as Notification;
}
throw new Error("Notification not found");
}
const { data } = await api.post<Notification>(
"/notifications/" + notificationId + "/ack"
);
return data;
},
// Mark all as read
markAllRead: async (): Promise<void> => {
if (isMockMode()) {
for (let i = 0; i < mockNotifications.length; i++) {
mockNotifications[i] = { ...mockNotifications[i], is_read: true };
}
return;
}
// Get all unread notifications
const { data } = await api.get<{ items: Notification[] }>("/notifications", {
params: { unread_only: true },
});
// Mark each as read
await Promise.all(
data.items.map((n) =>
api.post("/notifications/" + n.id + "/read").catch(() => {
// Ignore individual failures
})
)
);
},
};
+104
View File
@@ -0,0 +1,104 @@
import api from "./client";
import type { OrchestratorStatus, AgentStatusResponse, WaitingAgent } from "@/types";
import { isMockMode, mockOrchestratorStatus, mockWaitingAgents } from "@/lib/mock-data";
export interface SpawnAgentRequest {
task_id?: string;
initial_prompt?: string;
}
export const orchestratorApi = {
// Get orchestrator status
getStatus: async (): Promise<OrchestratorStatus> => {
if (isMockMode()) {
return mockOrchestratorStatus as OrchestratorStatus;
}
const { data } = await api.get<OrchestratorStatus>("/orchestrator/status");
return data;
},
// Get specific agent status
getAgentStatus: async (agentId: string): Promise<AgentStatusResponse> => {
if (isMockMode()) {
const found = mockOrchestratorStatus.agents.find(a => a.agent_id === agentId);
if (found) return found as AgentStatusResponse;
throw new Error("Agent not found");
}
const { data } = await api.get<AgentStatusResponse>("/orchestrator/agents/" + agentId);
return data;
},
// Get waiting agents
getWaitingAgents: async (): Promise<WaitingAgent[]> => {
if (isMockMode()) {
return mockWaitingAgents as WaitingAgent[];
}
const { data } = await api.get<WaitingAgent[]>("/orchestrator/waiting");
return data;
},
// Spawn an agent
spawn: async (agentId: string, request?: SpawnAgentRequest): Promise<AgentStatusResponse> => {
if (isMockMode()) {
// Find or create agent in mock status
const existing = mockOrchestratorStatus.agents.find(a => a.agent_id === agentId);
if (existing) {
existing.state = "running";
// Mock data uses string, so use empty string for no task
(existing as { task_id: string }).task_id = request?.task_id ?? "";
return existing as AgentStatusResponse;
}
const newAgent = {
agent_id: agentId,
state: "running",
task_id: request?.task_id ?? "",
error_count: 0,
started_at: new Date().toISOString(),
waiting_for: null as string | null,
};
mockOrchestratorStatus.agents.push(newAgent);
return newAgent as AgentStatusResponse;
}
const { data } = await api.post<AgentStatusResponse>(
"/orchestrator/agents/" + agentId + "/spawn",
{ agent_id: agentId, ...request }
);
return data;
},
// Stop an agent
stop: async (agentId: string, graceful: boolean = true): Promise<void> => {
if (isMockMode()) {
const agent = mockOrchestratorStatus.agents.find(a => a.agent_id === agentId);
if (agent) {
agent.state = "idle";
(agent as { task_id: string }).task_id = "";
}
return;
}
await api.post("/orchestrator/agents/" + agentId + "/stop", null, {
params: { graceful },
});
},
// Resolve a waiting agent
resolveWait: async (agentId: string, resolution: string): Promise<AgentStatusResponse> => {
if (isMockMode()) {
// Remove from waiting agents
const idx = mockWaitingAgents.findIndex(a => a.agent_id === agentId);
if (idx !== -1) mockWaitingAgents.splice(idx, 1);
// Update agent status
const agent = mockOrchestratorStatus.agents.find(a => a.agent_id === agentId);
if (agent) {
agent.state = "running";
return agent as AgentStatusResponse;
}
throw new Error("Agent not found");
}
const { data } = await api.post<AgentStatusResponse>(
"/orchestrator/agents/" + agentId + "/resolve-wait",
{ resolution }
);
return data;
},
};
+204
View File
@@ -0,0 +1,204 @@
import api from "./client";
import type {
Project,
ProjectCreate,
ProjectUpdate,
ProjectSummary,
Team,
} from "@/types";
import { isMockMode } from "@/lib/mock-data";
// Mock data for offline mode
const mockProjects: Project[] = [];
export interface ProjectFilters {
assigned_cell?: Team;
active_only?: boolean;
limit?: number;
offset?: number;
}
export const projectsApi = {
// List projects with optional filters
list: async (filters?: ProjectFilters): Promise<ProjectSummary[]> => {
if (isMockMode()) {
let projects = [...mockProjects];
if (filters?.assigned_cell) {
projects = projects.filter((p) => p.assigned_cell === filters.assigned_cell);
}
if (filters?.active_only) {
projects = projects.filter((p) => p.is_active);
}
return projects.map((p) => ({
id: p.id,
name: p.name,
slug: p.slug,
git_url: p.git_url,
assigned_cell: p.assigned_cell,
is_active: p.is_active,
has_workspace: !!p.workspace_path,
has_git_token: false, // Mock mode has no tokens
}));
}
const params = new URLSearchParams();
if (filters?.assigned_cell) params.append("assigned_cell", filters.assigned_cell);
if (filters?.active_only !== undefined) params.append("active_only", String(filters.active_only));
if (filters?.limit) params.append("limit", String(filters.limit));
if (filters?.offset) params.append("offset", String(filters.offset));
const url = "/projects?" + params.toString();
const { data } = await api.get<ProjectSummary[]>(url);
return data;
},
// Get single project
get: async (projectId: string): Promise<Project> => {
if (isMockMode()) {
const project = mockProjects.find((p) => p.id === projectId);
if (!project) throw new Error("Project not found");
return project;
}
const { data } = await api.get<Project>("/projects/" + projectId);
return data;
},
// Create project (PM only)
create: async (project: ProjectCreate): Promise<Project> => {
if (isMockMode()) {
const now = new Date().toISOString();
const newProject: Project = {
id: `project-${Date.now()}`,
name: project.name,
slug: project.slug,
git_url: project.git_url,
default_branch: project.default_branch ?? "main",
protected_branches: project.protected_branches ?? ["main", "master"],
assigned_cell: project.assigned_cell,
has_git_token: !!project.git_token, // Mock token status
is_active: true,
test_command: project.test_command ?? null,
lint_command: project.lint_command ?? null,
format_command: project.format_command ?? null,
typecheck_command: project.typecheck_command ?? null,
build_command: project.build_command ?? null,
workspace_path: null,
last_synced_at: null,
head_commit: null,
created_by: "mock-user",
created_at: now,
updated_at: null,
};
mockProjects.push(newProject);
return newProject;
}
const { data } = await api.post<Project>("/projects", project);
return data;
},
// Update project (PM only)
update: async (projectId: string, updates: ProjectUpdate): Promise<Project> => {
if (isMockMode()) {
const idx = mockProjects.findIndex((p) => p.id === projectId);
if (idx === -1) throw new Error("Project not found");
const now = new Date().toISOString();
mockProjects[idx] = { ...mockProjects[idx], ...updates, updated_at: now };
return mockProjects[idx];
}
const { data } = await api.patch<Project>("/projects/" + projectId, updates);
return data;
},
// Set workspace path for local development
setWorkspace: async (projectId: string, workspacePath: string): Promise<Project> => {
if (isMockMode()) {
const idx = mockProjects.findIndex((p) => p.id === projectId);
if (idx === -1) throw new Error("Project not found");
const now = new Date().toISOString();
mockProjects[idx] = { ...mockProjects[idx], workspace_path: workspacePath, updated_at: now };
return mockProjects[idx];
}
const { data } = await api.post<Project>("/projects/" + projectId + "/workspace", { workspace_path: workspacePath });
return data;
},
// Update sync state (for tracking git status)
updateSyncState: async (
projectId: string,
headCommit: string
): Promise<Project> => {
if (isMockMode()) {
const idx = mockProjects.findIndex((p) => p.id === projectId);
if (idx === -1) throw new Error("Project not found");
const now = new Date().toISOString();
mockProjects[idx] = {
...mockProjects[idx],
head_commit: headCommit,
last_synced_at: now,
updated_at: now,
};
return mockProjects[idx];
}
const { data } = await api.post<Project>("/projects/" + projectId + "/sync-state", {
head_commit: headCommit,
});
return data;
},
// Deactivate project (soft delete)
deactivate: async (projectId: string): Promise<Project> => {
if (isMockMode()) {
const idx = mockProjects.findIndex((p) => p.id === projectId);
if (idx === -1) throw new Error("Project not found");
const now = new Date().toISOString();
mockProjects[idx] = { ...mockProjects[idx], is_active: false, updated_at: now };
return mockProjects[idx];
}
const { data } = await api.patch<Project>("/projects/" + projectId, { is_active: false });
return data;
},
// Delete project permanently
delete: async (projectId: string): Promise<void> => {
if (isMockMode()) {
const idx = mockProjects.findIndex((p) => p.id === projectId);
if (idx !== -1) mockProjects.splice(idx, 1);
return;
}
await api.delete("/projects/" + projectId);
},
// Grant agent access to project
grantAccess: async (projectId: string, agentId: string): Promise<void> => {
if (isMockMode()) {
return;
}
await api.post("/projects/" + projectId + "/access/" + agentId);
},
// Revoke agent access from project
revokeAccess: async (projectId: string, agentId: string): Promise<void> => {
if (isMockMode()) {
return;
}
await api.delete("/projects/" + projectId + "/access/" + agentId);
},
// Trigger git sync for project
sync: async (projectId: string): Promise<Project> => {
if (isMockMode()) {
const idx = mockProjects.findIndex((p) => p.id === projectId);
if (idx === -1) throw new Error("Project not found");
const now = new Date().toISOString();
mockProjects[idx] = {
...mockProjects[idx],
last_synced_at: now,
updated_at: now,
};
return mockProjects[idx];
}
const { data } = await api.post<Project>("/projects/" + projectId + "/sync");
return data;
},
};
+68
View File
@@ -0,0 +1,68 @@
import api from "./client";
import type {
AssignmentScope,
ModelProvider,
} from "@/types";
// Matches the backend's CatalogEntryResponse.
export interface CatalogEntry {
model_name: string;
provider_type: ModelProvider;
display_name: string;
}
export interface OllamaKeyStatus {
has_key: boolean;
enabled: boolean;
}
export interface ModelAssignment {
id: string;
scope: AssignmentScope;
scope_value: string | null;
provider_type: ModelProvider;
model_name: string;
}
export type RoutingMode = "anthropic" | "ollama" | "mix";
export interface ModeSnapshot {
mode: RoutingMode;
assignments: ModelAssignment[];
}
export interface ApplyModePayload {
mode: RoutingMode;
default_model?: string;
// Required when mode=mix: map of agent_slug → model_name.
per_agent?: Record<string, string>;
}
export const providersApi = {
catalog: async (): Promise<CatalogEntry[]> => {
const { data } = await api.get<CatalogEntry[]>("/providers/catalog");
return data;
},
getOllamaKey: async (): Promise<OllamaKeyStatus> => {
const { data } = await api.get<OllamaKeyStatus>("/providers/ollama-key");
return data;
},
setOllamaKey: async (apiKey: string): Promise<OllamaKeyStatus> => {
const { data } = await api.put<OllamaKeyStatus>("/providers/ollama-key", {
api_key: apiKey,
});
return data;
},
getMode: async (): Promise<ModeSnapshot> => {
const { data } = await api.get<ModeSnapshot>("/providers");
return data;
},
applyMode: async (payload: ApplyModePayload): Promise<ModeSnapshot> => {
const { data } = await api.post<ModeSnapshot>("/providers", payload);
return data;
},
};
+182
View File
@@ -0,0 +1,182 @@
import api from "./client";
import type { Session } from "@/types";
import { SessionStatus, SessionScope } from "@/types";
import { isMockMode, mockSessions } from "@/lib/mock-data";
// Session-Task link response from API
export interface SessionTaskLinkResponse {
id: string;
session_id: string;
task_id: string;
is_primary: boolean;
relationship_type: string;
added_at: string;
added_by: string | null;
}
export interface SessionCreate {
group_id: string;
scope?: string;
}
export const sessionsApi = {
// List sessions for a group
listByGroup: async (groupId: string, limit: number = 50): Promise<Session[]> => {
if (isMockMode()) {
return (mockSessions as Session[]).slice(0, limit);
}
const { data } = await api.get<{ items: Session[]; total: number }>("/sessions", {
params: { group_id: groupId, limit },
});
return data.items;
},
// Get session by ID
get: async (sessionId: string): Promise<Session> => {
if (isMockMode()) {
const session = mockSessions.find((s) => s.id === sessionId);
if (session) return session as Session;
throw new Error("Session not found");
}
const { data } = await api.get<Session>("/sessions/" + sessionId);
return data;
},
// Get sessions linked to a task
getForTask: async (taskId: string): Promise<SessionTaskLinkResponse[]> => {
if (isMockMode()) {
return []; // No mock session-task links
}
const { data } = await api.get<SessionTaskLinkResponse[]>("/sessions/for-task/" + taskId);
return data;
},
// Close a session
close: async (sessionId: string): Promise<Session> => {
if (isMockMode()) {
const idx = mockSessions.findIndex((s) => s.id === sessionId);
if (idx !== -1) {
const session = mockSessions[idx] as Session;
const closedSession: Session = {
...session,
status: "closed" as SessionStatus,
closed_at: new Date().toISOString(),
};
(mockSessions as Session[])[idx] = closedSession;
return closedSession;
}
throw new Error("Session not found");
}
const { data } = await api.post<Session>("/sessions/" + sessionId + "/close");
return data;
},
// Link a task to a session (PM only)
linkTask: async (
sessionId: string,
taskId: string,
isPrimary: boolean = false,
relationshipType: string = "discussion"
): Promise<SessionTaskLinkResponse> => {
const { data } = await api.post<SessionTaskLinkResponse>(
"/sessions/" + sessionId + "/add-task",
{
task_id: taskId,
is_primary: isPrimary,
relationship_type: relationshipType,
}
);
return data;
},
// Unlink a task from a session (PM only)
unlinkTask: async (sessionId: string, taskId: string): Promise<void> => {
await api.delete("/sessions/" + sessionId + "/remove-task", {
data: { task_id: taskId },
});
},
// Get tasks linked to a session
getTasksForSession: async (sessionId: string): Promise<SessionTaskLinkResponse[]> => {
// Note: Backend doesn't have a dedicated GET endpoint for session tasks
// Task links are returned with session detail via get()
const session = await sessionsApi.get(sessionId);
// Return empty array if session doesn't have task_links
return (session as Session & { task_links?: SessionTaskLinkResponse[] }).task_links || [];
},
// Create a session for tasks (PM only)
createForTasks: async (
taskIds: string[],
channelSlug: string,
relationshipType: string = "discussion"
): Promise<{ session: Session; links: SessionTaskLinkResponse[] }> => {
const { data } = await api.post<{ session: Session; links: SessionTaskLinkResponse[] }>(
"/sessions/for-tasks",
{
task_ids: taskIds,
channel_slug: channelSlug,
relationship_type: relationshipType,
}
);
return data;
},
// Create a new session directly
create: async (session: SessionCreate): Promise<Session> => {
if (isMockMode()) {
const now = new Date().toISOString();
const newSession: Session = {
id: `session-${Date.now()}`,
group_id: session.group_id,
status: SessionStatus.ACTIVE,
scope: (session.scope as SessionScope) || SessionScope.CELL,
message_count: 0,
total_content_length: 0,
started_at: now,
last_activity_at: now,
closed_at: null,
task_links: [],
};
(mockSessions as Session[]).push(newSession);
return newSession;
}
const { data } = await api.post<Session>("/sessions", session);
return data;
},
// Update a task link in a session
updateTaskLink: async (
sessionId: string,
taskId: string,
updates: { is_primary?: boolean; relationship_type?: string }
): Promise<SessionTaskLinkResponse> => {
if (isMockMode()) {
return {
id: `link-${Date.now()}`,
session_id: sessionId,
task_id: taskId,
is_primary: updates.is_primary ?? false,
relationship_type: updates.relationship_type ?? "discussion",
added_at: new Date().toISOString(),
added_by: null,
};
}
const { data } = await api.post<SessionTaskLinkResponse>(
"/sessions/" + sessionId + "/update-task",
{ task_id: taskId, ...updates }
);
return data;
},
};
export const groupsApi = {
// Get groups for a channel
listByChannel: async (channelId: string): Promise<unknown[]> => {
if (isMockMode()) {
return [];
}
const { data } = await api.get<unknown[]>("/channels/" + channelId + "/groups");
return data;
},
};
+169
View File
@@ -0,0 +1,169 @@
/**
* Stream API Client
*
* API functions for transcription and extraction operations.
*/
import api from "./client";
import { isMockMode } from "@/lib/mock-data";
// =============================================================================
// Types
// =============================================================================
export interface StreamChunkRequest {
channel_name: string;
audio_data: string; // Base64 encoded
chunk_index: number;
is_final?: boolean;
}
export interface StreamChunkResponse {
status: string;
chunk_index: number;
queued: boolean;
}
export interface StreamCompleteRequest {
channel_name: string;
session_id?: string;
}
export interface StreamCompleteResponse {
status: string;
channel_name: string;
total_chunks: number;
transcription_id?: string;
}
export interface ExtractionRequest {
content: string;
extraction_type: "tasks" | "decisions" | "action_items" | "summary";
context?: string;
}
export interface ExtractionResponse {
extraction_type: string;
results: Record<string, unknown>[];
confidence: number;
}
export interface TranscriptionStats {
total_transcriptions: number;
active_channels: number;
chunks_processed: number;
avg_processing_time_ms: number;
}
export interface ChannelPermissions {
channel_name: string;
can_transcribe: boolean;
can_extract: boolean;
rate_limit?: number;
}
// =============================================================================
// API Client
// =============================================================================
export const streamApi = {
// ===========================================================================
// TRANSCRIPTION ENDPOINTS
// ===========================================================================
/**
* Submit an audio chunk for transcription
*/
submitChunk: async (request: StreamChunkRequest): Promise<StreamChunkResponse> => {
if (isMockMode()) {
return {
status: "queued",
chunk_index: request.chunk_index,
queued: true,
};
}
const { data } = await api.post<StreamChunkResponse>("/stream/chunk", request);
return data;
},
/**
* Complete a transcription session
*/
complete: async (request: StreamCompleteRequest): Promise<StreamCompleteResponse> => {
if (isMockMode()) {
return {
status: "completed",
channel_name: request.channel_name,
total_chunks: 10,
transcription_id: `trans-${Date.now()}`,
};
}
const { data } = await api.post<StreamCompleteResponse>("/stream/complete", request);
return data;
},
// ===========================================================================
// EXTRACTION ENDPOINTS
// ===========================================================================
/**
* Extract structured information from content
*/
extract: async (request: ExtractionRequest): Promise<ExtractionResponse> => {
if (isMockMode()) {
return {
extraction_type: request.extraction_type,
results: [],
confidence: 0.85,
};
}
const { data } = await api.post<ExtractionResponse>("/stream/extract", request);
return data;
},
// ===========================================================================
// STATS & PERMISSIONS
// ===========================================================================
/**
* Get transcription statistics
*/
getStats: async (): Promise<TranscriptionStats> => {
if (isMockMode()) {
return {
total_transcriptions: 100,
active_channels: 5,
chunks_processed: 1500,
avg_processing_time_ms: 250,
};
}
const { data } = await api.get<TranscriptionStats>("/stream/stats");
return data;
},
/**
* Get all channel permissions
*/
getPermissions: async (): Promise<ChannelPermissions[]> => {
if (isMockMode()) {
return [];
}
const { data } = await api.get<ChannelPermissions[]>("/stream/permissions");
return data;
},
/**
* Get permissions for a specific channel
*/
getChannelPermissions: async (channelName: string): Promise<ChannelPermissions> => {
if (isMockMode()) {
return {
channel_name: channelName,
can_transcribe: true,
can_extract: true,
};
}
const { data } = await api.get<ChannelPermissions>(`/stream/permissions/channel/${channelName}`);
return data;
},
};
+651
View File
@@ -0,0 +1,651 @@
import api from "./client";
import { TaskStatus, Complexity, TaskNature, TaskType } from "@/types";
import type {
Task,
TaskCreate,
Team,
ProgressRequest,
CheckpointRequest,
CommitRequest,
SoftBlockRequest,
EscalateRequest,
EscalateResponse,
TaskCountResponse,
} from "@/types";
import { isMockMode, mockTasks } from "@/lib/mock-data";
export interface TaskFilters {
status?: TaskStatus;
team?: Team;
limit?: number;
offset?: number;
}
export const tasksApi = {
// List tasks with optional filters
list: async (filters?: TaskFilters): Promise<Task[]> => {
if (isMockMode()) {
let tasks = [...mockTasks];
if (filters?.status) {
tasks = tasks.filter((t) => t.status === filters.status);
}
if (filters?.team) {
tasks = tasks.filter((t) => t.team === filters.team);
}
return tasks;
}
const params = new URLSearchParams();
if (filters?.status) params.append("status", filters.status);
if (filters?.team) params.append("team", filters.team);
if (filters?.limit) params.append("limit", String(filters.limit));
if (filters?.offset) params.append("offset", String(filters.offset));
const url = "/tasks?" + params.toString();
const { data } = await api.get<Task[]>(url);
return data;
},
// Get single task
get: async (taskId: string): Promise<Task> => {
if (isMockMode()) {
const task = mockTasks.find((t) => t.id === taskId);
if (!task) throw new Error("Task not found");
return task;
}
const { data } = await api.get<Task>("/tasks/" + taskId);
return data;
},
// Create task
create: async (task: TaskCreate): Promise<Task> => {
if (isMockMode()) {
const now = new Date().toISOString();
const newTask: Task = {
id: `task-${Date.now()}`,
title: task.title,
description: task.description,
team: task.team,
priority: task.priority ?? 2,
sequence: mockTasks.length + 1, // Auto-increment sequence
estimated_complexity: task.estimated_complexity ?? Complexity.MEDIUM,
nature: task.nature ?? TaskNature.TECHNICAL,
task_type: task.task_type ?? TaskType.CODE,
project_id: task.project_id,
docs_complete: false,
pr_created: false,
pm_approvals: {},
acceptance_criteria: task.acceptance_criteria,
parent_task_id: task.parent_task_id ?? null,
target_date: task.target_date ?? null,
status: task.status ?? TaskStatus.PENDING,
dependency_ids: [],
blocker_ids: [],
created_at: now,
updated_at: now,
created_by: "00000000-0000-0000-0000-000000000001", // CEO
assigned_to: null,
claimed_at: null,
started_at: null,
completed_at: null,
self_verified: false,
qa_verified: null,
plan: null,
progress_updates: [],
checkpoints: [],
commits: [],
dev_notes: null,
qa_notes: null,
auditor_notes: null,
quick_context: null,
sessions: [],
branch_name: null,
pr_number: null,
pr_url: null,
};
mockTasks.push(newTask);
return newTask;
}
const { data } = await api.post<Task>("/tasks", task);
return data;
},
// Update task
update: async (taskId: string, updates: Partial<Task>): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
mockTasks[idx] = { ...mockTasks[idx], ...updates, updated_at: new Date().toISOString() };
return mockTasks[idx];
}
const { data } = await api.put<Task>("/tasks/" + taskId, updates);
return data;
},
// Delete task
delete: async (taskId: string): Promise<void> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx !== -1) mockTasks.splice(idx, 1);
return;
}
await api.delete("/tasks/" + taskId);
},
// =========================================================================
// LIFECYCLE ACTIONS
// =========================================================================
claim: async (taskId: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.CLAIMED, claimed_at: now, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/claim");
return data;
},
start: async (taskId: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.IN_PROGRESS, started_at: now, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/start");
return data;
},
block: async (taskId: string, blockerId?: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
const blockerIds = blockerId ? [...mockTasks[idx].blocker_ids, blockerId] : mockTasks[idx].blocker_ids;
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.BLOCKED, blocker_ids: blockerIds, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/block", { blocker_id: blockerId });
return data;
},
unblock: async (taskId: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.IN_PROGRESS, blocker_ids: [], updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/unblock");
return data;
},
pause: async (taskId: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.PAUSED, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/pause");
return data;
},
resume: async (taskId: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.IN_PROGRESS, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/resume");
return data;
},
verify: async (taskId: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.VERIFYING, self_verified: true, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/verify");
return data;
},
submitQa: async (taskId: string, devNotes?: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.AWAITING_QA, dev_notes: devNotes ?? null, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/submit-qa", { dev_notes: devNotes });
return data;
},
passQa: async (taskId: string, qaNotes?: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.AWAITING_DOCUMENTATION, qa_verified: true, qa_notes: qaNotes ?? null, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/pass-qa", { qa_notes: qaNotes });
return data;
},
failQa: async (taskId: string, qaNotes?: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.NEEDS_REVISION, qa_verified: false, qa_notes: qaNotes ?? null, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/fail-qa", { qa_notes: qaNotes });
return data;
},
complete: async (taskId: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.COMPLETED, completed_at: now, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/complete");
return data;
},
cancel: async (taskId: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.CANCELLED, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/cancel");
return data;
},
// Note: reopen is not supported by backend - use update with status change instead
reopen: async (taskId: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.PENDING, completed_at: null, updated_at: now };
return mockTasks[idx];
}
// Backend doesn't have /reopen endpoint - use update instead
const { data } = await api.put<Task>("/tasks/" + taskId, { status: TaskStatus.PENDING });
return data;
},
// Activate a task from BACKLOG status (PM only)
activate: async (taskId: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.PENDING, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/activate");
return data;
},
// Mark documentation as complete (Documenter only)
docsComplete: async (taskId: string, docNotes?: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.AWAITING_PM_REVIEW, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/docs-complete", docNotes ?? null);
return data;
},
// =========================================================================
// CONVENIENCE METHODS
// =========================================================================
getMyTasks: async (): Promise<Task[]> => {
if (isMockMode()) {
// Return tasks assigned to "me" (mock: any assigned task)
return mockTasks.filter((t) => t.assigned_to !== null);
}
const { data } = await api.get<Task[]>("/tasks/my");
return data;
},
getPending: async (): Promise<Task[]> => {
if (isMockMode()) {
return mockTasks.filter((t) => t.status === TaskStatus.PENDING);
}
const { data } = await api.get<Task[]>("/tasks/pending");
return data;
},
getBlocked: async (): Promise<Task[]> => {
if (isMockMode()) {
return mockTasks.filter((t) => t.status === TaskStatus.BLOCKED);
}
const { data } = await api.get<Task[]>("/tasks/blocked");
return data;
},
getAwaitingQa: async (): Promise<Task[]> => {
if (isMockMode()) {
return mockTasks.filter((t) => t.status === TaskStatus.AWAITING_QA);
}
const { data } = await api.get<Task[]>("/tasks/awaiting-qa");
return data;
},
getTeamTasks: async (team: Team): Promise<Task[]> => {
if (isMockMode()) {
return mockTasks.filter((t) => t.team === team);
}
const { data } = await api.get<Task[]>("/tasks/team/" + team);
return data;
},
getAwaitingDocs: async (): Promise<Task[]> => {
if (isMockMode()) {
return mockTasks.filter((t) => t.status === TaskStatus.AWAITING_DOCUMENTATION);
}
const { data } = await api.get<Task[]>("/tasks/awaiting-docs");
return data;
},
getSubtasks: async (taskId: string): Promise<Task[]> => {
if (isMockMode()) {
return mockTasks.filter((t) => t.parent_task_id === taskId);
}
const { data } = await api.get<Task[]>("/tasks/" + taskId + "/subtasks");
return data;
},
// =========================================================================
// STATS
// =========================================================================
getStats: async (): Promise<TaskCountResponse> => {
if (isMockMode()) {
const counts: Record<string, number> = {};
mockTasks.forEach((t) => {
counts[t.status] = (counts[t.status] || 0) + 1;
});
return { counts };
}
const { data } = await api.get<TaskCountResponse>("/tasks/stats");
return data;
},
getStatsByTeam: async (): Promise<TaskCountResponse> => {
if (isMockMode()) {
const counts: Record<string, number> = {};
mockTasks.forEach((t) => {
counts[t.team] = (counts[t.team] || 0) + 1;
});
return { counts };
}
const { data } = await api.get<TaskCountResponse>("/tasks/stats/by-team");
return data;
},
// =========================================================================
// PROGRESS TRACKING
// =========================================================================
addProgress: async (taskId: string, request: ProgressRequest): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
const update = {
timestamp: now,
agent_id: "00000000-0000-0000-0000-000000000001",
message: request.message,
percentage: request.percentage ?? null,
};
mockTasks[idx] = {
...mockTasks[idx],
progress_updates: [...mockTasks[idx].progress_updates, update],
updated_at: now,
};
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/progress", request);
return data;
},
addCheckpoint: async (taskId: string, request: CheckpointRequest): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
const checkpoint = {
id: `checkpoint-${Date.now()}`,
timestamp: now,
agent_id: "00000000-0000-0000-0000-000000000001",
state_summary: request.state_summary,
remaining_work: request.remaining_work,
notes: request.notes ?? null,
};
mockTasks[idx] = {
...mockTasks[idx],
checkpoints: [...mockTasks[idx].checkpoints, checkpoint],
updated_at: now,
};
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/checkpoint", request);
return data;
},
addCommit: async (taskId: string, request: CommitRequest): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
const commit = {
hash: request.hash,
message: request.message,
timestamp: now,
author_agent_id: "00000000-0000-0000-0000-000000000001",
};
mockTasks[idx] = {
...mockTasks[idx],
commits: [...mockTasks[idx].commits, commit],
updated_at: now,
};
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/commit", request);
return data;
},
// =========================================================================
// SOFT BLOCK & ESCALATION
// =========================================================================
softBlock: async (taskId: string, request: SoftBlockRequest): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = {
...mockTasks[idx],
status: TaskStatus.BLOCKED,
updated_at: now,
};
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/soft-block", request);
return data;
},
escalate: async (taskId: string, request: EscalateRequest): Promise<EscalateResponse> => {
if (isMockMode()) {
return {
status: "escalated",
task_id: taskId,
escalated_to: request.escalate_to || "cell-pm",
reason: request.reason,
message: "Task escalated successfully (mock)",
};
}
const { data } = await api.post<EscalateResponse>("/tasks/" + taskId + "/escalate", request);
return data;
},
submitPmReview: async (taskId: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = {
...mockTasks[idx],
status: TaskStatus.AWAITING_PM_REVIEW,
updated_at: now,
};
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/submit-pm-review");
return data;
},
// =========================================================================
// CEO APPROVAL (CEO only - Human-in-the-Loop)
// =========================================================================
// Get tasks awaiting CEO approval
getAwaitingCeoApproval: async (): Promise<Task[]> => {
if (isMockMode()) {
return mockTasks.filter((t) => t.status === TaskStatus.AWAITING_CEO_APPROVAL);
}
const { data } = await api.get<Task[]>("/tasks/awaiting-ceo-approval");
return data;
},
// CEO approves a task (completes it)
ceoApprove: async (taskId: string, notes?: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = {
...mockTasks[idx],
status: TaskStatus.COMPLETED,
completed_at: now,
updated_at: now,
};
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/ceo-approve", { notes });
return data;
},
// CEO rejects a task (sends back for revision)
ceoReject: async (taskId: string, notes: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = {
...mockTasks[idx],
status: TaskStatus.NEEDS_REVISION,
updated_at: now,
};
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/ceo-reject", { notes });
return data;
},
// =========================================================================
// ADDITIONAL CONVENIENCE METHODS
// =========================================================================
// Get tasks awaiting PM review
getAwaitingPmReview: async (): Promise<Task[]> => {
if (isMockMode()) {
return mockTasks.filter((t) => t.status === TaskStatus.AWAITING_PM_REVIEW);
}
const { data } = await api.get<Task[]>("/tasks/awaiting-pm-review");
return data;
},
// Get all descendants of a task (subtasks, sub-subtasks, etc.)
getDescendants: async (taskId: string): Promise<Task[]> => {
if (isMockMode()) {
// Simple mock: just return direct subtasks
return mockTasks.filter((t) => t.parent_task_id === taskId);
}
const { data } = await api.get<Task[]>("/tasks/" + taskId + "/descendants");
return data;
},
// Get sessions linked to a task
getSessions: async (taskId: string): Promise<{ session_id: string; is_primary: boolean; relationship_type: string }[]> => {
if (isMockMode()) {
return [];
}
const { data } = await api.get<{ session_id: string; is_primary: boolean; relationship_type: string }[]>(
"/tasks/" + taskId + "/sessions"
);
return data;
},
// Escalate task directly to CEO
escalateToCeo: async (taskId: string, reason: string): Promise<EscalateResponse> => {
if (isMockMode()) {
return {
status: "escalated",
task_id: taskId,
escalated_to: "ceo",
reason,
message: "Task escalated to CEO (mock)",
};
}
const { data } = await api.post<EscalateResponse>("/tasks/" + taskId + "/escalate-to-ceo", { reason });
return data;
},
// Substitute a task with another (create replacement)
substitute: async (taskId: string, replacementTaskId: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = {
...mockTasks[idx],
status: TaskStatus.CANCELLED,
updated_at: now,
};
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/substitute", {
replacement_task_id: replacementTaskId,
});
return data;
},
};
+261
View File
@@ -0,0 +1,261 @@
import api from "./client";
import type {
WorkSession,
WorkSessionSummary,
WorkSessionCreate,
WorkSessionStatus,
} from "@/types";
import { isMockMode } from "@/lib/mock-data";
// Mock data for offline mode
const mockWorkSessions: WorkSession[] = [];
export interface WorkSessionFilters {
project_id?: string;
agent_id?: string;
status?: WorkSessionStatus;
active_only?: boolean;
}
export const workSessionsApi = {
// List work sessions with optional filters
list: async (filters?: WorkSessionFilters): Promise<WorkSessionSummary[]> => {
if (isMockMode()) {
let sessions = [...mockWorkSessions];
if (filters?.project_id) {
sessions = sessions.filter((s) => s.project_id === filters.project_id);
}
if (filters?.agent_id) {
sessions = sessions.filter((s) => s.agent_id === filters.agent_id);
}
if (filters?.status) {
sessions = sessions.filter((s) => s.status === filters.status);
}
if (filters?.active_only) {
sessions = sessions.filter((s) => s.status === "active");
}
return sessions.map((s) => ({
id: s.id,
task_id: s.task_id,
branch_name: s.branch_name,
status: s.status,
started_at: s.started_at,
has_pr: s.pr_number !== null,
}));
}
const params = new URLSearchParams();
if (filters?.project_id) params.append("project_id", filters.project_id);
if (filters?.agent_id) params.append("agent_id", filters.agent_id);
if (filters?.status) params.append("status", filters.status);
if (filters?.active_only) params.append("active_only", "true");
const url = "/work-sessions?" + params.toString();
const { data } = await api.get<WorkSessionSummary[]>(url);
return data;
},
// Get single work session
get: async (sessionId: string): Promise<WorkSession> => {
if (isMockMode()) {
const session = mockWorkSessions.find((s) => s.id === sessionId);
if (!session) throw new Error("Work session not found");
return session;
}
const { data } = await api.get<WorkSession>("/work-sessions/" + sessionId);
return data;
},
// Get active work session for a task
getForTask: async (taskId: string): Promise<WorkSession | null> => {
if (isMockMode()) {
const session = mockWorkSessions.find(
(s) => s.task_id === taskId && s.status === "active"
);
return session ?? null;
}
try {
const { data } = await api.get<WorkSession | null>("/work-sessions/task/" + taskId);
return data;
} catch {
return null;
}
},
// Create work session (Developer or PM)
create: async (session: WorkSessionCreate): Promise<WorkSession> => {
if (isMockMode()) {
const now = new Date().toISOString();
const newSession: WorkSession = {
id: `session-${Date.now()}`,
project_id: session.project_id,
task_id: session.task_id,
agent_id: "00000000-0000-0000-0000-000000000001", // CEO in mock
branch_name: session.branch_name,
base_branch: session.base_branch,
target_branch: session.target_branch,
started_at: now,
ended_at: null,
status: "active" as WorkSessionStatus,
commits: [],
files_modified: [],
pr_number: null,
pr_url: null,
pr_status: null,
pr_created_at: null,
pr_merged_at: null,
merged_by: null,
created_at: now,
updated_at: now,
};
mockWorkSessions.push(newSession);
return newSession;
}
const { data } = await api.post<WorkSession>("/work-sessions", session);
return data;
},
// Add a commit to the work session
addCommit: async (sessionId: string, commitSha: string): Promise<WorkSession> => {
if (isMockMode()) {
const idx = mockWorkSessions.findIndex((s) => s.id === sessionId);
if (idx === -1) throw new Error("Work session not found");
const now = new Date().toISOString();
mockWorkSessions[idx] = {
...mockWorkSessions[idx],
commits: [...mockWorkSessions[idx].commits, commitSha],
updated_at: now,
};
return mockWorkSessions[idx];
}
const { data } = await api.post<WorkSession>("/work-sessions/" + sessionId + "/commits", {
commit_sha: commitSha,
});
return data;
},
// Add modified files to the work session
addFiles: async (sessionId: string, filePaths: string[]): Promise<WorkSession> => {
if (isMockMode()) {
const idx = mockWorkSessions.findIndex((s) => s.id === sessionId);
if (idx === -1) throw new Error("Work session not found");
const now = new Date().toISOString();
const existingFiles = new Set(mockWorkSessions[idx].files_modified);
filePaths.forEach((f) => existingFiles.add(f));
mockWorkSessions[idx] = {
...mockWorkSessions[idx],
files_modified: Array.from(existingFiles),
updated_at: now,
};
return mockWorkSessions[idx];
}
const { data } = await api.post<WorkSession>("/work-sessions/" + sessionId + "/files", {
file_paths: filePaths,
});
return data;
},
// Record PR creation
createPR: async (sessionId: string, prNumber: number, prUrl: string): Promise<WorkSession> => {
if (isMockMode()) {
const idx = mockWorkSessions.findIndex((s) => s.id === sessionId);
if (idx === -1) throw new Error("Work session not found");
const now = new Date().toISOString();
mockWorkSessions[idx] = {
...mockWorkSessions[idx],
pr_number: prNumber,
pr_url: prUrl,
pr_status: "open",
pr_created_at: now,
updated_at: now,
};
return mockWorkSessions[idx];
}
const { data } = await api.post<WorkSession>("/work-sessions/" + sessionId + "/pr", {
pr_number: prNumber,
pr_url: prUrl,
});
return data;
},
// Update PR status
updatePRStatus: async (sessionId: string, prStatus: string): Promise<WorkSession> => {
if (isMockMode()) {
const idx = mockWorkSessions.findIndex((s) => s.id === sessionId);
if (idx === -1) throw new Error("Work session not found");
const now = new Date().toISOString();
mockWorkSessions[idx] = {
...mockWorkSessions[idx],
pr_status: prStatus,
updated_at: now,
};
return mockWorkSessions[idx];
}
const { data } = await api.patch<WorkSession>("/work-sessions/" + sessionId + "/pr", {
pr_status: prStatus,
});
return data;
},
// Record PR merge (PM only)
mergePR: async (sessionId: string, mergedBy: string): Promise<WorkSession> => {
if (isMockMode()) {
const idx = mockWorkSessions.findIndex((s) => s.id === sessionId);
if (idx === -1) throw new Error("Work session not found");
const now = new Date().toISOString();
mockWorkSessions[idx] = {
...mockWorkSessions[idx],
pr_status: "merged",
pr_merged_at: now,
merged_by: mergedBy,
status: "completed" as WorkSessionStatus,
ended_at: now,
updated_at: now,
};
return mockWorkSessions[idx];
}
const { data } = await api.post<WorkSession>("/work-sessions/" + sessionId + "/pr/merge", {
merged_by: mergedBy,
});
return data;
},
// Complete the session
complete: async (sessionId: string): Promise<WorkSession> => {
if (isMockMode()) {
const idx = mockWorkSessions.findIndex((s) => s.id === sessionId);
if (idx === -1) throw new Error("Work session not found");
const now = new Date().toISOString();
mockWorkSessions[idx] = {
...mockWorkSessions[idx],
status: "completed" as WorkSessionStatus,
ended_at: now,
updated_at: now,
};
return mockWorkSessions[idx];
}
const { data } = await api.post<WorkSession>("/work-sessions/" + sessionId + "/complete");
return data;
},
// Abandon the session
abandon: async (sessionId: string, reason?: string): Promise<WorkSession> => {
if (isMockMode()) {
const idx = mockWorkSessions.findIndex((s) => s.id === sessionId);
if (idx === -1) throw new Error("Work session not found");
const now = new Date().toISOString();
mockWorkSessions[idx] = {
...mockWorkSessions[idx],
status: "abandoned" as WorkSessionStatus,
ended_at: now,
updated_at: now,
};
return mockWorkSessions[idx];
}
const params = reason ? `?reason=${encodeURIComponent(reason)}` : "";
const { data } = await api.post<WorkSession>("/work-sessions/" + sessionId + "/abandon" + params);
return data;
},
};
+27
View File
@@ -0,0 +1,27 @@
/**
* Application Constants
*
* Centralized configuration values used across the application.
*/
// CEO agent credentials for control panel operations
// In production, this would come from authentication
export const CEO_AGENT_ID = "00000000-0000-0000-0000-000000000001";
export const CEO_ROLE = "ceo";
// API URLs - relative URLs go through Next.js proxy (avoids CORS)
export const API_URL = process.env.NEXT_PUBLIC_API_URL || "/api";
export const WS_URL = process.env.NEXT_PUBLIC_WS_URL || "/ws";
// Pagination defaults
export const DEFAULT_PAGE_SIZE = 20;
export const MAX_PAGE_SIZE = 100;
// WebSocket settings
export const WS_RECONNECT_INTERVAL = 5000; // Start at 5s, exponential backoff from there
export const WS_MAX_RECONNECT_ATTEMPTS = 3; // Give up after 3 attempts
export const WS_HEARTBEAT_INTERVAL = 30000;
// UI settings
export const STREAM_MAX_MESSAGES = 100;
export const NOTIFICATION_MAX_DISPLAY = 10;
+933
View File
@@ -0,0 +1,933 @@
import {
Task,
TaskStatus,
Team,
Complexity,
AgentRole,
NotificationType,
NotificationPriority,
JournalEntryType,
ChannelType,
SessionStatus,
SessionScope,
MessageType,
FlagSeverity,
TaskNature,
TaskType,
} from "@/types";
// Simple mock ID generator
let _id = 0;
const mockId = () => `mock-${++_id}`;
// Agent IDs - slugs (not UUIDs) except CEO
export const AGENT_IDS = {
// CEO (human) - only one with UUID
ceo: "00000000-0000-0000-0000-000000000001",
// Board/Management (team=null in seed data)
productOwner: "product-owner",
mainPm: "main-pm",
headMarketing: "head-marketing",
auditor: "auditor",
// Backend Cell
bePm: "be-pm",
beDev1: "be-dev-1",
beDev2: "be-dev-2",
beQa: "be-qa",
beDoc: "be-doc",
// Frontend Cell
fePm: "fe-pm",
feDev1: "fe-dev-1",
feDev2: "fe-dev-2",
feQa: "fe-qa",
feDoc: "fe-doc",
// UX/UI Cell
uxPm: "ux-pm",
uxDev1: "ux-dev-1",
uxDev2: "ux-dev-2",
uxQa: "ux-qa",
uxDoc: "ux-doc",
};
export const TASK_IDS = {
task1: "11111111-1111-1111-1111-111111111111",
task2: "22222222-2222-2222-2222-222222222222",
task3: "33333333-3333-3333-3333-333333333333",
task4: "44444444-4444-4444-4444-444444444444",
task5: "55555555-5555-5555-5555-555555555555",
task6: "66666666-6666-6666-6666-666666666666",
};
// Mock project IDs (all tasks require a project for git workflow)
export const PROJECT_IDS = {
roboco: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
robocoPanel: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
};
export const CHANNEL_IDS = {
backendCell: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
frontendCell: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
uxuiCell: "cccccccc-cccc-cccc-cccc-cccccccccccc",
devAll: "dddddddd-dddd-dddd-dddd-dddddddddddd",
qaAll: "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee",
pmAll: "ffffffff-ffff-ffff-ffff-ffffffffffff",
announcements: "00000000-0000-0000-0000-000000000100",
allHands: "00000000-0000-0000-0000-000000000101",
};
// Timestamps - computed dynamically to stay relative
const getNow = () => new Date();
const getMinutesAgo = (mins: number) => new Date(Date.now() - mins * 60 * 1000);
const getHoursAgo = (hours: number) => new Date(Date.now() - hours * 60 * 60 * 1000);
const getDaysAgo = (days: number) => new Date(Date.now() - days * 24 * 60 * 60 * 1000);
// Static references for initial data (these get stale but are used for tasks/etc)
const now = getNow();
const hourAgo = getHoursAgo(1);
const dayAgo = getDaysAgo(1);
const weekAgo = getDaysAgo(7);
// =============================================================================
// MOCK AGENTS - Matching backend seed data exactly
// =============================================================================
interface MockAgent {
id: string;
name: string;
slug: string;
role: AgentRole;
team: Team | null;
}
export const mockAgents: MockAgent[] = [
// Backend Cell
{ id: AGENT_IDS.beDev1, name: "Backend Developer 1", slug: "be-dev-1", role: AgentRole.DEVELOPER, team: Team.BACKEND },
{ id: AGENT_IDS.beDev2, name: "Backend Developer 2", slug: "be-dev-2", role: AgentRole.DEVELOPER, team: Team.BACKEND },
{ id: AGENT_IDS.beQa, name: "Backend QA", slug: "be-qa", role: AgentRole.QA, team: Team.BACKEND },
{ id: AGENT_IDS.bePm, name: "Backend PM", slug: "be-pm", role: AgentRole.CELL_PM, team: Team.BACKEND },
{ id: AGENT_IDS.beDoc, name: "Backend Documenter", slug: "be-doc", role: AgentRole.DOCUMENTER, team: Team.BACKEND },
// Frontend Cell
{ id: AGENT_IDS.feDev1, name: "Frontend Developer 1", slug: "fe-dev-1", role: AgentRole.DEVELOPER, team: Team.FRONTEND },
{ id: AGENT_IDS.feDev2, name: "Frontend Developer 2", slug: "fe-dev-2", role: AgentRole.DEVELOPER, team: Team.FRONTEND },
{ id: AGENT_IDS.feQa, name: "Frontend QA", slug: "fe-qa", role: AgentRole.QA, team: Team.FRONTEND },
{ id: AGENT_IDS.fePm, name: "Frontend PM", slug: "fe-pm", role: AgentRole.CELL_PM, team: Team.FRONTEND },
{ id: AGENT_IDS.feDoc, name: "Frontend Documenter", slug: "fe-doc", role: AgentRole.DOCUMENTER, team: Team.FRONTEND },
// UX/UI Cell (only 1 dev per seed data)
{ id: AGENT_IDS.uxDev1, name: "UX/UI Developer 1", slug: "ux-dev-1", role: AgentRole.DEVELOPER, team: Team.UX_UI },
{ id: AGENT_IDS.uxDev2, name: "UX/UI Developer 2", slug: "ux-dev-2", role: AgentRole.DEVELOPER, team: Team.UX_UI },
{ id: AGENT_IDS.uxQa, name: "UX/UI QA", slug: "ux-qa", role: AgentRole.QA, team: Team.UX_UI },
{ id: AGENT_IDS.uxPm, name: "UX/UI PM", slug: "ux-pm", role: AgentRole.CELL_PM, team: Team.UX_UI },
{ id: AGENT_IDS.uxDoc, name: "UX/UI Documenter", slug: "ux-doc", role: AgentRole.DOCUMENTER, team: Team.UX_UI },
// Board/Management (team=null per seed data)
{ id: AGENT_IDS.mainPm, name: "Main PM", slug: "main-pm", role: AgentRole.MAIN_PM, team: null },
{ id: AGENT_IDS.productOwner, name: "Product Owner", slug: "product-owner", role: AgentRole.PRODUCT_OWNER, team: null },
{ id: AGENT_IDS.headMarketing, name: "Head of Marketing", slug: "head-marketing", role: AgentRole.HEAD_MARKETING, team: null },
{ id: AGENT_IDS.auditor, name: "Auditor", slug: "auditor", role: AgentRole.AUDITOR, team: null },
// CEO (Human - Renzo)
{ id: AGENT_IDS.ceo, name: "Renzo", slug: "ceo", role: AgentRole.CEO, team: null },
];
// =============================================================================
// MOCK TASKS - Full structure matching backend TaskResponse schema
// =============================================================================
export const mockTasks: Task[] = [
{
id: TASK_IDS.task1,
title: "Implement user authentication flow",
description: `## Overview
Implement a complete user authentication system including:
- Login with email/password
- OAuth integration (Google, GitHub)
- Password reset functionality
- Session management
## Technical Requirements
- Use JWT tokens for session management
- Implement refresh token rotation
- Add rate limiting on auth endpoints`,
team: Team.BACKEND,
priority: 1,
sequence: 1,
status: TaskStatus.IN_PROGRESS,
estimated_complexity: Complexity.HIGH,
nature: TaskNature.TECHNICAL,
task_type: TaskType.CODE,
project_id: PROJECT_IDS.roboco,
docs_complete: false,
pr_created: false,
pm_approvals: {},
acceptance_criteria: [
"Users can log in with email/password",
"Users can log in with Google OAuth",
"Password reset emails are sent correctly",
"Sessions expire after 24 hours of inactivity",
],
dependency_ids: [],
blocker_ids: [],
parent_task_id: null,
target_date: null,
created_at: dayAgo.toISOString(),
updated_at: hourAgo.toISOString(),
created_by: AGENT_IDS.productOwner,
assigned_to: AGENT_IDS.beDev1,
claimed_at: dayAgo.toISOString(),
started_at: hourAgo.toISOString(),
completed_at: null,
self_verified: false,
qa_verified: null,
sessions: [],
branch_name: "feature/TASK-001-user-auth",
pr_number: null,
pr_url: null,
plan: {
approach: `## Implementation Strategy
1. **Database Schema**: Create users table with proper indexes
2. **Auth Service**: Implement core auth logic
3. **API Endpoints**: Build REST endpoints
4. **OAuth Integration**: Add Google/GitHub providers
5. **Testing**: Write comprehensive tests`,
sub_tasks: [
{ id: mockId(), title: "Create database migrations", description: null, order: 1, completed: true, estimated_hours: 2, notes: null },
{ id: mockId(), title: "Implement auth service", description: null, order: 2, completed: true, estimated_hours: 4, notes: null },
{ id: mockId(), title: "Build API endpoints", description: null, order: 3, completed: false, estimated_hours: 4, notes: null },
{ id: mockId(), title: "Add OAuth providers", description: null, order: 4, completed: false, estimated_hours: 3, notes: null },
{ id: mockId(), title: "Write tests", description: null, order: 5, completed: false, estimated_hours: 3, notes: null },
],
technical_considerations: [
"Consider using Redis for session storage",
"Implement proper error handling",
"Add request logging for audit trail",
],
risks: [{ description: "OAuth provider API changes", severity: "medium", mitigation: "Use official SDKs and monitor changelogs" }],
open_questions: [{ question: "Should we support 2FA in this iteration?", answer: "No, defer to Phase 2", answered_by: AGENT_IDS.productOwner, answered_at: dayAgo.toISOString() }],
},
progress_updates: [
{ timestamp: hourAgo.toISOString(), message: "Completed database migrations and auth service implementation", agent_id: AGENT_IDS.beDev1, percentage: 40 },
],
checkpoints: [],
commits: [{ hash: "abc123def456", message: "feat: add user authentication schema", timestamp: dayAgo.toISOString(), author_agent_id: AGENT_IDS.beDev1 }],
dev_notes: "Using bcrypt with 12 rounds for password hashing.",
qa_notes: null,
auditor_notes: null,
quick_context: "Currently implementing API endpoints. Auth service is ready.",
},
{
id: TASK_IDS.task2,
title: "Build dashboard components",
description: `Create reusable dashboard components for the CEO command center.
## Components Needed
- Key metrics panel
- Team health cards
- Activity feed
- Quick actions bar`,
team: Team.FRONTEND,
priority: 2,
sequence: 2,
status: TaskStatus.PENDING,
estimated_complexity: Complexity.MEDIUM,
nature: TaskNature.TECHNICAL,
task_type: TaskType.CODE,
project_id: PROJECT_IDS.roboco,
docs_complete: false,
pr_created: false,
pm_approvals: {},
acceptance_criteria: ["All components are responsive", "Components follow design system", "Unit tests cover 80%+ of code"],
dependency_ids: [],
blocker_ids: [],
parent_task_id: TASK_IDS.task1, // Child of "Implement user authentication flow"
target_date: null,
created_at: dayAgo.toISOString(),
updated_at: dayAgo.toISOString(),
created_by: AGENT_IDS.fePm,
assigned_to: null,
claimed_at: null,
started_at: null,
completed_at: null,
self_verified: false,
qa_verified: null,
sessions: [],
branch_name: null,
pr_number: null,
pr_url: null,
plan: null,
progress_updates: [],
checkpoints: [],
commits: [],
dev_notes: null,
qa_notes: null,
auditor_notes: null,
quick_context: null,
},
{
id: TASK_IDS.task3,
title: "Fix pagination bug in task list",
description: `The task list pagination breaks when filtering by status.
## Steps to Reproduce
1. Go to /tasks
2. Filter by "In Progress"
3. Click page 2
4. Filter resets unexpectedly
## Expected Behavior
Pagination should maintain filter state.`,
team: Team.FRONTEND,
priority: 0,
sequence: 3,
status: TaskStatus.CLAIMED,
estimated_complexity: Complexity.LOW,
nature: TaskNature.TECHNICAL,
task_type: TaskType.CODE,
project_id: PROJECT_IDS.roboco,
docs_complete: false,
pr_created: false,
pm_approvals: {},
acceptance_criteria: ["Pagination maintains filter state", "URL params reflect current filters", "No regression in existing functionality"],
dependency_ids: [],
blocker_ids: [],
parent_task_id: TASK_IDS.task1, // Child of "Implement user authentication flow"
target_date: null,
created_at: hourAgo.toISOString(),
updated_at: hourAgo.toISOString(),
created_by: AGENT_IDS.fePm,
assigned_to: AGENT_IDS.feDev1,
claimed_at: hourAgo.toISOString(),
started_at: null,
completed_at: null,
self_verified: false,
qa_verified: null,
sessions: [],
branch_name: "fix/TASK-003-pagination-bug",
pr_number: null,
pr_url: null,
plan: null,
progress_updates: [],
checkpoints: [],
commits: [],
dev_notes: null,
qa_notes: null,
auditor_notes: null,
quick_context: null,
},
{
id: TASK_IDS.task4,
title: "Design new onboarding flow",
description: `Create wireframes and mockups for the new user onboarding experience.
The flow should guide new users through:
- Account setup
- Team configuration
- First task creation`,
team: Team.UX_UI,
priority: 2,
sequence: 4,
status: TaskStatus.AWAITING_QA,
estimated_complexity: Complexity.MEDIUM,
nature: TaskNature.TECHNICAL,
task_type: TaskType.DESIGN,
project_id: PROJECT_IDS.robocoPanel,
docs_complete: false,
pr_created: false,
pm_approvals: {},
acceptance_criteria: ["Wireframes for all screens", "Interactive prototype", "User flow documentation"],
dependency_ids: [],
blocker_ids: [],
parent_task_id: null,
target_date: null,
created_at: dayAgo.toISOString(),
updated_at: hourAgo.toISOString(),
created_by: AGENT_IDS.uxPm,
assigned_to: AGENT_IDS.uxDev1,
claimed_at: dayAgo.toISOString(),
started_at: dayAgo.toISOString(),
completed_at: hourAgo.toISOString(),
self_verified: true,
qa_verified: null,
sessions: [],
branch_name: "feature/TASK-004-onboarding-design",
pr_number: 42,
pr_url: "https://github.com/roboco/roboco/pull/42",
plan: {
approach: "Create lo-fi wireframes first, then hi-fi mockups after stakeholder approval.",
sub_tasks: [
{ id: mockId(), title: "User research", description: null, order: 1, completed: true, estimated_hours: 2, notes: null },
{ id: mockId(), title: "Lo-fi wireframes", description: null, order: 2, completed: true, estimated_hours: 2, notes: null },
{ id: mockId(), title: "Hi-fi mockups", description: null, order: 3, completed: true, estimated_hours: 3, notes: null },
{ id: mockId(), title: "Interactive prototype", description: null, order: 4, completed: true, estimated_hours: 1, notes: null },
],
technical_considerations: [],
risks: [],
open_questions: [],
},
progress_updates: [
{ timestamp: dayAgo.toISOString(), message: "Completed user research and started wireframes", agent_id: AGENT_IDS.uxDev1, percentage: 30 },
{ timestamp: hourAgo.toISOString(), message: "Finished all mockups and prototype, submitting for QA", agent_id: AGENT_IDS.uxDev1, percentage: 100 },
],
checkpoints: [],
commits: [],
dev_notes: "Used Figma for all designs. Prototype link in the team channel.",
qa_notes: null,
auditor_notes: null,
quick_context: "Ready for QA review. All deliverables complete.",
},
{
id: TASK_IDS.task5,
title: "Optimize database queries",
description: `Several API endpoints are slow due to unoptimized queries.
## Affected Endpoints
- GET /tasks (> 500ms)
- GET /dashboard/metrics (> 1s)
## Goal
Reduce response time to < 100ms for all endpoints.`,
team: Team.BACKEND,
priority: 1,
sequence: 5,
status: TaskStatus.BLOCKED,
estimated_complexity: Complexity.HIGH,
nature: TaskNature.TECHNICAL,
task_type: TaskType.CODE,
project_id: PROJECT_IDS.roboco,
docs_complete: false,
pr_created: false,
pm_approvals: {},
acceptance_criteria: ["All listed endpoints respond in < 100ms", "Query execution plans are documented", "No N+1 queries remain"],
dependency_ids: [],
blocker_ids: [TASK_IDS.task1],
parent_task_id: null,
target_date: null,
created_at: dayAgo.toISOString(),
updated_at: hourAgo.toISOString(),
created_by: AGENT_IDS.bePm,
assigned_to: AGENT_IDS.beDev2,
claimed_at: dayAgo.toISOString(),
started_at: dayAgo.toISOString(),
completed_at: null,
self_verified: false,
qa_verified: null,
sessions: [],
branch_name: "perf/TASK-005-db-optimization",
pr_number: null,
pr_url: null,
plan: null,
progress_updates: [{ timestamp: hourAgo.toISOString(), message: "Blocked waiting for auth changes to merge", agent_id: AGENT_IDS.beDev2, percentage: 10 }],
checkpoints: [],
commits: [],
dev_notes: "Need to wait for task #1 to complete before proceeding.",
qa_notes: null,
auditor_notes: null,
quick_context: "Blocked by auth implementation.",
},
{
id: TASK_IDS.task6,
title: "Write API documentation",
description: `Document all public API endpoints using OpenAPI/Swagger.
Include:
- Endpoint descriptions
- Request/response schemas
- Authentication requirements
- Example requests`,
team: Team.BACKEND,
priority: 3,
sequence: 6,
status: TaskStatus.COMPLETED,
estimated_complexity: Complexity.LOW,
nature: TaskNature.TECHNICAL,
task_type: TaskType.DOCUMENTATION,
project_id: PROJECT_IDS.roboco,
docs_complete: true,
pr_created: true,
pm_approvals: { main_pm: true },
acceptance_criteria: ["All endpoints documented", "Swagger UI accessible at /docs", "Examples for each endpoint"],
dependency_ids: [],
blocker_ids: [],
parent_task_id: null,
target_date: null,
created_at: dayAgo.toISOString(),
updated_at: hourAgo.toISOString(),
created_by: AGENT_IDS.bePm,
assigned_to: AGENT_IDS.beDoc,
claimed_at: dayAgo.toISOString(),
started_at: dayAgo.toISOString(),
completed_at: hourAgo.toISOString(),
self_verified: true,
qa_verified: true,
sessions: [],
branch_name: "docs/TASK-006-api-documentation",
pr_number: 38,
pr_url: "https://github.com/roboco/roboco/pull/38",
plan: null,
progress_updates: [],
checkpoints: [],
commits: [{ hash: "def456789abc", message: "docs: add OpenAPI documentation", timestamp: hourAgo.toISOString(), author_agent_id: AGENT_IDS.beDoc }],
dev_notes: "Used FastAPI's built-in OpenAPI support.",
qa_notes: "Documentation is complete and accurate.",
auditor_notes: null,
quick_context: null,
},
];
// =============================================================================
// MOCK DASHBOARD DATA - Matching backend schemas exactly
// =============================================================================
export const mockDashboardStats = {
total_tasks: mockTasks.length,
tasks_in_progress: mockTasks.filter((t) => t.status === TaskStatus.IN_PROGRESS).length,
tasks_blocked: mockTasks.filter((t) => t.status === TaskStatus.BLOCKED).length,
tasks_completed_today: 1,
active_agents: 3,
};
// Team health - matches backend: [BACKEND, FRONTEND, UX_UI, MARKETING, BOARD]
export const mockTeamHealth = [
{ team: Team.BACKEND, status: "ok" as const, active_tasks: 3, blocked_tasks: 1, blocked_ratio: 0.33, completed_this_week: 2 },
{ team: Team.FRONTEND, status: "ok" as const, active_tasks: 2, blocked_tasks: 0, blocked_ratio: 0, completed_this_week: 1 },
{ team: Team.UX_UI, status: "slow" as const, active_tasks: 1, blocked_tasks: 0, blocked_ratio: 0, completed_this_week: 0 },
{ team: Team.MARKETING, status: "ok" as const, active_tasks: 0, blocked_tasks: 0, blocked_ratio: 0, completed_this_week: 0 },
{ team: Team.BOARD, status: "ok" as const, active_tasks: 2, blocked_tasks: 0, blocked_ratio: 0, completed_this_week: 1 },
];
// Recent activity - matching /dashboard/activity/recent response
// Use getter to ensure fresh timestamps on each access
export const getMockRecentActivity = () => [
{ id: mockId(), type: "task_update", timestamp: getMinutesAgo(5).toISOString(), agent_id: AGENT_IDS.beDev1, task_id: TASK_IDS.task1, title: "Implement user authentication flow", status: "in_progress", action: "started" },
{ id: mockId(), type: "task_update", timestamp: getMinutesAgo(15).toISOString(), agent_id: AGENT_IDS.feDev1, task_id: TASK_IDS.task3, title: "Fix pagination bug in task list", status: "claimed", action: "claimed" },
{ id: mockId(), type: "task_update", timestamp: getMinutesAgo(30).toISOString(), agent_id: AGENT_IDS.uxDev1, task_id: TASK_IDS.task4, title: "Design new onboarding flow", status: "awaiting_qa", action: "completed" },
{ id: mockId(), type: "task_update", timestamp: getMinutesAgo(45).toISOString(), agent_id: AGENT_IDS.beDev2, task_id: TASK_IDS.task5, title: "Optimize database queries", status: "blocked", action: "blocked" },
{ id: mockId(), type: "task_update", timestamp: getHoursAgo(2).toISOString(), agent_id: AGENT_IDS.beDoc, task_id: TASK_IDS.task6, title: "Write API documentation", status: "completed", action: "passed_qa" },
];
// Legacy export for backwards compatibility
export const mockRecentActivity = getMockRecentActivity();
// Key metrics - matching /dashboard/ceo response
export const mockKeyMetrics = {
velocity_weekly: 5,
completion_rate: 0.75,
documentation_coverage: 0.82,
active_blockers: 1,
};
// Auditor alerts
export const mockAuditorAlerts = {
urgent_count: 0,
warning_count: 1,
last_report_at: dayAgo.toISOString(),
};
// Roadmap progress
export const mockRoadmapProgress = {
current_quarter_progress: 0.65,
high_priority_total: 10,
high_priority_completed: 6,
};
// =============================================================================
// MOCK ORCHESTRATOR DATA - Matching backend OrchestratorStatusResponse
// =============================================================================
// OrchestratorStatus - matching backend schema exactly
export const mockOrchestratorStatus = {
total_agents: 19,
by_state: {
running: 2,
ready: 1,
waiting_long: 1,
idle: 15,
},
waiting_count: 1,
agents: [
{
agent_id: AGENT_IDS.beDev1,
state: "running",
task_id: TASK_IDS.task1,
error_count: 0,
started_at: hourAgo.toISOString(),
waiting_for: null,
},
{
agent_id: AGENT_IDS.feDev1,
state: "ready",
task_id: TASK_IDS.task3,
error_count: 0,
started_at: hourAgo.toISOString(),
waiting_for: null,
},
{
agent_id: AGENT_IDS.beDev2,
state: "waiting_long",
task_id: TASK_IDS.task5,
error_count: 0,
started_at: dayAgo.toISOString(),
waiting_for: "Waiting for task #1 auth implementation",
},
],
};
export const mockWaitingAgents = [
{ agent_id: AGENT_IDS.beDev2, task_id: TASK_IDS.task5, waiting_for: "Waiting for task #1 auth implementation", waiting_since: hourAgo.toISOString(), context: { blocker_task_id: TASK_IDS.task1 } },
];
// =============================================================================
// MOCK CHANNELS - Matching backend ChannelResponse schema
// =============================================================================
export const mockChannels = [
{ id: CHANNEL_IDS.backendCell, name: "Backend Cell", slug: "backend-cell", type: ChannelType.CELL, description: "Backend development team channel", topic: null, member_count: 6, message_count: 150, group_count: 3, is_archived: false, is_private: false, can_write: true },
{ id: CHANNEL_IDS.frontendCell, name: "Frontend Cell", slug: "frontend-cell", type: ChannelType.CELL, description: "Frontend development team channel", topic: null, member_count: 6, message_count: 120, group_count: 2, is_archived: false, is_private: false, can_write: true },
{ id: CHANNEL_IDS.uxuiCell, name: "UX/UI Cell", slug: "uxui-cell", type: ChannelType.CELL, description: "UX/UI design team channel", topic: null, member_count: 5, message_count: 80, group_count: 2, is_archived: false, is_private: false, can_write: true },
{ id: CHANNEL_IDS.devAll, name: "All Developers", slug: "dev-all", type: ChannelType.CROSS_CELL, description: "Cross-cell developer discussion", topic: null, member_count: 10, message_count: 200, group_count: 5, is_archived: false, is_private: false, can_write: true },
{ id: CHANNEL_IDS.announcements, name: "Announcements", slug: "announcements", type: ChannelType.SPECIAL, description: "Company-wide announcements", topic: null, member_count: 19, message_count: 25, group_count: 1, is_archived: false, is_private: false, can_write: false },
];
// =============================================================================
// MOCK NOTIFICATIONS - Matching backend NotificationResponse schema
// =============================================================================
export const mockNotifications = [
{
id: mockId(),
type: NotificationType.TASK_ASSIGNMENT,
priority: NotificationPriority.NORMAL,
from_agent: AGENT_IDS.bePm,
to_agents: [AGENT_IDS.beDev1],
subject: "New Task Assigned: Implement user authentication flow",
body: "You have been assigned a new high-priority task. Please review and start work.",
requires_ack: false,
is_acknowledged: false,
is_fully_acknowledged: false,
is_read: true,
related_task_id: TASK_IDS.task1,
timestamp: dayAgo.toISOString(),
expires_at: null,
},
{
id: mockId(),
type: NotificationType.BLOCKER_ESCALATION,
priority: NotificationPriority.HIGH,
from_agent: AGENT_IDS.beDev2,
to_agents: [AGENT_IDS.bePm, AGENT_IDS.mainPm],
subject: "Task Blocked: Optimize database queries",
body: "Task is blocked waiting for authentication implementation to complete.",
requires_ack: true,
is_acknowledged: false,
is_fully_acknowledged: false,
is_read: false,
related_task_id: TASK_IDS.task5,
timestamp: hourAgo.toISOString(),
expires_at: null,
},
{
id: mockId(),
type: NotificationType.REVIEW_REQUEST,
priority: NotificationPriority.NORMAL,
from_agent: AGENT_IDS.uxDev1,
to_agents: [AGENT_IDS.uxQa],
subject: "QA Review Requested: Design new onboarding flow",
body: "The onboarding flow design is complete and ready for QA review.",
requires_ack: true,
is_acknowledged: false,
is_fully_acknowledged: false,
is_read: false,
related_task_id: TASK_IDS.task4,
timestamp: hourAgo.toISOString(),
expires_at: null,
},
{
id: mockId(),
type: NotificationType.BROADCAST,
priority: NotificationPriority.NORMAL,
from_agent: AGENT_IDS.mainPm,
to_agents: mockAgents.map(a => a.id),
subject: "Weekly Standup Tomorrow",
body: "Reminder: Weekly standup meeting tomorrow at 10:00 AM.",
requires_ack: false,
is_acknowledged: false,
is_fully_acknowledged: false,
is_read: false,
related_task_id: null,
timestamp: hourAgo.toISOString(),
expires_at: null,
},
];
// =============================================================================
// MOCK JOURNALS - Matching backend JournalResponse/JournalEntryResponse schemas
// =============================================================================
export const mockJournals = [
{
id: mockId(),
agent_id: AGENT_IDS.beDev1,
total_entries: 5,
last_entry_at: hourAgo.toISOString(),
latest_summary: "Working on auth implementation. Good progress on database schema and service layer.",
summary_updated_at: hourAgo.toISOString(),
entries_by_type: { [JournalEntryType.TASK_REFLECTION]: 2, [JournalEntryType.DECISION_LOG]: 1, [JournalEntryType.LEARNING]: 2 },
created_at: weekAgo.toISOString(),
updated_at: hourAgo.toISOString(),
},
];
export const mockJournalEntries = [
{
id: mockId(),
journal_id: mockJournals[0].id,
type: JournalEntryType.TASK_REFLECTION,
title: "Auth Implementation Progress",
content: "Completed the database migrations and basic auth service. JWT implementation is clean and follows best practices.",
task_id: TASK_IDS.task1,
session_id: null,
timestamp: hourAgo.toISOString(),
tags: ["auth", "database", "jwt"],
sentiment: "positive",
is_private: false,
created_at: hourAgo.toISOString(),
updated_at: null,
},
{
id: mockId(),
journal_id: mockJournals[0].id,
type: JournalEntryType.DECISION_LOG,
title: "Chose bcrypt over argon2",
content: "Decided to use bcrypt for password hashing. While argon2 is newer, bcrypt is more widely supported and battle-tested.",
task_id: TASK_IDS.task1,
session_id: null,
timestamp: dayAgo.toISOString(),
tags: ["auth", "security", "decision"],
sentiment: "neutral",
is_private: false,
created_at: dayAgo.toISOString(),
updated_at: null,
},
];
// =============================================================================
// MOCK KANBAN BOARDS - Matching backend KanbanBoard schema
// =============================================================================
export const mockKanbanDevBoard = {
id: mockId(),
title: "Backend Development",
board_type: "dev",
team: Team.BACKEND,
columns: [
{ id: mockId(), title: "Backlog", status: TaskStatus.PENDING, cards: [{ id: TASK_IDS.task2, title: "Build dashboard components", priority: 2, status: TaskStatus.PENDING, assignee_name: null, is_blocked: false }], card_count: 1 },
{ id: mockId(), title: "In Progress", status: TaskStatus.IN_PROGRESS, cards: [{ id: TASK_IDS.task1, title: "Implement user authentication flow", priority: 1, status: TaskStatus.IN_PROGRESS, assignee_name: "Backend Developer 1", is_blocked: false }], card_count: 1 },
{ id: mockId(), title: "Blocked", status: TaskStatus.BLOCKED, cards: [{ id: TASK_IDS.task5, title: "Optimize database queries", priority: 1, status: TaskStatus.BLOCKED, assignee_name: "Backend Developer 2", is_blocked: true }], card_count: 1 },
{ id: mockId(), title: "Done", status: TaskStatus.COMPLETED, cards: [{ id: TASK_IDS.task6, title: "Write API documentation", priority: 3, status: TaskStatus.COMPLETED, assignee_name: "Backend Documenter", is_blocked: false }], card_count: 1 },
],
total_cards: 4,
blocked_count: 1,
};
// =============================================================================
// MOCK SESSIONS - Matching backend SessionResponse schema
// =============================================================================
export const mockSessions = [
{
id: mockId(),
group_id: mockId(),
status: SessionStatus.ACTIVE,
scope: SessionScope.TASK,
message_count: 25,
total_content_length: 5000,
started_at: hourAgo.toISOString(),
last_activity_at: now.toISOString(),
closed_at: null,
},
{
id: mockId(),
group_id: mockId(),
status: SessionStatus.CLOSED,
scope: SessionScope.CELL,
message_count: 50,
total_content_length: 12000,
started_at: dayAgo.toISOString(),
last_activity_at: new Date(now.getTime() - 2 * 60 * 60 * 1000).toISOString(),
closed_at: new Date(now.getTime() - 2 * 60 * 60 * 1000).toISOString(),
},
];
// =============================================================================
// MOCK MESSAGES - Matching backend MessageResponse schema
// =============================================================================
const messageGroupId = mockId();
// Use getter for fresh timestamps
export const getMockMessages = () => [
{
id: mockId(),
agent_id: AGENT_IDS.beDev1,
channel_id: CHANNEL_IDS.backendCell,
group_id: messageGroupId,
session_id: mockSessions[0].id,
type: MessageType.DIALOGUE,
content: "Just finished the auth service implementation. Ready to start on the API endpoints.",
content_length: 78,
is_reply: false,
reply_to: null,
mentions: [],
task_id: TASK_IDS.task1,
commit_ref: null,
timestamp: getMinutesAgo(60).toISOString(),
edited_at: null,
was_edited: false,
},
{
id: mockId(),
agent_id: AGENT_IDS.bePm,
channel_id: CHANNEL_IDS.backendCell,
group_id: messageGroupId,
session_id: mockSessions[0].id,
type: MessageType.DECISION,
content: "Great progress! Let's prioritize the OAuth integration next.",
content_length: 58,
is_reply: true,
reply_to: null,
mentions: [AGENT_IDS.beDev1],
task_id: TASK_IDS.task1,
commit_ref: null,
timestamp: getMinutesAgo(55).toISOString(),
edited_at: null,
was_edited: false,
},
{
id: mockId(),
agent_id: AGENT_IDS.beDev2,
channel_id: CHANNEL_IDS.backendCell,
group_id: messageGroupId,
session_id: mockSessions[0].id,
type: MessageType.BLOCKER,
content: "I'm blocked on the database optimization task. Need the auth changes to merge first.",
content_length: 82,
is_reply: false,
reply_to: null,
mentions: [AGENT_IDS.beDev1],
task_id: TASK_IDS.task5,
commit_ref: null,
timestamp: getMinutesAgo(50).toISOString(),
edited_at: null,
was_edited: false,
},
{
id: mockId(),
agent_id: AGENT_IDS.beDev1,
channel_id: CHANNEL_IDS.backendCell,
group_id: messageGroupId,
session_id: mockSessions[0].id,
type: MessageType.TECHNICAL,
content: "Commit pushed: feat(auth): add user authentication schema",
content_length: 55,
is_reply: false,
reply_to: null,
mentions: [],
task_id: TASK_IDS.task1,
commit_ref: "abc123def456",
timestamp: getMinutesAgo(45).toISOString(),
edited_at: null,
was_edited: false,
},
];
// Legacy export for backwards compatibility
export const mockMessages = getMockMessages();
// =============================================================================
// MOCK GROUPS - Matching backend GroupResponse schema
// =============================================================================
export const mockGroups = [
{
id: mockId(),
name: "General Discussion",
hierarchy_level: 0,
is_active: true,
total_messages: 150,
active_session_id: mockSessions[0].id,
},
{
id: mockId(),
name: "Tech Talk",
hierarchy_level: 1,
is_active: true,
total_messages: 80,
active_session_id: null,
},
];
// =============================================================================
// MOCK AUDITOR DATA - Matching backend dashboard.py schemas
// =============================================================================
export const mockAuditorFlags = [
{
id: mockId(),
severity: FlagSeverity.WARNING,
category: "blocked",
title: "Task blocked for extended period",
description: "Task 'Optimize database queries' has been blocked for over 24 hours.",
related_task_id: TASK_IDS.task5,
related_agent_id: AGENT_IDS.beDev2,
created_at: hourAgo.toISOString(),
resolved_at: null,
notes: null,
},
{
id: mockId(),
severity: FlagSeverity.INFO,
category: "process",
title: "QA queue growing",
description: "Multiple tasks awaiting QA review. Consider allocating additional QA resources.",
related_task_id: null,
related_agent_id: null,
created_at: dayAgo.toISOString(),
resolved_at: null,
notes: null,
},
];
export const mockAuditorReports = [
{
id: mockId(),
report_type: "daily",
title: "Daily Status Report - " + now.toLocaleDateString(),
summary: "Overall team productivity is good. One blocker identified in backend team.",
sections: [
{ title: "Completed Tasks", content: "1 task completed (API documentation)" },
{ title: "In Progress", content: "2 tasks in progress (auth, bug fix)" },
{ title: "Blockers", content: "1 task blocked (database optimization)" },
],
created_at: hourAgo.toISOString(),
sent_at: null,
},
];
export const mockAuditorDashboard = {
live_feeds: [
{ id: CHANNEL_IDS.backendCell, name: "Backend Cell", status: "streaming", last_activity: now.toISOString(), message_count_24h: 25 },
{ id: CHANNEL_IDS.frontendCell, name: "Frontend Cell", status: "idle", last_activity: hourAgo.toISOString(), message_count_24h: 15 },
{ id: CHANNEL_IDS.uxuiCell, name: "UX/UI Cell", status: "idle", last_activity: dayAgo.toISOString(), message_count_24h: 5 },
],
flagged_items: mockAuditorFlags,
metrics: {
total_flags: 2,
unresolved_flags: 2,
tasks_reviewed_today: 1,
avg_review_time_hours: 2.5,
},
audit_queue: [
{ type: "qa_review", title: "Design new onboarding flow", task_id: TASK_IDS.task4, team: Team.UX_UI },
],
recent_reports: mockAuditorReports,
};
// Mock mode: dev = mock, production = real backend
export const isMockMode = () => process.env.NODE_ENV === "development";
+1
View File
@@ -0,0 +1 @@
export { useUIStore } from "./ui-store";
+112
View File
@@ -0,0 +1,112 @@
/**
* UI State Store
*
* Persists UI state across navigation using Zustand with sessionStorage.
* This handles state that doesn't belong in URL params but should survive navigation.
*/
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
interface ScrollPosition {
x: number;
y: number;
}
interface UIState {
// Scroll positions per route
scrollPositions: Record<string, ScrollPosition>;
// Expanded/collapsed state for accordions, cards, etc.
expandedSections: Record<string, boolean>;
// Selected items that aren't in URL (e.g., multi-select temporary state)
selectedItems: Record<string, string[]>;
// Last visited routes per section (for "back" behavior)
lastVisited: Record<string, string>;
// Actions
setScrollPosition: (route: string, position: ScrollPosition) => void;
getScrollPosition: (route: string) => ScrollPosition | undefined;
toggleSection: (sectionId: string) => void;
setSectionExpanded: (sectionId: string, expanded: boolean) => void;
isSectionExpanded: (sectionId: string) => boolean;
setSelectedItems: (key: string, items: string[]) => void;
getSelectedItems: (key: string) => string[];
clearSelectedItems: (key: string) => void;
setLastVisited: (section: string, route: string) => void;
getLastVisited: (section: string) => string | undefined;
}
export const useUIStore = create<UIState>()(
persist(
(set, get) => ({
scrollPositions: {},
expandedSections: {},
selectedItems: {},
lastVisited: {},
// Scroll position management
setScrollPosition: (route, position) =>
set((state) => ({
scrollPositions: { ...state.scrollPositions, [route]: position },
})),
getScrollPosition: (route) => get().scrollPositions[route],
// Section expansion management
toggleSection: (sectionId) =>
set((state) => ({
expandedSections: {
...state.expandedSections,
[sectionId]: !state.expandedSections[sectionId],
},
})),
setSectionExpanded: (sectionId, expanded) =>
set((state) => ({
expandedSections: { ...state.expandedSections, [sectionId]: expanded },
})),
isSectionExpanded: (sectionId) => get().expandedSections[sectionId] ?? true,
// Selected items management
setSelectedItems: (key, items) =>
set((state) => ({
selectedItems: { ...state.selectedItems, [key]: items },
})),
getSelectedItems: (key) => get().selectedItems[key] ?? [],
clearSelectedItems: (key) =>
set((state) => {
const { [key]: _removed, ...rest } = state.selectedItems;
void _removed; // Intentionally discarded
return { selectedItems: rest };
}),
// Last visited route management
setLastVisited: (section, route) =>
set((state) => ({
lastVisited: { ...state.lastVisited, [section]: route },
})),
getLastVisited: (section) => get().lastVisited[section],
}),
{
name: "roboco-ui-state",
storage: createJSONStorage(() => sessionStorage),
// Only persist certain keys
partialize: (state) => ({
scrollPositions: state.scrollPositions,
expandedSections: state.expandedSections,
lastVisited: state.lastVisited,
// Don't persist selectedItems - they're temporary
}),
}
)
);
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+192
View File
@@ -0,0 +1,192 @@
/**
* WebSocket Connection Manager
*
* Handles WebSocket connections with auto-reconnect, heartbeat,
* and event-based message handling.
*/
import {
WS_URL,
WS_RECONNECT_INTERVAL,
WS_MAX_RECONNECT_ATTEMPTS,
WS_HEARTBEAT_INTERVAL,
} from "@/lib/constants";
export type MessageHandler = (data: unknown) => void;
export type ConnectionState = "connecting" | "connected" | "disconnected" | "reconnecting";
export interface WebSocketOptions {
url: string;
onMessage?: MessageHandler;
onStateChange?: (state: ConnectionState) => void;
reconnectInterval?: number;
maxReconnectAttempts?: number;
heartbeatInterval?: number;
}
export class WebSocketConnection {
private ws: WebSocket | null = null;
private url: string;
private onMessage?: MessageHandler;
private onStateChange?: (state: ConnectionState) => void;
private reconnectInterval: number;
private maxReconnectAttempts: number;
private heartbeatInterval: number;
private reconnectAttempts = 0;
private reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
private heartbeatTimeout: ReturnType<typeof setInterval> | null = null;
private state: ConnectionState = "disconnected";
private manualClose = false;
constructor(options: WebSocketOptions) {
this.url = options.url;
this.onMessage = options.onMessage;
this.onStateChange = options.onStateChange;
this.reconnectInterval = options.reconnectInterval || WS_RECONNECT_INTERVAL;
this.maxReconnectAttempts = options.maxReconnectAttempts || WS_MAX_RECONNECT_ATTEMPTS;
this.heartbeatInterval = options.heartbeatInterval || WS_HEARTBEAT_INTERVAL;
}
private setState(state: ConnectionState): void {
this.state = state;
this.onStateChange?.(state);
}
connect(): void {
if (this.ws?.readyState === WebSocket.OPEN) {
return;
}
this.manualClose = false;
this.setState("connecting");
try {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.setState("connected");
this.reconnectAttempts = 0;
this.startHeartbeat();
};
this.ws.onmessage = (event) => {
try {
// Handle pong responses
if (event.data === "pong") {
return;
}
const data = JSON.parse(event.data);
this.onMessage?.(data);
} catch {
// Silently ignore parse errors
}
};
this.ws.onclose = (event) => {
this.stopHeartbeat();
// Don't reconnect if manually closed or max attempts reached
// Also stop if we're getting resource errors (code 1006 with no clean close)
const shouldReconnect = !this.manualClose &&
this.reconnectAttempts < this.maxReconnectAttempts &&
event.code !== 1008 && // Policy violation
event.code !== 1011; // Server error
if (shouldReconnect) {
this.setState("reconnecting");
this.scheduleReconnect();
} else {
this.setState("disconnected");
}
};
this.ws.onerror = () => {
// WebSocket errors are expected when backend is offline
// Don't log - the onclose handler will manage reconnection
// Increment attempts on error to prevent infinite loops
this.reconnectAttempts++;
};
} catch {
// Connection failed - backend likely offline
this.setState("disconnected");
}
}
disconnect(): void {
this.manualClose = true;
this.stopHeartbeat();
this.clearReconnectTimeout();
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.setState("disconnected");
}
send(data: string | object): void {
if (this.ws?.readyState === WebSocket.OPEN) {
const message = typeof data === "string" ? data : JSON.stringify(data);
this.ws.send(message);
}
}
getState(): ConnectionState {
return this.state;
}
private startHeartbeat(): void {
this.stopHeartbeat();
this.heartbeatTimeout = setInterval(() => {
this.send("ping");
}, this.heartbeatInterval);
}
private stopHeartbeat(): void {
if (this.heartbeatTimeout) {
clearInterval(this.heartbeatTimeout);
this.heartbeatTimeout = null;
}
}
private scheduleReconnect(): void {
this.clearReconnectTimeout();
const delay = this.reconnectInterval * Math.pow(1.5, this.reconnectAttempts);
this.reconnectAttempts++;
this.reconnectTimeout = setTimeout(() => {
this.connect();
}, delay);
}
private clearReconnectTimeout(): void {
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = null;
}
}
}
/**
* Get WebSocket base URL from environment or construct from current location
*/
export function getWebSocketUrl(): string {
// If we have an absolute WS URL configured, use it
if (WS_URL.startsWith("ws://") || WS_URL.startsWith("wss://")) {
return WS_URL;
}
// For relative URLs, construct absolute WebSocket URL from current location
if (typeof window !== "undefined") {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const host = window.location.host;
const path = WS_URL.startsWith("/") ? WS_URL : `/${WS_URL}`;
return `${protocol}//${host}${path}`;
}
// Fallback for SSR
return WS_URL;
}