Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
370709c
feat(frontend): unified navigation sidebar and project cache
eipasteur May 21, 2026
4138ab6
feat(frontend): add observability dashboard with agent monitoring
eipasteur May 21, 2026
394098f
Merge branch 'main' into feature/project-homepage
eipasteur May 21, 2026
76f6842
Merge branch 'feature/project-homepage' into feature/observability-da…
eipasteur May 21, 2026
0412c41
fix(frontend): remove unused projectId prop and useState import
eipasteur May 21, 2026
4ebeb19
Merge branch 'feature/project-homepage' into feature/observability-da…
eipasteur May 21, 2026
c321fd4
fix(observability): remove unused imports and variables
eipasteur May 21, 2026
82e8493
feat(frontend): wire Dashboard through useProjectsCache and drop dead…
eipasteur May 21, 2026
83e0049
Merge branch 'feature/project-homepage' into feature/observability-da…
eipasteur May 21, 2026
cc5dace
feat(frontend): wire /observability route to ObservabilityLayout
eipasteur May 21, 2026
1bd2499
feat(frontend): redesign project home page with cache and live updates
eipasteur May 21, 2026
82b8fe0
Merge branch 'feature/project-homepage' into feature/observability-da…
eipasteur May 21, 2026
09b6c36
feat(frontend): align Issues/Iterations columns and add Start iterati…
eipasteur May 21, 2026
63dd574
Merge branch 'feature/project-homepage' into feature/observability-da…
eipasteur May 21, 2026
c34a38e
fix(project-home): align Issues/Iterations headers and tighten title …
eipasteur May 21, 2026
599c2a0
Merge branch 'feature/project-homepage' into feature/observability-da…
eipasteur May 21, 2026
13ba728
Merge branch 'main' into feature/observability-dashboard
eipasteur May 21, 2026
11bc28e
fix(github-lambda): bundle via esbuild to resolve runtime ESM import
eipasteur May 22, 2026
d8c8343
Merge branch 'fix/lambda-github-esbuild-runtime' into feature/observa…
eipasteur May 22, 2026
1b9b2cb
fix(github-lambda): bundle via esbuild + inline resolveGitToken
eipasteur May 22, 2026
aad2c6e
Merge branch 'fix/lambda-github-esbuild-runtime' into feature/observa…
eipasteur May 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import AgentPage from './pages/AgentPage';
import SprintGraph from './pages/SprintGraph';
import GitHubCallback from './pages/GitHubCallback';
import Admin from './pages/Admin';
import ObservabilityPage from './pages/ObservabilityPage';
import ObservabilityLayout from './pages/ObservabilityLayout';

