forked from cardosofelipe/pragma-stack
Compare commits
6 Commits
15d747eb28
...
f0b04d53af
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0b04d53af | ||
|
|
35af7daf90 | ||
|
|
5fab15a11e | ||
|
|
ab913575e1 | ||
|
|
82cb6386a6 | ||
|
|
2d05035c1d |
@@ -2,14 +2,14 @@
|
||||
|
||||
Revision ID: 0004
|
||||
Revises: 0003
|
||||
Create Date: 2025-12-30
|
||||
Create Date: 2025-12-31
|
||||
|
||||
This migration creates the core Syndarix domain tables:
|
||||
- projects: Client engagement projects
|
||||
- agent_types: Agent template configurations
|
||||
- agent_instances: Spawned agent instances assigned to projects
|
||||
- issues: Work items (stories, tasks, bugs)
|
||||
- sprints: Sprint containers for issues
|
||||
- issues: Work items (epics, stories, tasks, bugs)
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
@@ -28,7 +28,9 @@ depends_on: str | Sequence[str] | None = None
|
||||
def upgrade() -> None:
|
||||
"""Create Syndarix domain tables."""
|
||||
|
||||
# Create ENUM types first
|
||||
# =========================================================================
|
||||
# Create ENUM types
|
||||
# =========================================================================
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TYPE autonomy_level AS ENUM (
|
||||
@@ -64,24 +66,24 @@ def upgrade() -> None:
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TYPE issue_type AS ENUM (
|
||||
'epic', 'story', 'task', 'bug'
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TYPE issue_status AS ENUM (
|
||||
'open', 'in_progress', 'in_review', 'closed', 'blocked'
|
||||
'open', 'in_progress', 'in_review', 'blocked', 'closed'
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TYPE issue_priority AS ENUM (
|
||||
'critical', 'high', 'medium', 'low'
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TYPE external_tracker_type AS ENUM (
|
||||
'gitea', 'github', 'gitlab', 'jira'
|
||||
'low', 'medium', 'high', 'critical'
|
||||
)
|
||||
"""
|
||||
)
|
||||
@@ -95,12 +97,14 @@ def upgrade() -> None:
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TYPE sprint_status AS ENUM (
|
||||
'planned', 'active', 'completed', 'cancelled'
|
||||
'planned', 'active', 'in_review', 'completed', 'cancelled'
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# Create projects table
|
||||
# =========================================================================
|
||||
op.create_table(
|
||||
"projects",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||
@@ -152,7 +156,10 @@ def upgrade() -> None:
|
||||
server_default="auto",
|
||||
),
|
||||
sa.Column(
|
||||
"settings", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default="{}"
|
||||
"settings",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
nullable=False,
|
||||
server_default="{}",
|
||||
),
|
||||
sa.Column("owner_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column(
|
||||
@@ -168,11 +175,10 @@ def upgrade() -> None:
|
||||
server_default=sa.text("now()"),
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.ForeignKeyConstraint(
|
||||
["owner_id"], ["users.id"], ondelete="SET NULL"
|
||||
),
|
||||
sa.ForeignKeyConstraint(["owner_id"], ["users.id"], ondelete="SET NULL"),
|
||||
sa.UniqueConstraint("slug"),
|
||||
)
|
||||
# Single column indexes
|
||||
op.create_index("ix_projects_name", "projects", ["name"])
|
||||
op.create_index("ix_projects_slug", "projects", ["slug"])
|
||||
op.create_index("ix_projects_status", "projects", ["status"])
|
||||
@@ -180,6 +186,7 @@ def upgrade() -> None:
|
||||
op.create_index("ix_projects_complexity", "projects", ["complexity"])
|
||||
op.create_index("ix_projects_client_mode", "projects", ["client_mode"])
|
||||
op.create_index("ix_projects_owner_id", "projects", ["owner_id"])
|
||||
# Composite indexes
|
||||
op.create_index("ix_projects_slug_status", "projects", ["slug", "status"])
|
||||
op.create_index("ix_projects_owner_status", "projects", ["owner_id", "status"])
|
||||
op.create_index(
|
||||
@@ -189,13 +196,25 @@ def upgrade() -> None:
|
||||
"ix_projects_complexity_status", "projects", ["complexity", "status"]
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# Create agent_types table
|
||||
# =========================================================================
|
||||
op.create_table(
|
||||
"agent_types",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column("name", sa.String(100), nullable=False),
|
||||
sa.Column("slug", sa.String(100), nullable=False),
|
||||
sa.Column("name", sa.String(255), nullable=False),
|
||||
sa.Column("slug", sa.String(255), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
# Areas of expertise (e.g., ["python", "fastapi", "databases"])
|
||||
sa.Column(
|
||||
"expertise",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
nullable=False,
|
||||
server_default="[]",
|
||||
),
|
||||
# System prompt defining personality and behavior (required)
|
||||
sa.Column("personality_prompt", sa.Text(), nullable=False),
|
||||
# LLM model configuration
|
||||
sa.Column("primary_model", sa.String(100), nullable=False),
|
||||
sa.Column(
|
||||
"fallback_models",
|
||||
@@ -203,16 +222,23 @@ def upgrade() -> None:
|
||||
nullable=False,
|
||||
server_default="[]",
|
||||
),
|
||||
sa.Column("system_prompt", sa.Text(), nullable=True),
|
||||
sa.Column("personality_prompt", sa.Text(), nullable=True),
|
||||
# Model parameters (temperature, max_tokens, etc.)
|
||||
sa.Column(
|
||||
"capabilities",
|
||||
"model_params",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
nullable=False,
|
||||
server_default="{}",
|
||||
),
|
||||
# MCP servers this agent can connect to
|
||||
sa.Column(
|
||||
"mcp_servers",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
nullable=False,
|
||||
server_default="[]",
|
||||
),
|
||||
# Tool permissions configuration
|
||||
sa.Column(
|
||||
"default_config",
|
||||
"tool_permissions",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
nullable=False,
|
||||
server_default="{}",
|
||||
@@ -233,12 +259,17 @@ def upgrade() -> None:
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("slug"),
|
||||
)
|
||||
# Single column indexes
|
||||
op.create_index("ix_agent_types_name", "agent_types", ["name"])
|
||||
op.create_index("ix_agent_types_slug", "agent_types", ["slug"])
|
||||
op.create_index("ix_agent_types_is_active", "agent_types", ["is_active"])
|
||||
op.create_index("ix_agent_types_primary_model", "agent_types", ["primary_model"])
|
||||
# Composite indexes
|
||||
op.create_index("ix_agent_types_slug_active", "agent_types", ["slug", "is_active"])
|
||||
op.create_index("ix_agent_types_name_active", "agent_types", ["name", "is_active"])
|
||||
|
||||
# =========================================================================
|
||||
# Create agent_instances table
|
||||
# =========================================================================
|
||||
op.create_table(
|
||||
"agent_instances",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||
@@ -260,17 +291,28 @@ def upgrade() -> None:
|
||||
server_default="idle",
|
||||
),
|
||||
sa.Column("current_task", sa.Text(), nullable=True),
|
||||
# Short-term memory (conversation context, recent decisions)
|
||||
sa.Column(
|
||||
"config_overrides",
|
||||
"short_term_memory",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
nullable=False,
|
||||
server_default="{}",
|
||||
),
|
||||
# Reference to long-term memory in vector store
|
||||
sa.Column("long_term_memory_ref", sa.String(500), nullable=True),
|
||||
# Session ID for active MCP connections
|
||||
sa.Column("session_id", sa.String(255), nullable=True),
|
||||
# Activity tracking
|
||||
sa.Column("last_activity_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("terminated_at", sa.DateTime(timezone=True), nullable=True),
|
||||
# Usage metrics
|
||||
sa.Column("tasks_completed", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("tokens_used", sa.BigInteger(), nullable=False, server_default="0"),
|
||||
sa.Column(
|
||||
"metadata",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
"cost_incurred",
|
||||
sa.Numeric(precision=10, scale=4),
|
||||
nullable=False,
|
||||
server_default="{}",
|
||||
server_default="0",
|
||||
),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
@@ -290,12 +332,21 @@ def upgrade() -> None:
|
||||
),
|
||||
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"),
|
||||
)
|
||||
# Single column indexes
|
||||
op.create_index("ix_agent_instances_name", "agent_instances", ["name"])
|
||||
op.create_index("ix_agent_instances_status", "agent_instances", ["status"])
|
||||
op.create_index(
|
||||
"ix_agent_instances_agent_type_id", "agent_instances", ["agent_type_id"]
|
||||
)
|
||||
op.create_index("ix_agent_instances_project_id", "agent_instances", ["project_id"])
|
||||
op.create_index("ix_agent_instances_session_id", "agent_instances", ["session_id"])
|
||||
op.create_index(
|
||||
"ix_agent_instances_last_activity_at", "agent_instances", ["last_activity_at"]
|
||||
)
|
||||
op.create_index(
|
||||
"ix_agent_instances_terminated_at", "agent_instances", ["terminated_at"]
|
||||
)
|
||||
# Composite indexes
|
||||
op.create_index(
|
||||
"ix_agent_instances_project_status",
|
||||
"agent_instances",
|
||||
@@ -306,22 +357,30 @@ def upgrade() -> None:
|
||||
"agent_instances",
|
||||
["agent_type_id", "status"],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_agent_instances_project_type",
|
||||
"agent_instances",
|
||||
["project_id", "agent_type_id"],
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# Create sprints table (before issues for FK reference)
|
||||
# =========================================================================
|
||||
op.create_table(
|
||||
"sprints",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column("name", sa.String(100), nullable=False),
|
||||
sa.Column("name", sa.String(255), nullable=False),
|
||||
sa.Column("number", sa.Integer(), nullable=False),
|
||||
sa.Column("goal", sa.Text(), nullable=True),
|
||||
sa.Column("start_date", sa.Date(), nullable=True),
|
||||
sa.Column("end_date", sa.Date(), nullable=True),
|
||||
sa.Column("start_date", sa.Date(), nullable=False),
|
||||
sa.Column("end_date", sa.Date(), nullable=False),
|
||||
sa.Column(
|
||||
"status",
|
||||
sa.Enum(
|
||||
"planned",
|
||||
"active",
|
||||
"in_review",
|
||||
"completed",
|
||||
"cancelled",
|
||||
name="sprint_status",
|
||||
@@ -348,29 +407,53 @@ def upgrade() -> None:
|
||||
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"),
|
||||
sa.UniqueConstraint("project_id", "number", name="uq_sprint_project_number"),
|
||||
)
|
||||
op.create_index("ix_sprints_name", "sprints", ["name"])
|
||||
op.create_index("ix_sprints_number", "sprints", ["number"])
|
||||
op.create_index("ix_sprints_status", "sprints", ["status"])
|
||||
# Single column indexes
|
||||
op.create_index("ix_sprints_project_id", "sprints", ["project_id"])
|
||||
op.create_index("ix_sprints_status", "sprints", ["status"])
|
||||
op.create_index("ix_sprints_start_date", "sprints", ["start_date"])
|
||||
op.create_index("ix_sprints_end_date", "sprints", ["end_date"])
|
||||
# Composite indexes
|
||||
op.create_index("ix_sprints_project_status", "sprints", ["project_id", "status"])
|
||||
op.create_index("ix_sprints_project_number", "sprints", ["project_id", "number"])
|
||||
op.create_index("ix_sprints_date_range", "sprints", ["start_date", "end_date"])
|
||||
|
||||
# =========================================================================
|
||||
# Create issues table
|
||||
# =========================================================================
|
||||
op.create_table(
|
||||
"issues",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column("sprint_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column("assigned_agent_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||
# Parent issue for hierarchy (Epic -> Story -> Task)
|
||||
sa.Column("parent_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||
# Issue type (epic, story, task, bug)
|
||||
sa.Column(
|
||||
"type",
|
||||
sa.Enum(
|
||||
"epic",
|
||||
"story",
|
||||
"task",
|
||||
"bug",
|
||||
name="issue_type",
|
||||
create_type=False,
|
||||
),
|
||||
nullable=False,
|
||||
server_default="task",
|
||||
),
|
||||
# Reporter (who created this issue)
|
||||
sa.Column("reporter_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||
# Issue content
|
||||
sa.Column("title", sa.String(500), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("body", sa.Text(), nullable=False, server_default=""),
|
||||
# Status and priority
|
||||
sa.Column(
|
||||
"status",
|
||||
sa.Enum(
|
||||
"open",
|
||||
"in_progress",
|
||||
"in_review",
|
||||
"closed",
|
||||
"blocked",
|
||||
"closed",
|
||||
name="issue_status",
|
||||
create_type=False,
|
||||
),
|
||||
@@ -380,33 +463,37 @@ def upgrade() -> None:
|
||||
sa.Column(
|
||||
"priority",
|
||||
sa.Enum(
|
||||
"critical", "high", "medium", "low", name="issue_priority", create_type=False
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"critical",
|
||||
name="issue_priority",
|
||||
create_type=False,
|
||||
),
|
||||
nullable=False,
|
||||
server_default="medium",
|
||||
),
|
||||
sa.Column("story_points", sa.Integer(), nullable=True),
|
||||
# Labels for categorization
|
||||
sa.Column(
|
||||
"labels",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
nullable=False,
|
||||
server_default="[]",
|
||||
),
|
||||
sa.Column(
|
||||
"external_tracker",
|
||||
sa.Enum(
|
||||
"gitea",
|
||||
"github",
|
||||
"gitlab",
|
||||
"jira",
|
||||
name="external_tracker_type",
|
||||
create_type=False,
|
||||
),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column("external_id", sa.String(255), nullable=True),
|
||||
sa.Column("external_url", sa.String(2048), nullable=True),
|
||||
sa.Column("external_number", sa.Integer(), nullable=True),
|
||||
# Assignment - agent or human (mutually exclusive)
|
||||
sa.Column("assigned_agent_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column("human_assignee", sa.String(255), nullable=True),
|
||||
# Sprint association
|
||||
sa.Column("sprint_id", postgresql.UUID(as_uuid=True), nullable=True),
|
||||
# Estimation
|
||||
sa.Column("story_points", sa.Integer(), nullable=True),
|
||||
sa.Column("due_date", sa.Date(), nullable=True),
|
||||
# External tracker integration (String for flexibility)
|
||||
sa.Column("external_tracker_type", sa.String(50), nullable=True),
|
||||
sa.Column("external_issue_id", sa.String(255), nullable=True),
|
||||
sa.Column("remote_url", sa.String(1000), nullable=True),
|
||||
sa.Column("external_issue_number", sa.Integer(), nullable=True),
|
||||
# Sync status
|
||||
sa.Column(
|
||||
"sync_status",
|
||||
sa.Enum(
|
||||
@@ -417,15 +504,13 @@ def upgrade() -> None:
|
||||
name="sync_status",
|
||||
create_type=False,
|
||||
),
|
||||
nullable=True,
|
||||
nullable=False,
|
||||
server_default="synced",
|
||||
),
|
||||
sa.Column("last_synced_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column(
|
||||
"metadata",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
nullable=False,
|
||||
server_default="{}",
|
||||
),
|
||||
sa.Column("external_updated_at", sa.DateTime(timezone=True), nullable=True),
|
||||
# Lifecycle
|
||||
sa.Column("closed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
@@ -440,29 +525,43 @@ def upgrade() -> None:
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(
|
||||
["parent_id"], ["issues.id"], ondelete="CASCADE"
|
||||
),
|
||||
sa.ForeignKeyConstraint(["sprint_id"], ["sprints.id"], ondelete="SET NULL"),
|
||||
sa.ForeignKeyConstraint(
|
||||
["assigned_agent_id"], ["agent_instances.id"], ondelete="SET NULL"
|
||||
),
|
||||
)
|
||||
op.create_index("ix_issues_title", "issues", ["title"])
|
||||
# Single column indexes
|
||||
op.create_index("ix_issues_project_id", "issues", ["project_id"])
|
||||
op.create_index("ix_issues_parent_id", "issues", ["parent_id"])
|
||||
op.create_index("ix_issues_type", "issues", ["type"])
|
||||
op.create_index("ix_issues_reporter_id", "issues", ["reporter_id"])
|
||||
op.create_index("ix_issues_status", "issues", ["status"])
|
||||
op.create_index("ix_issues_priority", "issues", ["priority"])
|
||||
op.create_index("ix_issues_project_id", "issues", ["project_id"])
|
||||
op.create_index("ix_issues_sprint_id", "issues", ["sprint_id"])
|
||||
op.create_index("ix_issues_assigned_agent_id", "issues", ["assigned_agent_id"])
|
||||
op.create_index("ix_issues_external_tracker", "issues", ["external_tracker"])
|
||||
op.create_index("ix_issues_human_assignee", "issues", ["human_assignee"])
|
||||
op.create_index("ix_issues_sprint_id", "issues", ["sprint_id"])
|
||||
op.create_index("ix_issues_due_date", "issues", ["due_date"])
|
||||
op.create_index("ix_issues_external_tracker_type", "issues", ["external_tracker_type"])
|
||||
op.create_index("ix_issues_sync_status", "issues", ["sync_status"])
|
||||
op.create_index("ix_issues_closed_at", "issues", ["closed_at"])
|
||||
# Composite indexes
|
||||
op.create_index("ix_issues_project_status", "issues", ["project_id", "status"])
|
||||
op.create_index("ix_issues_project_priority", "issues", ["project_id", "priority"])
|
||||
op.create_index("ix_issues_project_sprint", "issues", ["project_id", "sprint_id"])
|
||||
op.create_index("ix_issues_project_type", "issues", ["project_id", "type"])
|
||||
op.create_index("ix_issues_project_agent", "issues", ["project_id", "assigned_agent_id"])
|
||||
op.create_index(
|
||||
"ix_issues_project_status_priority",
|
||||
"issues",
|
||||
["project_id", "status", "priority"],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_issues_external",
|
||||
"ix_issues_external_tracker_id",
|
||||
"issues",
|
||||
["project_id", "external_tracker", "external_id"],
|
||||
["external_tracker_type", "external_issue_id"],
|
||||
)
|
||||
|
||||
|
||||
@@ -478,9 +577,9 @@ def downgrade() -> None:
|
||||
# Drop ENUM types
|
||||
op.execute("DROP TYPE IF EXISTS sprint_status")
|
||||
op.execute("DROP TYPE IF EXISTS sync_status")
|
||||
op.execute("DROP TYPE IF EXISTS external_tracker_type")
|
||||
op.execute("DROP TYPE IF EXISTS issue_priority")
|
||||
op.execute("DROP TYPE IF EXISTS issue_status")
|
||||
op.execute("DROP TYPE IF EXISTS issue_type")
|
||||
op.execute("DROP TYPE IF EXISTS agent_status")
|
||||
op.execute("DROP TYPE IF EXISTS client_mode")
|
||||
op.execute("DROP TYPE IF EXISTS project_complexity")
|
||||
|
||||
@@ -5,7 +5,7 @@ Sprint model for Syndarix AI consulting platform.
|
||||
A Sprint represents a time-boxed iteration for organizing and delivering work.
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, Date, Enum, ForeignKey, Index, Integer, String, Text
|
||||
from sqlalchemy import Column, Date, Enum, ForeignKey, Index, Integer, String, Text, UniqueConstraint
|
||||
from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
@@ -65,6 +65,8 @@ class Sprint(Base, UUIDMixin, TimestampMixin):
|
||||
Index("ix_sprints_project_status", "project_id", "status"),
|
||||
Index("ix_sprints_project_number", "project_id", "number"),
|
||||
Index("ix_sprints_date_range", "start_date", "end_date"),
|
||||
# Ensure sprint numbers are unique within a project
|
||||
UniqueConstraint("project_id", "number", name="uq_sprint_project_number"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
|
||||
158
frontend/src/components/common/ErrorBoundary.tsx
Normal file
158
frontend/src/components/common/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* Error Boundary Component
|
||||
*
|
||||
* Catches JavaScript errors in child component tree and displays fallback UI.
|
||||
* Prevents the entire app from crashing due to component-level errors.
|
||||
*
|
||||
* @see https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { Component, type ReactNode } from 'react';
|
||||
import { AlertTriangle, RefreshCw } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
/** Child components to render */
|
||||
children: ReactNode;
|
||||
/** Optional fallback component to render on error */
|
||||
fallback?: ReactNode;
|
||||
/** Optional callback when error occurs */
|
||||
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
||||
/** Whether to show reset button (default: true) */
|
||||
showReset?: boolean;
|
||||
}
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Default Fallback Component
|
||||
// ============================================================================
|
||||
|
||||
interface DefaultFallbackProps {
|
||||
error: Error | null;
|
||||
onReset: () => void;
|
||||
showReset: boolean;
|
||||
}
|
||||
|
||||
function DefaultFallback({ error, onReset, showReset }: DefaultFallbackProps) {
|
||||
return (
|
||||
<Card className="m-4 border-destructive/50 bg-destructive/5">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-destructive">
|
||||
<AlertTriangle className="h-5 w-5" aria-hidden="true" />
|
||||
Something went wrong
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
An unexpected error occurred. Please try again or contact support if
|
||||
the problem persists.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{error && (
|
||||
<div className="mb-4 rounded-md bg-muted p-3">
|
||||
<p className="font-mono text-sm text-muted-foreground">
|
||||
{error.message}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{showReset && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onReset}
|
||||
className="gap-2"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" aria-hidden="true" />
|
||||
Try again
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Error Boundary Component
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Error boundary for catching and handling render errors in React components.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <ErrorBoundary onError={(error) => logError(error)}>
|
||||
* <MyComponent />
|
||||
* </ErrorBoundary>
|
||||
* ```
|
||||
*
|
||||
* @example With custom fallback
|
||||
* ```tsx
|
||||
* <ErrorBoundary fallback={<CustomErrorUI />}>
|
||||
* <MyComponent />
|
||||
* </ErrorBoundary>
|
||||
* ```
|
||||
*/
|
||||
export class ErrorBoundary extends Component<
|
||||
ErrorBoundaryProps,
|
||||
ErrorBoundaryState
|
||||
> {
|
||||
constructor(props: ErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
||||
// Log error to console in development
|
||||
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
||||
|
||||
// Call optional error callback
|
||||
this.props.onError?.(error, errorInfo);
|
||||
}
|
||||
|
||||
handleReset = (): void => {
|
||||
this.setState({ hasError: false, error: null });
|
||||
};
|
||||
|
||||
render(): ReactNode {
|
||||
const { hasError, error } = this.state;
|
||||
const { children, fallback, showReset = true } = this.props;
|
||||
|
||||
if (hasError) {
|
||||
if (fallback) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return (
|
||||
<DefaultFallback
|
||||
error={error}
|
||||
onReset={this.handleReset}
|
||||
showReset={showReset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
@@ -1,4 +1,4 @@
|
||||
// Common reusable components
|
||||
// Examples: LoadingSpinner, ErrorBoundary, ConfirmDialog, etc.
|
||||
|
||||
export {};
|
||||
export { ErrorBoundary } from './ErrorBoundary';
|
||||
|
||||
@@ -126,7 +126,7 @@ function AgentListItem({
|
||||
<DropdownMenuItem onClick={() => onAction(agent.id, 'view')}>
|
||||
View Details
|
||||
</DropdownMenuItem>
|
||||
{agent.status === 'active' || agent.status === 'working' ? (
|
||||
{agent.status === 'working' || agent.status === 'waiting' ? (
|
||||
<DropdownMenuItem onClick={() => onAction(agent.id, 'pause')}>
|
||||
Pause Agent
|
||||
</DropdownMenuItem>
|
||||
@@ -196,7 +196,7 @@ export function AgentPanel({
|
||||
}
|
||||
|
||||
const activeAgentCount = agents.filter(
|
||||
(a) => a.status === 'active' || a.status === 'working'
|
||||
(a) => a.status === 'working' || a.status === 'waiting'
|
||||
).length;
|
||||
|
||||
return (
|
||||
|
||||
@@ -14,21 +14,17 @@ const statusConfig: Record<AgentStatus, { color: string; label: string }> = {
|
||||
color: 'bg-yellow-500',
|
||||
label: 'Idle',
|
||||
},
|
||||
active: {
|
||||
color: 'bg-green-500',
|
||||
label: 'Active',
|
||||
},
|
||||
working: {
|
||||
color: 'bg-green-500 animate-pulse',
|
||||
label: 'Working',
|
||||
},
|
||||
pending: {
|
||||
color: 'bg-gray-400',
|
||||
label: 'Pending',
|
||||
waiting: {
|
||||
color: 'bg-blue-500',
|
||||
label: 'Waiting',
|
||||
},
|
||||
error: {
|
||||
color: 'bg-red-500',
|
||||
label: 'Error',
|
||||
paused: {
|
||||
color: 'bg-gray-400',
|
||||
label: 'Paused',
|
||||
},
|
||||
terminated: {
|
||||
color: 'bg-gray-600',
|
||||
@@ -49,7 +45,7 @@ export function AgentStatusIndicator({
|
||||
showLabel = false,
|
||||
className,
|
||||
}: AgentStatusIndicatorProps) {
|
||||
const config = statusConfig[status] || statusConfig.pending;
|
||||
const config = statusConfig[status] || statusConfig.idle;
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'h-2 w-2',
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
} from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import type { IssueSummary as IssueSummaryType } from './types';
|
||||
import type { IssueCountSummary } from './types';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
@@ -32,7 +32,7 @@ import type { IssueSummary as IssueSummaryType } from './types';
|
||||
|
||||
interface IssueSummaryProps {
|
||||
/** Issue summary data */
|
||||
summary: IssueSummaryType | null;
|
||||
summary: IssueCountSummary | null;
|
||||
/** Whether data is loading */
|
||||
isLoading?: boolean;
|
||||
/** Callback when "View All Issues" is clicked */
|
||||
@@ -171,8 +171,8 @@ export function IssueSummary({
|
||||
<StatusRow
|
||||
icon={CheckCircle2}
|
||||
iconColor="text-green-500"
|
||||
label="Completed"
|
||||
count={summary.done}
|
||||
label="Closed"
|
||||
count={summary.closed}
|
||||
/>
|
||||
|
||||
{onViewAllIssues && (
|
||||
|
||||
@@ -28,7 +28,7 @@ import type {
|
||||
AgentInstance,
|
||||
Sprint,
|
||||
BurndownDataPoint,
|
||||
IssueSummary as IssueSummaryType,
|
||||
IssueCountSummary,
|
||||
ActivityItem,
|
||||
} from './types';
|
||||
|
||||
@@ -52,7 +52,7 @@ const mockProject: Project = {
|
||||
id: 'proj-001',
|
||||
name: 'E-Commerce Platform Redesign',
|
||||
description: 'Complete redesign of the e-commerce platform with modern UI/UX',
|
||||
status: 'in_progress',
|
||||
status: 'active',
|
||||
autonomy_level: 'milestone',
|
||||
current_sprint_id: 'sprint-003',
|
||||
created_at: '2025-01-15T00:00:00Z',
|
||||
@@ -66,7 +66,7 @@ const mockAgents: AgentInstance[] = [
|
||||
project_id: 'proj-001',
|
||||
name: 'Product Owner',
|
||||
role: 'product_owner',
|
||||
status: 'active',
|
||||
status: 'working',
|
||||
current_task: 'Reviewing user story acceptance criteria',
|
||||
last_activity_at: new Date(Date.now() - 2 * 60 * 1000).toISOString(),
|
||||
spawned_at: '2025-01-15T00:00:00Z',
|
||||
@@ -102,7 +102,7 @@ const mockAgents: AgentInstance[] = [
|
||||
project_id: 'proj-001',
|
||||
name: 'Frontend Engineer',
|
||||
role: 'frontend_engineer',
|
||||
status: 'active',
|
||||
status: 'working',
|
||||
current_task: 'Implementing product catalog component',
|
||||
last_activity_at: new Date(Date.now() - 1 * 60 * 1000).toISOString(),
|
||||
spawned_at: '2025-01-15T00:00:00Z',
|
||||
@@ -114,7 +114,7 @@ const mockAgents: AgentInstance[] = [
|
||||
project_id: 'proj-001',
|
||||
name: 'QA Engineer',
|
||||
role: 'qa_engineer',
|
||||
status: 'pending',
|
||||
status: 'waiting',
|
||||
current_task: 'Preparing test cases for Sprint 3',
|
||||
last_activity_at: new Date(Date.now() - 30 * 60 * 1000).toISOString(),
|
||||
spawned_at: '2025-01-15T00:00:00Z',
|
||||
@@ -148,12 +148,12 @@ const mockBurndownData: BurndownDataPoint[] = [
|
||||
{ day: 8, remaining: 20, ideal: 24 },
|
||||
];
|
||||
|
||||
const mockIssueSummary: IssueSummaryType = {
|
||||
const mockIssueSummary: IssueCountSummary = {
|
||||
open: 12,
|
||||
in_progress: 8,
|
||||
in_review: 3,
|
||||
blocked: 2,
|
||||
done: 45,
|
||||
closed: 45,
|
||||
total: 70,
|
||||
};
|
||||
|
||||
@@ -392,7 +392,7 @@ export function ProjectDashboard({ projectId, className }: ProjectDashboardProps
|
||||
<ProjectHeader
|
||||
project={project}
|
||||
isLoading={isLoading}
|
||||
canPause={project.status === 'in_progress'}
|
||||
canPause={project.status === 'active'}
|
||||
canStart={true}
|
||||
onStartSprint={handleStartSprint}
|
||||
onPauseProject={handlePauseProject}
|
||||
|
||||
@@ -84,7 +84,7 @@ export function ProjectHeader({
|
||||
return null;
|
||||
}
|
||||
|
||||
const showPauseButton = canPause && project.status === 'in_progress';
|
||||
const showPauseButton = canPause && project.status === 'active';
|
||||
const showStartButton = canStart && project.status !== 'completed' && project.status !== 'archived';
|
||||
|
||||
return (
|
||||
|
||||
@@ -16,12 +16,8 @@ import type { ProjectStatus, AutonomyLevel } from './types';
|
||||
// ============================================================================
|
||||
|
||||
const projectStatusConfig: Record<ProjectStatus, { label: string; className: string }> = {
|
||||
draft: {
|
||||
label: 'Draft',
|
||||
className: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200',
|
||||
},
|
||||
in_progress: {
|
||||
label: 'In Progress',
|
||||
active: {
|
||||
label: 'Active',
|
||||
className: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
|
||||
},
|
||||
paused: {
|
||||
@@ -32,10 +28,6 @@ const projectStatusConfig: Record<ProjectStatus, { label: string; className: str
|
||||
label: 'Completed',
|
||||
className: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||
},
|
||||
blocked: {
|
||||
label: 'Blocked',
|
||||
className: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||||
},
|
||||
archived: {
|
||||
label: 'Archived',
|
||||
className: 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400',
|
||||
@@ -48,7 +40,7 @@ interface ProjectStatusBadgeProps {
|
||||
}
|
||||
|
||||
export function ProjectStatusBadge({ status, className }: ProjectStatusBadgeProps) {
|
||||
const config = projectStatusConfig[status] || projectStatusConfig.draft;
|
||||
const config = projectStatusConfig[status] || projectStatusConfig.active;
|
||||
|
||||
return (
|
||||
<Badge variant="outline" className={cn(config.className, className)}>
|
||||
|
||||
@@ -11,7 +11,10 @@
|
||||
// Project Types
|
||||
// ============================================================================
|
||||
|
||||
export type ProjectStatus = 'draft' | 'in_progress' | 'paused' | 'completed' | 'blocked' | 'archived';
|
||||
/**
|
||||
* Matches backend: ProjectStatus enum in app/models/syndarix/enums.py
|
||||
*/
|
||||
export type ProjectStatus = 'active' | 'paused' | 'completed' | 'archived';
|
||||
|
||||
export type AutonomyLevel = 'full_control' | 'milestone' | 'autonomous';
|
||||
|
||||
@@ -31,7 +34,10 @@ export interface Project {
|
||||
// Agent Types
|
||||
// ============================================================================
|
||||
|
||||
export type AgentStatus = 'idle' | 'active' | 'working' | 'pending' | 'error' | 'terminated';
|
||||
/**
|
||||
* Matches backend: AgentStatus enum in app/models/syndarix/enums.py
|
||||
*/
|
||||
export type AgentStatus = 'idle' | 'working' | 'waiting' | 'paused' | 'terminated';
|
||||
|
||||
export interface AgentInstance {
|
||||
id: string;
|
||||
@@ -50,7 +56,10 @@ export interface AgentInstance {
|
||||
// Sprint Types
|
||||
// ============================================================================
|
||||
|
||||
export type SprintStatus = 'planning' | 'active' | 'review' | 'completed';
|
||||
/**
|
||||
* Matches backend: SprintStatus enum in app/models/syndarix/enums.py
|
||||
*/
|
||||
export type SprintStatus = 'planned' | 'active' | 'in_review' | 'completed' | 'cancelled';
|
||||
|
||||
export interface Sprint {
|
||||
id: string;
|
||||
@@ -78,7 +87,10 @@ export interface BurndownDataPoint {
|
||||
// Issue Types
|
||||
// ============================================================================
|
||||
|
||||
export type IssueStatus = 'open' | 'in_progress' | 'in_review' | 'blocked' | 'done' | 'closed';
|
||||
/**
|
||||
* Matches backend: IssueStatus enum in app/models/syndarix/enums.py
|
||||
*/
|
||||
export type IssueStatus = 'open' | 'in_progress' | 'in_review' | 'blocked' | 'closed';
|
||||
|
||||
export type IssuePriority = 'low' | 'medium' | 'high' | 'critical';
|
||||
|
||||
@@ -96,12 +108,12 @@ export interface Issue {
|
||||
labels?: string[];
|
||||
}
|
||||
|
||||
export interface IssueSummary {
|
||||
export interface IssueCountSummary {
|
||||
open: number;
|
||||
in_progress: number;
|
||||
in_review: number;
|
||||
blocked: number;
|
||||
done: number;
|
||||
closed: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ const STATUS_ICONS = {
|
||||
in_progress: PlayCircle,
|
||||
in_review: Clock,
|
||||
blocked: AlertCircle,
|
||||
done: CheckCircle2,
|
||||
closed: XCircle,
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -22,14 +22,17 @@ export const STATUS_CONFIG: Record<IssueStatus, StatusConfig> = {
|
||||
in_progress: { label: 'In Progress', color: 'text-yellow-500' },
|
||||
in_review: { label: 'In Review', color: 'text-purple-500' },
|
||||
blocked: { label: 'Blocked', color: 'text-red-500' },
|
||||
done: { label: 'Done', color: 'text-green-500' },
|
||||
closed: { label: 'Closed', color: 'text-muted-foreground' },
|
||||
closed: { label: 'Closed', color: 'text-green-500' },
|
||||
};
|
||||
|
||||
/**
|
||||
* Priority configuration with labels and colors
|
||||
*/
|
||||
export const PRIORITY_CONFIG: Record<IssuePriority, PriorityConfig> = {
|
||||
critical: {
|
||||
label: 'Critical',
|
||||
color: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
|
||||
},
|
||||
high: { label: 'High', color: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' },
|
||||
medium: {
|
||||
label: 'Medium',
|
||||
@@ -46,10 +49,9 @@ export const STATUS_TRANSITIONS: StatusTransition[] = [
|
||||
{ from: 'open', to: 'in_progress', label: 'Start Work' },
|
||||
{ from: 'in_progress', to: 'in_review', label: 'Submit for Review' },
|
||||
{ from: 'in_progress', to: 'blocked', label: 'Mark Blocked' },
|
||||
{ from: 'in_review', to: 'done', label: 'Mark Done' },
|
||||
{ from: 'in_review', to: 'closed', label: 'Complete' },
|
||||
{ from: 'in_review', to: 'in_progress', label: 'Request Changes' },
|
||||
{ from: 'blocked', to: 'in_progress', label: 'Unblock' },
|
||||
{ from: 'done', to: 'closed', label: 'Close Issue' },
|
||||
{ from: 'closed', to: 'open', label: 'Reopen' },
|
||||
];
|
||||
|
||||
@@ -76,14 +78,13 @@ export const STATUS_ORDER: IssueStatus[] = [
|
||||
'in_progress',
|
||||
'in_review',
|
||||
'blocked',
|
||||
'done',
|
||||
'closed',
|
||||
];
|
||||
|
||||
/**
|
||||
* All possible priorities in order
|
||||
*/
|
||||
export const PRIORITY_ORDER: IssuePriority[] = ['high', 'medium', 'low'];
|
||||
export const PRIORITY_ORDER: IssuePriority[] = ['critical', 'high', 'medium', 'low'];
|
||||
|
||||
/**
|
||||
* Sync status configuration
|
||||
|
||||
@@ -94,7 +94,7 @@ function filterAndSortIssues(
|
||||
case 'number':
|
||||
return (a.number - b.number) * direction;
|
||||
case 'priority': {
|
||||
const priorityOrder = { high: 3, medium: 2, low: 1 };
|
||||
const priorityOrder = { critical: 4, high: 3, medium: 2, low: 1 };
|
||||
return (priorityOrder[a.priority] - priorityOrder[b.priority]) * direction;
|
||||
}
|
||||
case 'updated_at':
|
||||
|
||||
@@ -16,6 +16,7 @@ export const mockIssues: IssueSummary[] = [
|
||||
{
|
||||
id: 'ISS-001',
|
||||
number: 42,
|
||||
type: 'story',
|
||||
title: 'Implement user authentication flow',
|
||||
description:
|
||||
'Create complete authentication flow with login, register, and password reset.',
|
||||
@@ -31,6 +32,7 @@ export const mockIssues: IssueSummary[] = [
|
||||
{
|
||||
id: 'ISS-002',
|
||||
number: 43,
|
||||
type: 'task',
|
||||
title: 'Design product catalog component',
|
||||
description: 'Create reusable product card and catalog grid components.',
|
||||
status: 'in_review',
|
||||
@@ -45,6 +47,7 @@ export const mockIssues: IssueSummary[] = [
|
||||
{
|
||||
id: 'ISS-003',
|
||||
number: 44,
|
||||
type: 'bug',
|
||||
title: 'Fix cart total calculation bug',
|
||||
description: 'Cart total shows incorrect amount when discount is applied.',
|
||||
status: 'blocked',
|
||||
@@ -60,6 +63,7 @@ export const mockIssues: IssueSummary[] = [
|
||||
{
|
||||
id: 'ISS-004',
|
||||
number: 45,
|
||||
type: 'story',
|
||||
title: 'Add product search functionality',
|
||||
description: 'Implement full-text search with filters for the product catalog.',
|
||||
status: 'open',
|
||||
@@ -74,9 +78,10 @@ export const mockIssues: IssueSummary[] = [
|
||||
{
|
||||
id: 'ISS-005',
|
||||
number: 46,
|
||||
type: 'task',
|
||||
title: 'Optimize database queries for product listing',
|
||||
description: 'Performance optimization for product queries with pagination.',
|
||||
status: 'done',
|
||||
status: 'closed',
|
||||
priority: 'low',
|
||||
labels: ['performance', 'backend', 'database'],
|
||||
sprint: 'Sprint 2',
|
||||
@@ -88,9 +93,10 @@ export const mockIssues: IssueSummary[] = [
|
||||
{
|
||||
id: 'ISS-006',
|
||||
number: 47,
|
||||
type: 'task',
|
||||
title: 'Create checkout page wireframes',
|
||||
description: 'Design wireframes for the checkout flow including payment selection.',
|
||||
status: 'done',
|
||||
status: 'closed',
|
||||
priority: 'high',
|
||||
labels: ['design', 'checkout', 'ui'],
|
||||
sprint: 'Sprint 2',
|
||||
@@ -102,6 +108,7 @@ export const mockIssues: IssueSummary[] = [
|
||||
{
|
||||
id: 'ISS-007',
|
||||
number: 48,
|
||||
type: 'story',
|
||||
title: 'Implement responsive navigation',
|
||||
description: 'Create mobile-friendly navigation with hamburger menu.',
|
||||
status: 'open',
|
||||
@@ -116,6 +123,7 @@ export const mockIssues: IssueSummary[] = [
|
||||
{
|
||||
id: 'ISS-008',
|
||||
number: 49,
|
||||
type: 'task',
|
||||
title: 'Set up E2E test framework',
|
||||
description: 'Configure Playwright for end-to-end testing.',
|
||||
status: 'in_progress',
|
||||
@@ -135,6 +143,7 @@ export const mockIssues: IssueSummary[] = [
|
||||
export const mockIssueDetail: IssueDetail = {
|
||||
id: 'ISS-001',
|
||||
number: 42,
|
||||
type: 'story',
|
||||
title: 'Implement user authentication flow',
|
||||
description: `## Overview
|
||||
Create a complete authentication flow for the e-commerce platform.
|
||||
|
||||
@@ -8,14 +8,22 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Issue status values
|
||||
* Issue type values (for hierarchy: Epic -> Story -> Task)
|
||||
* Matches backend: IssueType enum in app/models/syndarix/enums.py
|
||||
*/
|
||||
export type IssueStatus = 'open' | 'in_progress' | 'in_review' | 'blocked' | 'done' | 'closed';
|
||||
export type IssueType = 'epic' | 'story' | 'task' | 'bug';
|
||||
|
||||
/**
|
||||
* Issue status values
|
||||
* Matches backend: IssueStatus enum in app/models/syndarix/enums.py
|
||||
*/
|
||||
export type IssueStatus = 'open' | 'in_progress' | 'in_review' | 'blocked' | 'closed';
|
||||
|
||||
/**
|
||||
* Issue priority values
|
||||
* Matches backend: IssuePriority enum in app/models/syndarix/enums.py
|
||||
*/
|
||||
export type IssuePriority = 'high' | 'medium' | 'low';
|
||||
export type IssuePriority = 'low' | 'medium' | 'high' | 'critical';
|
||||
|
||||
/**
|
||||
* Sync status with external trackers
|
||||
@@ -64,6 +72,7 @@ export interface IssueActivity {
|
||||
export interface IssueSummary {
|
||||
id: string;
|
||||
number: number;
|
||||
type: IssueType;
|
||||
title: string;
|
||||
description: string;
|
||||
status: IssueStatus;
|
||||
|
||||
@@ -10,7 +10,7 @@ const mockAgents: AgentInstance[] = [
|
||||
project_id: 'proj-001',
|
||||
name: 'Product Owner',
|
||||
role: 'product_owner',
|
||||
status: 'active',
|
||||
status: 'working',
|
||||
current_task: 'Reviewing user stories',
|
||||
last_activity_at: new Date().toISOString(),
|
||||
spawned_at: '2025-01-15T00:00:00Z',
|
||||
|
||||
@@ -8,12 +8,6 @@ describe('AgentStatusIndicator', () => {
|
||||
expect(indicator).toHaveClass('bg-yellow-500');
|
||||
});
|
||||
|
||||
it('renders active status with correct color', () => {
|
||||
const { container } = render(<AgentStatusIndicator status="active" />);
|
||||
const indicator = container.querySelector('span > span');
|
||||
expect(indicator).toHaveClass('bg-green-500');
|
||||
});
|
||||
|
||||
it('renders working status with animation', () => {
|
||||
const { container } = render(<AgentStatusIndicator status="working" />);
|
||||
const indicator = container.querySelector('span > span');
|
||||
@@ -21,16 +15,16 @@ describe('AgentStatusIndicator', () => {
|
||||
expect(indicator).toHaveClass('animate-pulse');
|
||||
});
|
||||
|
||||
it('renders pending status with correct color', () => {
|
||||
const { container } = render(<AgentStatusIndicator status="pending" />);
|
||||
it('renders waiting status with correct color', () => {
|
||||
const { container } = render(<AgentStatusIndicator status="waiting" />);
|
||||
const indicator = container.querySelector('span > span');
|
||||
expect(indicator).toHaveClass('bg-gray-400');
|
||||
expect(indicator).toHaveClass('bg-blue-500');
|
||||
});
|
||||
|
||||
it('renders error status with correct color', () => {
|
||||
const { container } = render(<AgentStatusIndicator status="error" />);
|
||||
it('renders paused status with correct color', () => {
|
||||
const { container } = render(<AgentStatusIndicator status="paused" />);
|
||||
const indicator = container.querySelector('span > span');
|
||||
expect(indicator).toHaveClass('bg-red-500');
|
||||
expect(indicator).toHaveClass('bg-gray-400');
|
||||
});
|
||||
|
||||
it('renders terminated status with correct color', () => {
|
||||
@@ -40,41 +34,41 @@ describe('AgentStatusIndicator', () => {
|
||||
});
|
||||
|
||||
it('applies small size by default', () => {
|
||||
const { container } = render(<AgentStatusIndicator status="active" />);
|
||||
const { container } = render(<AgentStatusIndicator status="working" />);
|
||||
const indicator = container.querySelector('span > span');
|
||||
expect(indicator).toHaveClass('h-2', 'w-2');
|
||||
});
|
||||
|
||||
it('applies medium size when specified', () => {
|
||||
const { container } = render(<AgentStatusIndicator status="active" size="md" />);
|
||||
const { container } = render(<AgentStatusIndicator status="working" size="md" />);
|
||||
const indicator = container.querySelector('span > span');
|
||||
expect(indicator).toHaveClass('h-3', 'w-3');
|
||||
});
|
||||
|
||||
it('applies large size when specified', () => {
|
||||
const { container } = render(<AgentStatusIndicator status="active" size="lg" />);
|
||||
const { container } = render(<AgentStatusIndicator status="working" size="lg" />);
|
||||
const indicator = container.querySelector('span > span');
|
||||
expect(indicator).toHaveClass('h-4', 'w-4');
|
||||
});
|
||||
|
||||
it('shows label when showLabel is true', () => {
|
||||
render(<AgentStatusIndicator status="active" showLabel />);
|
||||
expect(screen.getByText('Active')).toBeInTheDocument();
|
||||
render(<AgentStatusIndicator status="working" showLabel />);
|
||||
expect(screen.getByText('Working')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show label by default', () => {
|
||||
render(<AgentStatusIndicator status="active" />);
|
||||
expect(screen.queryByText('Active')).not.toBeInTheDocument();
|
||||
render(<AgentStatusIndicator status="working" />);
|
||||
expect(screen.queryByText('Working')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('has accessible status role and label', () => {
|
||||
render(<AgentStatusIndicator status="active" />);
|
||||
expect(screen.getByRole('status')).toHaveAttribute('aria-label', 'Status: Active');
|
||||
render(<AgentStatusIndicator status="working" />);
|
||||
expect(screen.getByRole('status')).toHaveAttribute('aria-label', 'Status: Working');
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(
|
||||
<AgentStatusIndicator status="active" className="custom-class" />
|
||||
<AgentStatusIndicator status="working" className="custom-class" />
|
||||
);
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IssueSummary } from '@/components/projects/IssueSummary';
|
||||
import type { IssueSummary as IssueSummaryType } from '@/components/projects/types';
|
||||
import type { IssueCountSummary } from '@/components/projects/types';
|
||||
|
||||
const mockSummary: IssueSummaryType = {
|
||||
const mockSummary: IssueCountSummary = {
|
||||
open: 12,
|
||||
in_progress: 8,
|
||||
in_review: 3,
|
||||
blocked: 2,
|
||||
done: 45,
|
||||
closed: 45,
|
||||
total: 70,
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@ describe('IssueSummary', () => {
|
||||
expect(screen.getByText('Blocked')).toBeInTheDocument();
|
||||
expect(screen.getByText('2')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Completed')).toBeInTheDocument();
|
||||
expect(screen.getByText('Closed')).toBeInTheDocument();
|
||||
expect(screen.getByText('45')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const mockProject: Project = {
|
||||
id: 'proj-001',
|
||||
name: 'Test Project',
|
||||
description: 'A test project for unit testing',
|
||||
status: 'in_progress',
|
||||
status: 'active',
|
||||
autonomy_level: 'milestone',
|
||||
created_at: '2025-01-15T00:00:00Z',
|
||||
owner_id: 'user-001',
|
||||
@@ -26,7 +26,7 @@ describe('ProjectHeader', () => {
|
||||
|
||||
it('renders project status badge', () => {
|
||||
render(<ProjectHeader project={mockProject} />);
|
||||
expect(screen.getByText('In Progress')).toBeInTheDocument();
|
||||
expect(screen.getByText('Active')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders autonomy level badge', () => {
|
||||
@@ -44,7 +44,7 @@ describe('ProjectHeader', () => {
|
||||
expect(container.querySelectorAll('.animate-pulse').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('shows pause button when canPause is true and project is in_progress', () => {
|
||||
it('shows pause button when canPause is true and project is active', () => {
|
||||
const onPauseProject = jest.fn();
|
||||
render(
|
||||
<ProjectHeader
|
||||
@@ -56,7 +56,7 @@ describe('ProjectHeader', () => {
|
||||
expect(screen.getByRole('button', { name: /pause project/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show pause button when project is not in_progress', () => {
|
||||
it('does not show pause button when project is not active', () => {
|
||||
const completedProject = { ...mockProject, status: 'completed' as const };
|
||||
render(<ProjectHeader project={completedProject} canPause={true} />);
|
||||
expect(screen.queryByRole('button', { name: /pause project/i })).not.toBeInTheDocument();
|
||||
|
||||
@@ -2,9 +2,9 @@ import { render, screen } from '@testing-library/react';
|
||||
import { ProjectStatusBadge, AutonomyBadge } from '@/components/projects/StatusBadge';
|
||||
|
||||
describe('ProjectStatusBadge', () => {
|
||||
it('renders in_progress status correctly', () => {
|
||||
render(<ProjectStatusBadge status="in_progress" />);
|
||||
expect(screen.getByText('In Progress')).toBeInTheDocument();
|
||||
it('renders active status correctly', () => {
|
||||
render(<ProjectStatusBadge status="active" />);
|
||||
expect(screen.getByText('Active')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders completed status correctly', () => {
|
||||
@@ -12,21 +12,11 @@ describe('ProjectStatusBadge', () => {
|
||||
expect(screen.getByText('Completed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders blocked status correctly', () => {
|
||||
render(<ProjectStatusBadge status="blocked" />);
|
||||
expect(screen.getByText('Blocked')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders paused status correctly', () => {
|
||||
render(<ProjectStatusBadge status="paused" />);
|
||||
expect(screen.getByText('Paused')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders draft status correctly', () => {
|
||||
render(<ProjectStatusBadge status="draft" />);
|
||||
expect(screen.getByText('Draft')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders archived status correctly', () => {
|
||||
render(<ProjectStatusBadge status="archived" />);
|
||||
expect(screen.getByText('Archived')).toBeInTheDocument();
|
||||
@@ -34,7 +24,7 @@ describe('ProjectStatusBadge', () => {
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(
|
||||
<ProjectStatusBadge status="in_progress" className="custom-class" />
|
||||
<ProjectStatusBadge status="active" className="custom-class" />
|
||||
);
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ const mockIssues: IssueSummary[] = [
|
||||
{
|
||||
id: 'issue-1',
|
||||
number: 42,
|
||||
type: 'bug',
|
||||
title: 'Test Issue 1',
|
||||
description: 'Description 1',
|
||||
status: 'open',
|
||||
@@ -25,6 +26,7 @@ const mockIssues: IssueSummary[] = [
|
||||
{
|
||||
id: 'issue-2',
|
||||
number: 43,
|
||||
type: 'story',
|
||||
title: 'Test Issue 2',
|
||||
description: 'Description 2',
|
||||
status: 'in_progress',
|
||||
|
||||
@@ -11,12 +11,11 @@ const statusLabels: Record<IssueStatus, string> = {
|
||||
in_progress: 'In Progress',
|
||||
in_review: 'In Review',
|
||||
blocked: 'Blocked',
|
||||
done: 'Done',
|
||||
closed: 'Closed',
|
||||
};
|
||||
|
||||
describe('StatusBadge', () => {
|
||||
const statuses: IssueStatus[] = ['open', 'in_progress', 'in_review', 'blocked', 'done', 'closed'];
|
||||
const statuses: IssueStatus[] = ['open', 'in_progress', 'in_review', 'blocked', 'closed'];
|
||||
|
||||
it.each(statuses)('renders %s status correctly', (status) => {
|
||||
render(<StatusBadge status={status} />);
|
||||
|
||||
@@ -22,7 +22,6 @@ describe('StatusWorkflow', () => {
|
||||
expect(screen.getByText('In Progress')).toBeInTheDocument();
|
||||
expect(screen.getByText('In Review')).toBeInTheDocument();
|
||||
expect(screen.getByText('Blocked')).toBeInTheDocument();
|
||||
expect(screen.getByText('Done')).toBeInTheDocument();
|
||||
expect(screen.getByText('Closed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user