feat(my-deepagent): v0.2 PR #3 — FastAPI + SSE + minimal Web GUI (mydeepagent serve)
Closes the "GUI 미존재" gap from the user's first-session requirements (REPL + workflow + GUI). v0.2 PR #1's Postgres migration made a second concurrent writer safe; v0.2 PR #2a/#2b wired durable resume; this commit ships the HTTP + browser surface that uses them. No auth, no multi-tenant, single uvicorn worker — per DR-3 boundaries. v0.3+ will add auth, multi-worker fanout, LISTEN/NOTIFY SSE upgrade. Backend - `src/my_deepagent/api/`: - `app.py` create_app() factory. lifespan stores db/config/personas/ workflows on app.state. CORS allow_origin_regex http://localhost(:port)?. /static mount + /, /{page}.html for the HTML frontend. - `models.py` — pydantic v2 DTOs (extra="forbid") for every route. Auto OpenAPI/Swagger via FastAPI's response_model. - `deps.py` — get_db / get_config / get_personas / get_workflows. - `runner.py` — start_new_run / start_resume. Pre-allocates run_id via new `WorkflowEngine.run(pre_allocated_run_id=...)` so the route returns the id immediately while the engine runs in asyncio.create_task. - `sse.py` — 0.5 s poll over run_events.seq. Emits ServerSentEvent rows; sends `event: done` and HTTP-200-closes when run hits terminal. - `routes/{runs,personas,workflows,budget}.py`: GET /api/runs (list, ?limit + ?state) GET /api/runs/{id} (detail + phases + artifacts + events) POST /api/runs (start; mock-able via runner.start_new_run) POST /api/runs/{id}/resume POST /api/runs/{id}/abort GET /api/runs/{id}/events (SSE; Last-Event-ID header + ?last_event_id) GET /api/personas GET /api/workflows GET /api/budget CLI - `cli/serve.py` mydeepagent serve [--host 127.0.0.1] [--port 8000]. Loud stderr warning if --host is not loopback (no auth = footgun). uvicorn.run(factory=True, workers=1). - `cli/main.py` serve command registered. Static frontend (vanilla HTML/JS/CSS, no build system) - index.html — runs list + budget summary - new.html — start-run form (workflow select, repo path, requirements, per-role persona override) - run.html — run detail + live SSE event log + Resume/Abort buttons - app.js — fetch + EventSource. XSS policy HARDCODED at file top: textContent only, innerHTML/insertAdjacentHTML/outerHTML forbidden. - style.css — dark theme, single file. Engine - WorkflowEngine.run(... pre_allocated_run_id: UUID|None = None). None → uuid4() (existing behavior). Set → use that UUID. Backward compatible. Tests - tests/integration/test_api_read.py (5): list empty, get 404, personas seed count (12), workflows seed (>=3), budget empty. - tests/integration/test_api_write.py (5): missing template 400, extra field 422, resume 404, abort 404, mock-runner happy path. - tests/integration/test_api_sse.py (1): seed terminal run + 3 events, drain stream, assert types present + stream closes within 3 s. - tests/integration/test_api_static.py (5): index/new/run HTML 200, app.js content-type + XSS-policy substring assertion, style.css content-type. - All fixtures use httpx ASGITransport + app.router.lifespan_context (httpx does NOT auto-trigger FastAPI lifespan) + sqlite tmp_path. Gates - ruff check + ruff format --check + mypy --strict: PASS (120 source files) - pytest non-E2E: 603 PASS (12.15 s) — +16 from new API tests - pytest E2E real OpenRouter on Postgres: PASS 60.44 s (baseline 71–122 s range; well within DR-3 acceptance threshold ≤+20%) Manual browser verification deferred to a follow-up (docker compose up, mydeepagent serve, open http://localhost:8000). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
192
my-deepagent/static/style.css
Normal file
192
my-deepagent/static/style.css
Normal file
@@ -0,0 +1,192 @@
|
||||
/* my-deepagent Web GUI — v0.2 PR #3
|
||||
* Vanilla CSS. No framework. Single dark-friendly theme tuned for
|
||||
* data-heavy tables.
|
||||
*/
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Apple SD Gothic Neo",
|
||||
"Noto Sans KR", "Helvetica Neue", Arial, sans-serif;
|
||||
margin: 0;
|
||||
background: #0f1115;
|
||||
color: #e6e7eb;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
header {
|
||||
background: #1a1d24;
|
||||
padding: 1rem 2rem;
|
||||
border-bottom: 1px solid #2a2d36;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #f5f6f9;
|
||||
}
|
||||
|
||||
header nav a {
|
||||
color: #8db4ff;
|
||||
margin-left: 1rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
header nav a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.1rem;
|
||||
margin: 1.5rem 0 0.75rem;
|
||||
color: #f5f6f9;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: #161922;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
th, td {
|
||||
text-align: left;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-bottom: 1px solid #232633;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #1f2230;
|
||||
color: #c4c6d0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
td.state-completed { color: #8ee084; }
|
||||
td.state-running, td.state-executing { color: #f5d674; }
|
||||
td.state-failed, td.state-aborted { color: #f08585; }
|
||||
td.state-pending, td.state-created { color: #8db4ff; }
|
||||
|
||||
a {
|
||||
color: #8db4ff;
|
||||
}
|
||||
|
||||
button, .button {
|
||||
background: #2c3145;
|
||||
color: #e6e7eb;
|
||||
border: 1px solid #3a3f55;
|
||||
border-radius: 4px;
|
||||
padding: 0.4rem 0.9rem;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
button:hover, .button:hover {
|
||||
background: #353a54;
|
||||
}
|
||||
|
||||
button.danger {
|
||||
background: #4a2a2a;
|
||||
border-color: #5e3535;
|
||||
}
|
||||
|
||||
button.danger:hover {
|
||||
background: #5e3535;
|
||||
}
|
||||
|
||||
input, textarea, select {
|
||||
width: 100%;
|
||||
background: #1a1d24;
|
||||
color: #e6e7eb;
|
||||
border: 1px solid #2a2d36;
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-family: inherit;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin: 0.75rem 0 0.25rem;
|
||||
color: #c4c6d0;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: #6c7080;
|
||||
font-style: italic;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #161922;
|
||||
border: 1px solid #232633;
|
||||
border-radius: 4px;
|
||||
padding: 0.75rem;
|
||||
overflow-x: auto;
|
||||
font-size: 0.8rem;
|
||||
font-family: "SF Mono", "Monaco", "Cascadia Code", monospace;
|
||||
}
|
||||
|
||||
.events {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
background: #161922;
|
||||
border: 1px solid #232633;
|
||||
border-radius: 4px;
|
||||
padding: 0.75rem;
|
||||
font-family: "SF Mono", "Monaco", "Cascadia Code", monospace;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.event-line {
|
||||
margin-bottom: 0.25rem;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.event-line .ts { color: #6c7080; }
|
||||
.event-line .type { color: #8db4ff; font-weight: 500; }
|
||||
|
||||
.action-bar {
|
||||
margin: 1rem 0;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.budget-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.budget-line .scope { color: #c4c6d0; }
|
||||
.budget-line .amount { color: #8ee084; }
|
||||
.budget-line .amount.warn { color: #f5d674; }
|
||||
.budget-line .amount.over { color: #f08585; }
|
||||
|
||||
.error-banner {
|
||||
background: #4a2a2a;
|
||||
border: 1px solid #5e3535;
|
||||
border-radius: 4px;
|
||||
padding: 0.75rem 1rem;
|
||||
margin: 1rem 0;
|
||||
color: #f4c1c1;
|
||||
}
|
||||
Reference in New Issue
Block a user