function App() {
return (
Expand All @@ -37,7 +37,7 @@ function App() {
>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/admin" element={<Admin />} />
<Route path="/observability" element={<ObservabilityPage />} />
<Route path="/observability" element={<ObservabilityLayout />} />
<Route path="/project/:projectId" element={<Project />} />
<Route path="/project/:projectId/settings" element={<ProjectSettings />} />

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/IssueListPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export function IssueListPanel({ project, sprints, onSprintCreated }: Props) {

if (!repoInfo) {
return (
<Card className="border-dashed mt-6">
<Card className="border-dashed">
<CardContent className="p-4 text-sm text-muted-foreground">
Issue integration is enabled, but the project's git repository is not in{' '}
<code className="font-mono">owner/repo</code> format.
Expand All @@ -230,7 +230,7 @@ export function IssueListPanel({ project, sprints, onSprintCreated }: Props) {
})();

return (
<Card className="mt-6">
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between gap-3 flex-wrap">
<div className="flex items-center gap-2">
Expand Down
41 changes: 25 additions & 16 deletions frontend/src/components/layout/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,40 @@ import { AppHeader } from '@/components/layout/AppHeader';
import { ActivityPanel } from '@/components/layout/ActivityPanel';
import { StatusBar } from '@/components/layout/StatusBar';
import { CommandPalette } from '@/components/layout/CommandPalette';
import { useState, useCallback } from 'react';
import { useProjectSprintsCache } from '@/hooks/useProjectsCache';
import { useState, useCallback, useMemo, useEffect } from 'react';

export function AppShell() {
const { sprintId } = useParams<{ sprintId: string }>();
const { sprintId, projectId } = useParams<{ sprintId: string; projectId: string }>();
const inSprint = !!sprintId;
const onProjectPage = !!projectId && !inSprint;

const [activityPanelOpen, setActivityPanelOpen] = useState(true);
const { sprints: projectSprints } = useProjectSprintsCache(onProjectPage ? projectId : null);
const latestActiveSprintId = useMemo(() => {
if (inSprint) return sprintId;
const active = projectSprints.find(
(s) => s.currentAgentStatus === 'running' || s.currentAgentStatus === 'waiting',
);
return active?.id ?? projectSprints[0]?.id ?? null;
}, [inSprint, sprintId, projectSprints]);

const [activityPanelOpen, setActivityPanelOpen] = useState(inSprint);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [commandOpen, setCommandOpen] = useState(false);

// Only show panels when inside a sprint
const showSidebar = inSprint && !sidebarCollapsed;
const showActivity = inSprint && activityPanelOpen;
useEffect(() => {
setActivityPanelOpen(inSprint);
}, [inSprint]);

const showSidebar = !sidebarCollapsed;
const showActivity = (inSprint || onProjectPage) && activityPanelOpen;

const toggleSidebar = useCallback(() => setSidebarCollapsed((prev) => !prev), []);
const toggleActivity = useCallback(() => setActivityPanelOpen((prev) => !prev), []);

return (
<TooltipProvider delayDuration={200}>
<div className="flex h-screen flex-col bg-background">
{/* Header */}
<AppHeader
onToggleSidebar={toggleSidebar}
onToggleActivity={toggleActivity}
Expand All @@ -35,43 +48,39 @@ export function AppShell() {
inSprint={inSprint}
/>

{/* Main content area */}
<div
className="flex-1 overflow-hidden grid"
style={{
gridTemplateColumns: [
showSidebar ? 'minmax(240px, 280px)' : '',
showSidebar ? '240px' : '',
'1fr',
showActivity ? 'minmax(280px, 360px)' : '',
]
.filter(Boolean)
.join(' '),
}}
>
{/* Sidebar - only in sprint views */}
{showSidebar && (
<aside className="hidden md:flex border-r overflow-hidden">
<AppSidebar />
</aside>
)}

{/* Main content */}
<main className="h-full overflow-y-auto min-w-0">
<Outlet />
</main>

{/* Activity panel - only in sprint views */}
{showActivity && (
<aside className="hidden lg:flex overflow-hidden">
<ActivityPanel sprintId={sprintId} onClose={() => setActivityPanelOpen(false)} />
<ActivityPanel
sprintId={latestActiveSprintId ?? undefined}
onClose={() => setActivityPanelOpen(false)}
/>
</aside>
)}
</div>

{/* Status bar */}
<StatusBar />

{/* Command palette */}
<CommandPalette open={commandOpen} onOpenChange={setCommandOpen} />
</div>
</TooltipProvider>
Expand Down
204 changes: 116 additions & 88 deletions frontend/src/components/layout/AppSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,112 +1,140 @@
import { useNavigate, useParams, useLocation } from 'react-router-dom';
import { useNavigate, useLocation, useParams } from 'react-router-dom';
import {
Activity,
LayoutDashboard,
Loader2,
MessageCircleQuestion,
Settings,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Button } from '@/components/ui/button';
import { Settings, ArrowLeft } from 'lucide-react';
import { PipelineView } from '@/components/layout/PipelineView';
import { useState, useEffect, useCallback } from 'react';
import { sprintsService, type Sprint } from '@/services/sprints';
import { realtimeService } from '@/services/realtime';
import { useProjectsCache } from '@/hooks/useProjectsCache';

const PHASE_URL_SUFFIX: Record<string, string> = {
INCEPTION: '',
CONSTRUCTION: '/construction',
REVIEW: '/review',
const STATUS_DOT: Record<string, string> = {
running: 'bg-agent-running',
waiting: 'bg-agent-waiting',
completed: 'bg-agent-success',
failed: 'bg-agent-error',
};

export function AppSidebar() {
const navigate = useNavigate();
const params = useParams();
const location = useLocation();
const [sprint, setSprint] = useState<Sprint | null>(null);
const params = useParams<{ projectId?: string }>();
const { projects } = useProjectsCache();

const projectId = params.projectId || '';
const sprintId = params.sprintId || '';
const runningCount = projects.filter((p) => {
const s = p.latestSprint?.currentAgentStatus;
return s === 'running' || s === 'waiting';
}).length;

const loadSprint = useCallback(async () => {
if (!projectId || !sprintId) return;
try {
const data = await sprintsService.get(projectId, sprintId);
setSprint(data);
} catch (err) {
console.error('Failed to load sprint:', err);
}
}, [projectId, sprintId]);

useEffect(() => {
loadSprint();
}, [loadSprint]);

// Re-fetch sprint on agent/phase events and auto-navigate on phase change
useEffect(() => {
if (!sprintId) return;
const unsubs = [
realtimeService.on('agent.started', () => loadSprint()),
realtimeService.on('agent.completed', () => loadSprint()),
realtimeService.on('agent.error', () => loadSprint()),
realtimeService.on('sprint.phaseChanged', (data: { phase?: string }) => {
loadSprint();
if (data.phase && PHASE_URL_SUFFIX[data.phase] !== undefined) {
navigate(`/project/${projectId}/sprint/${sprintId}${PHASE_URL_SUFFIX[data.phase]}`);
}
}),
];
return () => unsubs.forEach((unsub) => unsub());
}, [sprintId, projectId, loadSprint, navigate]);

const currentPhase = location.pathname.includes('/construction')
? 'CONSTRUCTION'
: location.pathname.includes('/review')
? 'REVIEW'
: location.pathname.includes('/graph')
? 'GRAPH'
: location.pathname.includes('/agent')
? 'AGENT'
: 'INCEPTION';
const isOnDashboard = location.pathname === '/dashboard';
const isOnObservability = location.pathname === '/observability';
const isOnAdmin = location.pathname === '/admin';
const activeProjectId = params.projectId ?? null;

return (
<div className="flex h-full w-full flex-col bg-sidebar text-sidebar-foreground">
{/* Brand header */}
<div className="flex h-12 items-center gap-2 px-4 border-b border-sidebar-border">
<img src="/logo.svg" alt="AI-DLC" className="h-7 w-7 shrink-0" />
<span className="font-semibold text-sm tracking-wide">AI-DLC</span>
</div>
<nav className="flex flex-col gap-0.5 px-3 py-3">
<button
onClick={() => navigate('/dashboard')}
className={cn(
'flex items-center gap-2.5 px-3 py-2 text-[13px] font-medium transition-colors rounded-md text-left w-full',
isOnDashboard
? 'bg-sidebar-accent text-sidebar-foreground'
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground',
)}
>
<LayoutDashboard className="h-4 w-4 shrink-0" />
<span className="flex-1 truncate">Projects</span>
</button>

{/* Back to project */}
<div className="px-3 pt-3 pb-1">
<Button
variant="ghost"
size="sm"
className="w-full justify-start gap-2 text-sidebar-foreground/60 hover:text-sidebar-foreground h-7 text-xs"
onClick={() => navigate(`/project/${projectId}`)}
<button
onClick={() => navigate('/observability')}
className={cn(
'flex items-center gap-2.5 px-3 py-2 text-[13px] font-medium transition-colors rounded-md text-left w-full',
isOnObservability
? 'bg-sidebar-accent text-sidebar-foreground'
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground',
)}
>
<ArrowLeft className="h-3 w-3" />
Back to project
</Button>
<Activity className="h-4 w-4 shrink-0" />
<span className="flex-1 truncate">Observability</span>
{runningCount > 0 && (
<span className="flex items-center gap-1.5">
<span className="relative flex h-2 w-2">
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-agent-running opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-agent-running" />
</span>
<span className="text-[11px] font-medium text-agent-running">{runningCount}</span>
</span>
)}
</button>
</nav>

<div className="px-3 py-1.5">
<div className="text-[10px] font-medium uppercase tracking-widest font-mono text-sidebar-foreground/40 px-3">
Projects
</div>
</div>

{/* Pipeline view -- the core of the sidebar */}
<ScrollArea className="flex-1">
<div className="p-3">
<PipelineView
projectId={projectId}
sprintId={sprintId}
currentPhase={currentPhase}
sprint={sprint ?? undefined}
/>
<ScrollArea className="flex-1 min-h-0">
<div className="flex flex-col gap-0.5 px-3 pb-3">
{projects.map(({ project, latestSprint }) => {
const status = latestSprint?.currentAgentStatus;
const isActive = status === 'running' || status === 'waiting';
const dotColor = status ? STATUS_DOT[status] : undefined;
const isSelected = activeProjectId === project.id;

return (
<button
key={project.id}
onClick={() => navigate(`/project/${project.id}`)}
className={cn(
'flex items-center gap-2.5 px-3 py-1.5 text-[13px] font-medium transition-colors rounded-md text-left w-full',
isSelected
? 'bg-sidebar-accent text-sidebar-foreground'
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground',
)}
>
<span className="relative shrink-0">
<span className="block h-3.5 w-3.5 rounded-sm bg-sidebar-primary/30" />
{dotColor && (
<span
className={cn(
'absolute -right-0.5 -top-0.5 h-2 w-2 rounded-full shadow-[0_0_0_2px_hsl(var(--sidebar-background))]',
dotColor,
isActive && 'animate-pulse',
)}
/>
)}
</span>
<span className="flex-1 truncate">{project.name}</span>
{status === 'running' && (
<Loader2 className="h-3 w-3 text-agent-running animate-spin shrink-0" />
)}
{status === 'waiting' && (
<MessageCircleQuestion className="h-3 w-3 text-agent-waiting shrink-0" />
)}
</button>
);
})}
</div>
</ScrollArea>

{/* Bottom: Settings */}
<div className="border-t border-sidebar-border p-2">
<Button
variant="ghost"
size="sm"
className="w-full justify-start gap-2 text-sidebar-foreground/60 hover:text-sidebar-foreground h-8 text-xs"
onClick={() => navigate(`/project/${projectId}/settings`)}
<button
onClick={() => navigate('/admin')}
className={cn(
'flex items-center gap-2.5 px-3 py-2 text-[13px] font-medium transition-colors rounded-md text-left w-full',
isOnAdmin
? 'bg-sidebar-accent text-sidebar-foreground'
: 'text-sidebar-foreground/60 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground',
)}
>
<Settings className="h-3.5 w-3.5" />
Project Settings
</Button>
<Settings className="h-3.5 w-3.5 shrink-0" />
Admin & Settings
</button>
</div>
</div>
);
Expand Down
Loading
Loading