Feat/web dashboard - #5
Conversation
There was a problem hiding this comment.
Pull request overview
Implements the StackForge web dashboard (Vite/React) backed by a new reusable UI package, and adds API runtime/provider status + readiness gating so generation fails fast when an LLM provider isn’t configured.
Changes:
- Added
@stackforge/uicomponent primitives (Button/Card/Badge/Spinner/Toast) for consistent dashboard UI. - Added
@stackforge/webapp with Home + Job pages, demo-mode simulation, and SSE-driven live agent timeline + blueprint rendering. - Added API runtime provider resolution (
openrouter/mock/auto),/api/runtimestatus endpoint, and generation readiness gating + integration test coverage.
Reviewed changes
Copilot reviewed 34 out of 36 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Documents provider modes and runtime endpoint. |
| packages/ui/tsconfig.json | TypeScript config for new UI workspace package. |
| packages/ui/package.json | Defines UI package entrypoints, scripts, and React peer deps. |
| packages/ui/src/index.ts | Barrel exports for UI primitives. |
| packages/ui/src/Button.tsx | Adds reusable Button component (variants/sizes/loading). |
| packages/ui/src/Card.tsx | Adds reusable Card component with hover/glow styling. |
| packages/ui/src/Badge.tsx | Adds status Badge component for agent/job states. |
| packages/ui/src/Spinner.tsx | Adds Spinner component for loading indicators. |
| packages/ui/src/Toast.tsx | Adds ToastProvider + useToast hook for notifications. |
| apps/web/package.json | Scaffolds web app dependencies and Vite/TS build scripts. |
| apps/web/tsconfig.json | TS config for the web app + reference to packages/ui. |
| apps/web/vite.config.ts | Adds Vite config + API proxy for local dev. |
| apps/web/vite-env.d.ts | Adds Vite client type reference. |
| apps/web/index.html | Adds web app HTML entry. |
| apps/web/src/main.tsx | Bootstraps React root rendering. |
| apps/web/src/App.tsx | Adds router + global ToastProvider + layout shell. |
| apps/web/src/index.css | Adds Tailwind import + global tokens/animations/utilities. |
| apps/web/src/lib/api.ts | Adds web-facing API client for generate/job/runtime endpoints. |
| apps/web/src/lib/mock-data.ts | Adds demo blueprint + demo SSE simulation utilities. |
| apps/web/src/hooks/useJobStream.ts | Adds SSE hook for live job/agent updates (demo + real). |
| apps/web/src/pages/Home.tsx | Implements prompt + stack selection + runtime readiness UI + demo entry. |
| apps/web/src/pages/JobPage.tsx | Implements job view: live timeline + blueprint rendering + toasts. |
| apps/web/src/components/Layout.tsx | Adds dashboard layout with nav/footer. |
| apps/web/src/components/AgentTimeline.tsx | Renders vertical agent timeline from stream state. |
| apps/web/src/components/AgentCard.tsx | Renders individual agent status cards. |
| apps/web/src/components/BlueprintView.tsx | Renders generated blueprint sections + JSON download. |
| apps/web/src/components/CollapsibleSection.tsx | Adds reusable collapsible section UI for blueprint panels. |
| apps/web/src/components/FileTree.tsx | Adds file tree renderer for blueprint folder structure. |
| apps/api/src/services/generate.service.ts | Adds provider mode resolution + runtime status + mock provider support. |
| apps/api/src/controllers/generate.controller.ts | Adds fail-fast readiness check before job creation. |
| apps/api/src/controllers/jobs.controller.ts | Adds runtime status controller + wires into routing. |
| apps/api/src/routes/index.ts | Adds /runtime route under /api. |
| apps/api/src/index.ts | Adds CORS headers middleware (for web dev/prod). |
| apps/api/test/integration.test.ts | Adds integration test coverage for runtime endpoint. |
| apps/api/.env.example | Documents STACKFORGE_PROVIDER config with default auto. |
| bun.lock | Captures new dependencies for web/ui (React, router, Vite, Tailwind, etc.). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| STACKFORGE_PROVIDER=openrouter | ||
| OPENROUTER_API_KEY=your_key_here | ||
| OPENROUTER_ENDPOINT=https://openrouter.ai/api/v1/chat/completions | ||
| OPENROUTER_APP_NAME=stackforge-api | ||
| OPENROUTER_APP_URL=http://localhost:3001 | ||
| ``` | ||
|
|
||
| `STACKFORGE_PROVIDER` supports: | ||
| - `openrouter`: forces real LLM execution (requires `OPENROUTER_API_KEY`) | ||
| - `mock`: forces deterministic offline agent responses | ||
| - omitted/`auto`: uses `openrouter` when API key is present, otherwise `mock` |
There was a problem hiding this comment.
STACKFORGE_PROVIDER is set to openrouter in the README example, but the text below says the default/auto mode will fall back to mock when no API key is present. Setting openrouter here will force 503 errors for anyone who hasn’t set OPENROUTER_API_KEY. Consider changing the example to STACKFORGE_PROVIDER=auto (or omitting it) and mentioning openrouter as an optional override.
| Runtime/provider health is exposed at: | ||
| - `GET /api/runtime` | ||
|
|
There was a problem hiding this comment.
The PR description mentions a GET /runtime/status endpoint, but the docs/implementation/tests here use GET /api/runtime. Please align the PR description and docs so there’s a single canonical runtime status endpoint name/path.
| "main": "./src/index.ts", | ||
| "types": "./src/index.ts", | ||
| "exports": { | ||
| ".": { | ||
| "import": "./src/index.ts", | ||
| "types": "./src/index.ts" |
There was a problem hiding this comment.
main/types/exports point at ./src/index.ts, but this package’s build emits to dist/ (and other workspace packages export dist artifacts). Pointing exports at source TS can break non-bundler consumers and makes the tsc build output unused. Consider switching to ./dist/index.js + ./dist/index.d.ts (and keeping src for dev via TS project references if needed).
| "main": "./src/index.ts", | |
| "types": "./src/index.ts", | |
| "exports": { | |
| ".": { | |
| "import": "./src/index.ts", | |
| "types": "./src/index.ts" | |
| "main": "./dist/index.js", | |
| "types": "./dist/index.d.ts", | |
| "exports": { | |
| ".": { | |
| "import": "./dist/index.js", | |
| "types": "./dist/index.d.ts" |
| export function ToastProvider({ children }: { children: React.ReactNode }) { | ||
| const [toasts, setToasts] = useState<ToastItem[]>([]); | ||
|
|
||
| const addToast = useCallback((type: ToastType, message: string) => { | ||
| const id = ++nextId; | ||
| setToasts((prev) => [...prev, { id, type, message }]); | ||
| setTimeout(() => { | ||
| setToasts((prev) => prev.filter((t) => t.id !== id)); | ||
| }, 4000); | ||
| }, []); |
There was a problem hiding this comment.
ToastProvider schedules setTimeout calls but never clears them on unmount. If the provider unmounts (HMR, tests, embedding), pending timers can call setToasts after unmount and trigger warnings/leaks. Track timeout IDs in a ref and clear them in a useEffect cleanup.
|
|
||
| app.use(express.json()); | ||
|
|
||
| // CORS for Vite dev server | ||
| app.use((_req, res, next) => { | ||
| res.setHeader("Access-Control-Allow-Origin", "*"); | ||
| res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS"); | ||
| res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); | ||
| if (_req.method === "OPTIONS") { | ||
| res.sendStatus(204); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Wildcard CORS (Access-Control-Allow-Origin: *) is applied unconditionally to the entire API. If this service is deployed with real LLM credentials, any website can call the generation endpoints from a browser and read responses, increasing abuse risk. Consider enabling CORS only in development, or restricting origins via an allowlist/env var (or using the cors middleware).
| app.use(express.json()); | |
| // CORS for Vite dev server | |
| app.use((_req, res, next) => { | |
| res.setHeader("Access-Control-Allow-Origin", "*"); | |
| res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS"); | |
| res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); | |
| if (_req.method === "OPTIONS") { | |
| res.sendStatus(204); | |
| return; | |
| } | |
| const allowedOrigins = | |
| (process.env["CORS_ORIGINS"] ?? "") | |
| .split(",") | |
| .map((origin) => origin.trim()) | |
| .filter((origin) => origin.length > 0); | |
| app.use(express.json()); | |
| // CORS for Vite dev server (permissive in development, restricted by allowlist otherwise) | |
| app.use((_req, res, next) => { | |
| const origin = _req.headers.origin as string | undefined; | |
| if (process.env["NODE_ENV"] === "development") { | |
| res.setHeader("Access-Control-Allow-Origin", "*"); | |
| } else if (origin && allowedOrigins.includes(origin)) { | |
| res.setHeader("Access-Control-Allow-Origin", origin); | |
| } | |
| res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS"); | |
| res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); | |
| if (_req.method === "OPTIONS") { | |
| res.sendStatus(204); | |
| return; | |
| } |
| runtimeController, | ||
| listJobsController, | ||
| getJobController, | ||
| streamController, |
There was a problem hiding this comment.
This import block uses tab indentation, while the surrounding codebase uses spaces. This can cause noisy diffs and potentially fail formatting checks. Please reformat this block to match the existing spacing style.
| runtimeController, | |
| listJobsController, | |
| getJobController, | |
| streamController, | |
| runtimeController, | |
| listJobsController, | |
| getJobController, | |
| streamController, |
| const [runtimeError, setRuntimeError] = useState<string | null>(null); | ||
|
|
||
| const isValid = prompt.trim().length >= 10; | ||
| const canGenerate = isValid && runtime?.ready !== false; |
There was a problem hiding this comment.
canGenerate is computed as runtime?.ready !== false, which evaluates to true when runtime status hasn’t loaded yet (runtime === null) and also doesn’t account for runtimeError. This allows users to submit generation requests even when runtime status fetch failed or is still pending (undermining the “readiness gating” UX). Consider requiring runtime?.ready === true (or disabling until status is known) and incorporating runtimeError into the disabled state/message.
| const canGenerate = isValid && runtime?.ready !== false; | |
| const canGenerate = isValid && runtime?.ready === true && !runtimeError; |
| a.click(); | ||
| URL.revokeObjectURL(url); |
There was a problem hiding this comment.
handleDownload revokes the object URL immediately after triggering a.click(). In some browsers this can cancel the download or produce intermittent failures. Consider revoking in a setTimeout (or after the click via requestAnimationFrame) and optionally appending/removing the anchor element for better cross-browser behavior.
| a.click(); | |
| URL.revokeObjectURL(url); | |
| document.body.appendChild(a); | |
| a.click(); | |
| setTimeout(() => { | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }, 0); |
| <button | ||
| onClick={() => setOpen(!open)} | ||
| style={{ | ||
| width: "100%", | ||
| display: "flex", | ||
| alignItems: "center", | ||
| gap: "10px", | ||
| padding: "16px 20px", | ||
| background: "transparent", | ||
| border: "none", | ||
| color: "#f0f0f5", | ||
| fontSize: "15px", | ||
| fontWeight: 600, | ||
| fontFamily: "'Inter', system-ui, sans-serif", | ||
| cursor: "pointer", | ||
| textAlign: "left", | ||
| }} | ||
| > |
There was a problem hiding this comment.
The collapsible toggle button doesn’t expose its expanded/collapsed state to assistive tech. Consider adding aria-expanded={open}, an aria-controls pointing at the panel region, and ensuring the disclosure icon is marked decorative so screen readers announce a meaningful label.
| export async function getRuntime(): Promise<RuntimeResponse> { | ||
| const res = await fetch(`${API_BASE}/runtime`); | ||
|
|
||
| if (!res.ok) { | ||
| const body = await res.json().catch(() => ({})); | ||
| throw new Error((body as { error?: string }).error ?? `Request failed (${res.status})`); | ||
| } | ||
|
|
||
| return res.json() as Promise<RuntimeResponse>; |
There was a problem hiding this comment.
getRuntime() currently throws for any non-2xx response. The runtime endpoint intentionally uses 503 to signal “not ready” while still returning a structured { provider, ready: false, reason } payload. Treating 503 as an exception here loses the reason and makes the UI show a generic “Request failed (503)” instead of the real readiness status. Consider allowing 200 and 503 to both parse into RuntimeResponse (only throw for other status codes / invalid JSON).
Summary
This PR delivers the StackForge web dashboard end-to-end and makes agent execution production-realistic with provider readiness checks.
Problem
The dashboard experience existed conceptually, but running in true real-agent mode could fail late and feel ambiguous to users. There was no explicit runtime readiness signal before triggering generation.
Solution
This PR separates concerns across UI, web app, API runtime, tests, and docs.
Changes
🧩 UI Foundation
packages/ui) to standardize dashboard look and behaviorButton,Card,Badge,Spinner, andToast🖥️ Web Dashboard
⚙️ API Runtime Readiness & Provider Behavior
openrouter,mock, andautomodesGET /runtime/statusendpoint to expose provider and readiness state.env.exampleto include provider mode configuration🧪 Quality & Docs
Commits
feat(ui)feat(web)feat(api)test(api)docsTesting
Notes
Checklist