feat: Replace initiative card N+1 queries with server-computed activity indicator
listInitiatives now returns an activity object (state, activePhase, phase counts) derived server-side from phases, eliminating per-card listPhases queries. Initiative cards show a StatusDot with pulse animation + label instead of a static StatusBadge. Removed redundant View and Spawn Architect buttons from cards. Added variant override prop to StatusDot.
This commit is contained in:
47
apps/server/trpc/routers/initiative-activity.ts
Normal file
47
apps/server/trpc/routers/initiative-activity.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* Initiative Activity — derives current activity state from initiative + phases.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Initiative, Phase } from '../../db/schema.js';
|
||||||
|
import type { InitiativeActivity, InitiativeActivityState } from '@codewalk-district/shared';
|
||||||
|
|
||||||
|
export function deriveInitiativeActivity(initiative: Initiative, phases: Phase[]): InitiativeActivity {
|
||||||
|
const phasesTotal = phases.length;
|
||||||
|
const phasesCompleted = phases.filter(p => p.status === 'completed').length;
|
||||||
|
const base = { phasesTotal, phasesCompleted };
|
||||||
|
|
||||||
|
if (initiative.status === 'archived') {
|
||||||
|
return { ...base, state: 'archived' };
|
||||||
|
}
|
||||||
|
if (initiative.status === 'completed') {
|
||||||
|
return { ...base, state: 'complete' };
|
||||||
|
}
|
||||||
|
if (phasesTotal === 0) {
|
||||||
|
return { ...base, state: 'idle' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority-ordered state detection (first match wins)
|
||||||
|
const priorities: Array<{ status: Phase['status']; state: InitiativeActivityState }> = [
|
||||||
|
{ status: 'pending_review', state: 'pending_review' },
|
||||||
|
{ status: 'in_progress', state: 'executing' },
|
||||||
|
{ status: 'blocked', state: 'blocked' },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { status, state } of priorities) {
|
||||||
|
const match = phases.find(p => p.status === status);
|
||||||
|
if (match) {
|
||||||
|
return { ...base, state, activePhase: { id: match.id, name: match.name } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phasesCompleted === phasesTotal) {
|
||||||
|
return { ...base, state: 'complete' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const approved = phases.find(p => p.status === 'approved');
|
||||||
|
if (approved) {
|
||||||
|
return { ...base, state: 'ready', activePhase: { id: approved.id, name: approved.name } };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...base, state: 'planning' };
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { TRPCError } from '@trpc/server';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import type { ProcedureBuilder } from '../trpc.js';
|
import type { ProcedureBuilder } from '../trpc.js';
|
||||||
import { requireInitiativeRepository, requireProjectRepository, requireTaskRepository } from './_helpers.js';
|
import { requireInitiativeRepository, requireProjectRepository, requireTaskRepository } from './_helpers.js';
|
||||||
|
import { deriveInitiativeActivity } from './initiative-activity.js';
|
||||||
|
|
||||||
export function initiativeProcedures(publicProcedure: ProcedureBuilder) {
|
export function initiativeProcedures(publicProcedure: ProcedureBuilder) {
|
||||||
return {
|
return {
|
||||||
@@ -63,10 +64,22 @@ export function initiativeProcedures(publicProcedure: ProcedureBuilder) {
|
|||||||
}).optional())
|
}).optional())
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const repo = requireInitiativeRepository(ctx);
|
const repo = requireInitiativeRepository(ctx);
|
||||||
if (input?.status) {
|
const initiatives = input?.status
|
||||||
return repo.findByStatus(input.status);
|
? await repo.findByStatus(input.status)
|
||||||
|
: await repo.findAll();
|
||||||
|
|
||||||
|
if (ctx.phaseRepository) {
|
||||||
|
const phaseRepo = ctx.phaseRepository;
|
||||||
|
return Promise.all(initiatives.map(async (init) => {
|
||||||
|
const phases = await phaseRepo.findByInitiativeId(init.id);
|
||||||
|
return { ...init, activity: deriveInitiativeActivity(init, phases) };
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
return repo.findAll();
|
|
||||||
|
return initiatives.map(init => ({
|
||||||
|
...init,
|
||||||
|
activity: deriveInitiativeActivity(init, []),
|
||||||
|
}));
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getInitiative: publicProcedure
|
getInitiative: publicProcedure
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export function CreateInitiativeDialog({
|
|||||||
mergeRequiresApproval: true,
|
mergeRequiresApproval: true,
|
||||||
branch: null,
|
branch: null,
|
||||||
projects: [],
|
projects: [],
|
||||||
|
activity: { state: 'idle' as const, phasesTotal: 0, phasesCompleted: 0 },
|
||||||
};
|
};
|
||||||
utils.listInitiatives.setData(undefined, (old = []) => [tempInitiative, ...old]);
|
utils.listInitiatives.setData(undefined, (old = []) => [tempInitiative, ...old]);
|
||||||
return { previousInitiatives };
|
return { previousInitiatives };
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MoreHorizontal, Eye, Bot } from "lucide-react";
|
import { MoreHorizontal } from "lucide-react";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { StatusBadge } from "@/components/StatusBadge";
|
import { StatusDot, type StatusVariant } from "@/components/StatusDot";
|
||||||
import { ProgressBar } from "@/components/ProgressBar";
|
import { ProgressBar } from "@/components/ProgressBar";
|
||||||
import { trpc } from "@/lib/trpc";
|
import { trpc } from "@/lib/trpc";
|
||||||
|
|
||||||
@@ -21,19 +21,33 @@ export interface SerializedInitiative {
|
|||||||
branch: string | null;
|
branch: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
activity: {
|
||||||
|
state: string;
|
||||||
|
activePhase?: { id: string; name: string };
|
||||||
|
phasesTotal: number;
|
||||||
|
phasesCompleted: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function activityVisual(state: string): { label: string; variant: StatusVariant; pulse: boolean } {
|
||||||
|
switch (state) {
|
||||||
|
case "executing": return { label: "Executing", variant: "active", pulse: true };
|
||||||
|
case "pending_review": return { label: "Pending Review", variant: "warning", pulse: true };
|
||||||
|
case "ready": return { label: "Ready", variant: "active", pulse: false };
|
||||||
|
case "blocked": return { label: "Blocked", variant: "error", pulse: false };
|
||||||
|
case "complete": return { label: "Complete", variant: "success", pulse: false };
|
||||||
|
case "planning": return { label: "Planning", variant: "neutral", pulse: false };
|
||||||
|
case "archived": return { label: "Archived", variant: "neutral", pulse: false };
|
||||||
|
default: return { label: "Idle", variant: "neutral", pulse: false };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InitiativeCardProps {
|
interface InitiativeCardProps {
|
||||||
initiative: SerializedInitiative;
|
initiative: SerializedInitiative;
|
||||||
onView: () => void;
|
onClick: () => void;
|
||||||
onSpawnArchitect: (mode: "discuss" | "plan") => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InitiativeCard({
|
export function InitiativeCard({ initiative, onClick }: InitiativeCardProps) {
|
||||||
initiative,
|
|
||||||
onView,
|
|
||||||
onSpawnArchitect,
|
|
||||||
}: InitiativeCardProps) {
|
|
||||||
const utils = trpc.useUtils();
|
const utils = trpc.useUtils();
|
||||||
const archiveMutation = trpc.updateInitiative.useMutation({
|
const archiveMutation = trpc.updateInitiative.useMutation({
|
||||||
onSuccess: () => utils.listInitiatives.invalidate(),
|
onSuccess: () => utils.listInitiatives.invalidate(),
|
||||||
@@ -62,75 +76,23 @@ export function InitiativeCard({
|
|||||||
deleteMutation.mutate({ id: initiative.id });
|
deleteMutation.mutate({ id: initiative.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Each card fetches its own phase stats (N+1 acceptable for v1 small counts)
|
const { activity } = initiative;
|
||||||
const phasesQuery = trpc.listPhases.useQuery({
|
const visual = activityVisual(activity.state);
|
||||||
initiativeId: initiative.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const phases = phasesQuery.data ?? [];
|
|
||||||
const completedCount = phases.filter((p) => p.status === "completed").length;
|
|
||||||
const totalCount = phases.length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="cursor-pointer p-4 transition-colors hover:bg-accent/50"
|
className="cursor-pointer p-4 transition-colors hover:bg-accent/50"
|
||||||
onClick={onView}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
{/* Row 1: Name + overflow menu */}
|
||||||
{/* Left: Initiative name */}
|
<div className="flex items-center justify-between">
|
||||||
<div className="min-w-0 flex-shrink-0">
|
<span className="min-w-0 truncate text-base font-bold">
|
||||||
<span className="text-base font-bold">{initiative.name}</span>
|
{initiative.name}
|
||||||
</div>
|
</span>
|
||||||
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
{/* Middle: Status + Progress + Phase count */}
|
|
||||||
<div className="flex flex-1 items-center gap-4">
|
|
||||||
<StatusBadge status={initiative.status} />
|
|
||||||
<ProgressBar
|
|
||||||
completed={completedCount}
|
|
||||||
total={totalCount}
|
|
||||||
className="w-32"
|
|
||||||
/>
|
|
||||||
<span className="hidden text-sm text-muted-foreground md:inline">
|
|
||||||
{completedCount}/{totalCount} phases
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right: Action buttons */}
|
|
||||||
<div
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<Button variant="outline" size="sm" onClick={onView}>
|
|
||||||
<Eye className="mr-1 h-4 w-4" />
|
|
||||||
View
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Spawn Architect Dropdown */}
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="outline" size="sm">
|
<Button variant="ghost" size="icon" className="h-7 w-7">
|
||||||
<Bot className="mr-1 h-4 w-4" />
|
|
||||||
Spawn Architect
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => onSpawnArchitect("discuss")}
|
|
||||||
>
|
|
||||||
Discuss
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => onSpawnArchitect("plan")}
|
|
||||||
>
|
|
||||||
Plan
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
|
|
||||||
{/* More Actions Menu */}
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon">
|
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -147,6 +109,35 @@ export function InitiativeCard({
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Row 2: Activity dot + label + active phase + progress */}
|
||||||
|
<div className="mt-1.5 flex items-center gap-3">
|
||||||
|
<StatusDot
|
||||||
|
status={activity.state}
|
||||||
|
variant={visual.variant}
|
||||||
|
size="sm"
|
||||||
|
pulse={visual.pulse}
|
||||||
|
label={visual.label}
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium">{visual.label}</span>
|
||||||
|
{activity.activePhase && (
|
||||||
|
<span className="truncate text-sm text-muted-foreground">
|
||||||
|
{activity.activePhase.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{activity.phasesTotal > 0 && (
|
||||||
|
<>
|
||||||
|
<ProgressBar
|
||||||
|
completed={activity.phasesCompleted}
|
||||||
|
total={activity.phasesTotal}
|
||||||
|
className="ml-auto w-24"
|
||||||
|
/>
|
||||||
|
<span className="hidden text-xs text-muted-foreground md:inline">
|
||||||
|
{activity.phasesCompleted}/{activity.phasesTotal}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,17 +9,12 @@ interface InitiativeListProps {
|
|||||||
statusFilter?: "all" | "active" | "completed" | "archived";
|
statusFilter?: "all" | "active" | "completed" | "archived";
|
||||||
onCreateNew: () => void;
|
onCreateNew: () => void;
|
||||||
onViewInitiative: (id: string) => void;
|
onViewInitiative: (id: string) => void;
|
||||||
onSpawnArchitect: (
|
|
||||||
initiativeId: string,
|
|
||||||
mode: "discuss" | "plan",
|
|
||||||
) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InitiativeList({
|
export function InitiativeList({
|
||||||
statusFilter = "all",
|
statusFilter = "all",
|
||||||
onCreateNew,
|
onCreateNew,
|
||||||
onViewInitiative,
|
onViewInitiative,
|
||||||
onSpawnArchitect,
|
|
||||||
}: InitiativeListProps) {
|
}: InitiativeListProps) {
|
||||||
const initiativesQuery = trpc.listInitiatives.useQuery(
|
const initiativesQuery = trpc.listInitiatives.useQuery(
|
||||||
statusFilter === "all" ? undefined : { status: statusFilter },
|
statusFilter === "all" ? undefined : { status: statusFilter },
|
||||||
@@ -31,13 +26,14 @@ export function InitiativeList({
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{Array.from({ length: 3 }).map((_, i) => (
|
{Array.from({ length: 3 }).map((_, i) => (
|
||||||
<Card key={i} className="p-4">
|
<Card key={i} className="p-4">
|
||||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Skeleton className="h-5 w-48" />
|
<Skeleton className="h-5 w-48" />
|
||||||
<div className="flex flex-1 items-center gap-4">
|
<Skeleton className="h-7 w-7 rounded" />
|
||||||
<Skeleton className="h-5 w-16" />
|
</div>
|
||||||
<Skeleton className="h-2 w-32" />
|
<div className="mt-1.5 flex items-center gap-3">
|
||||||
<Skeleton className="h-4 w-24" />
|
<Skeleton className="h-2 w-2 rounded-full" />
|
||||||
</div>
|
<Skeleton className="h-4 w-20" />
|
||||||
|
<Skeleton className="ml-auto h-2 w-24" />
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
@@ -91,8 +87,7 @@ export function InitiativeList({
|
|||||||
<InitiativeCard
|
<InitiativeCard
|
||||||
key={initiative.id}
|
key={initiative.id}
|
||||||
initiative={initiative}
|
initiative={initiative}
|
||||||
onView={() => onViewInitiative(initiative.id)}
|
onClick={() => onViewInitiative(initiative.id)}
|
||||||
onSpawnArchitect={(mode) => onSpawnArchitect(initiative.id, mode)}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ export function mapEntityStatus(rawStatus: string): StatusVariant {
|
|||||||
|
|
||||||
interface StatusDotProps {
|
interface StatusDotProps {
|
||||||
status: string;
|
status: string;
|
||||||
|
/** Override the auto-mapped variant with an explicit one. */
|
||||||
|
variant?: StatusVariant;
|
||||||
size?: "sm" | "md" | "lg";
|
size?: "sm" | "md" | "lg";
|
||||||
pulse?: boolean;
|
pulse?: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
@@ -68,6 +70,7 @@ interface StatusDotProps {
|
|||||||
|
|
||||||
export function StatusDot({
|
export function StatusDot({
|
||||||
status,
|
status,
|
||||||
|
variant: variantOverride,
|
||||||
size = "md",
|
size = "md",
|
||||||
pulse = false,
|
pulse = false,
|
||||||
label,
|
label,
|
||||||
@@ -79,7 +82,7 @@ export function StatusDot({
|
|||||||
lg: "h-4 w-4",
|
lg: "h-4 w-4",
|
||||||
};
|
};
|
||||||
|
|
||||||
const variant = mapEntityStatus(status);
|
const variant = variantOverride ?? mapEntityStatus(status);
|
||||||
const color = dotColors[variant];
|
const color = dotColors[variant];
|
||||||
const displayLabel = label ?? status.replace(/_/g, " ").toLowerCase();
|
const displayLabel = label ?? status.replace(/_/g, " ").toLowerCase();
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ function DashboardPage() {
|
|||||||
|
|
||||||
// Single SSE stream for live updates
|
// Single SSE stream for live updates
|
||||||
useLiveUpdates([
|
useLiveUpdates([
|
||||||
{ prefix: 'task:', invalidate: ['listInitiatives', 'listPhases'] },
|
{ prefix: 'task:', invalidate: ['listInitiatives'] },
|
||||||
{ prefix: 'phase:', invalidate: ['listInitiatives', 'listPhases'] },
|
{ prefix: 'phase:', invalidate: ['listInitiatives'] },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -63,10 +63,6 @@ function DashboardPage() {
|
|||||||
onViewInitiative={(id) =>
|
onViewInitiative={(id) =>
|
||||||
navigate({ to: "/initiatives/$id", params: { id } })
|
navigate({ to: "/initiatives/$id", params: { id } })
|
||||||
}
|
}
|
||||||
onSpawnArchitect={(_initiativeId, _mode) => {
|
|
||||||
// Architect spawning is self-contained within SpawnArchitectDropdown
|
|
||||||
// This callback is available for future toast notifications
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Create initiative dialog */}
|
{/* Create initiative dialog */}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ The initiative detail page has three tabs managed via local state (not URL param
|
|||||||
### Core Components (`src/components/`)
|
### Core Components (`src/components/`)
|
||||||
| Component | Purpose |
|
| Component | Purpose |
|
||||||
|-----------|---------|
|
|-----------|---------|
|
||||||
|
| `InitiativeCard` | Initiative list card with activity indicator (dot + label + phase progress), overflow menu |
|
||||||
| `InitiativeHeader` | Initiative name, project badges, inline-editable execution mode & branch |
|
| `InitiativeHeader` | Initiative name, project badges, inline-editable execution mode & branch |
|
||||||
| `InitiativeContent` | Content tab with page tree + editor |
|
| `InitiativeContent` | Content tab with page tree + editor |
|
||||||
| `StatusDot` | Small colored dot using status tokens, with pulse animation |
|
| `StatusDot` | Small colored dot using status tokens, with pulse animation |
|
||||||
@@ -172,4 +173,11 @@ Configured in `src/lib/trpc.ts`. Uses `@trpc/react-query` with TanStack Query fo
|
|||||||
`packages/shared/` exports:
|
`packages/shared/` exports:
|
||||||
- `sortByPriorityAndQueueTime()` — priority-based task sorting
|
- `sortByPriorityAndQueueTime()` — priority-based task sorting
|
||||||
- `topologicalSort()` / `groupByPipelineColumn()` — phase DAG layout
|
- `topologicalSort()` / `groupByPipelineColumn()` — phase DAG layout
|
||||||
|
- `InitiativeActivity` / `InitiativeActivityState` — server-computed activity state for initiative cards
|
||||||
- Shared type re-exports from `packages/shared/src/types.ts` (which re-exports from `apps/server/`)
|
- Shared type re-exports from `packages/shared/src/types.ts` (which re-exports from `apps/server/`)
|
||||||
|
|
||||||
|
## Initiative Activity Indicator
|
||||||
|
|
||||||
|
`listInitiatives` returns an `activity` field on each initiative, computed server-side from phase statuses via `deriveInitiativeActivity()` in `apps/server/trpc/routers/initiative-activity.ts`. This eliminates per-card N+1 `listPhases` queries.
|
||||||
|
|
||||||
|
Activity states (priority order): `pending_review` > `executing` > `blocked` > `complete` > `ready` > `planning` > `idle` > `archived`. Each state maps to a `StatusVariant` + pulse animation in `InitiativeCard`'s `activityVisual()` function.
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ Each procedure uses `require*Repository(ctx)` helpers that throw `TRPCError(INTE
|
|||||||
| Procedure | Type | Description |
|
| Procedure | Type | Description |
|
||||||
|-----------|------|-------------|
|
|-----------|------|-------------|
|
||||||
| createInitiative | mutation | Create with optional branch/projectIds, auto-creates root page |
|
| createInitiative | mutation | Create with optional branch/projectIds, auto-creates root page |
|
||||||
| listInitiatives | query | Filter by status |
|
| listInitiatives | query | Filter by status; returns `activity` (state, activePhase, phase counts) computed from phases |
|
||||||
| getInitiative | query | With projects array |
|
| getInitiative | query | With projects array |
|
||||||
| updateInitiative | mutation | Name, status |
|
| updateInitiative | mutation | Name, status |
|
||||||
| deleteInitiative | mutation | Cascade delete initiative and all children |
|
| deleteInitiative | mutation | Cascade delete initiative and all children |
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export type { AppRouter } from './trpc.js';
|
export type { AppRouter } from './trpc.js';
|
||||||
export type { Initiative, Phase, Task, Agent, Message, PendingQuestions, QuestionItem, SubscriptionEvent, Project, ChangeSet, ChangeSetEntry } from './types.js';
|
export type { Initiative, Phase, Task, Agent, Message, PendingQuestions, QuestionItem, SubscriptionEvent, Project, ChangeSet, ChangeSetEntry, InitiativeActivityState, InitiativeActivity } from './types.js';
|
||||||
export { sortByPriorityAndQueueTime, topologicalSortPhases, groupPhasesByDependencyLevel, type SortableItem, type PhaseForSort, type DependencyEdge, type PipelineColumn } from './utils.js';
|
export { sortByPriorityAndQueueTime, topologicalSortPhases, groupPhasesByDependencyLevel, type SortableItem, type PhaseForSort, type DependencyEdge, type PipelineColumn } from './utils.js';
|
||||||
|
|||||||
@@ -2,6 +2,23 @@ export type { Initiative, Phase, Task, Agent, Message, Page, Project, Account, C
|
|||||||
export type { PendingQuestions, QuestionItem } from '../../../apps/server/agent/types.js';
|
export type { PendingQuestions, QuestionItem } from '../../../apps/server/agent/types.js';
|
||||||
|
|
||||||
export type ExecutionMode = 'yolo' | 'review_per_phase';
|
export type ExecutionMode = 'yolo' | 'review_per_phase';
|
||||||
|
|
||||||
|
export type InitiativeActivityState =
|
||||||
|
| 'idle' // Active but no phases
|
||||||
|
| 'planning' // All phases pending (no work started)
|
||||||
|
| 'ready' // Phases approved, waiting to execute
|
||||||
|
| 'executing' // At least one phase in_progress
|
||||||
|
| 'pending_review' // At least one phase pending_review
|
||||||
|
| 'blocked' // At least one phase blocked (none in_progress/pending_review)
|
||||||
|
| 'complete' // All phases completed
|
||||||
|
| 'archived'; // Initiative archived
|
||||||
|
|
||||||
|
export interface InitiativeActivity {
|
||||||
|
state: InitiativeActivityState;
|
||||||
|
activePhase?: { id: string; name: string };
|
||||||
|
phasesTotal: number;
|
||||||
|
phasesCompleted: number;
|
||||||
|
}
|
||||||
export type PhaseStatus = 'pending' | 'approved' | 'in_progress' | 'completed' | 'blocked' | 'pending_review';
|
export type PhaseStatus = 'pending' | 'approved' | 'in_progress' | 'completed' | 'blocked' | 'pending_review';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user