vignette/docs/dev_dashboard.html

391 lines
21 KiB
HTML

<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Vignette 개발 대시보드</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/variable/pretendardvariable.min.css">
<style>
:root {
--paper:#FBFAF8;
--paper-2:#F4F2EE;
--surface:#FFFFFF;
--hair:#E5E1DA;
--ink:#1C2A2A;
--ink-2:#5A6663;
--ink-3:#93A09C;
--accent:#3E7A6E;
--accent-deep:#316257;
--accent-tint:#EEF4F2;
--clay:#B0735C;
--pos:#2C6E49;
--warn:#8A5A00;
--crit:#A8352C;
--pos-bg:#E8F3EC;
--warn-bg:#FAF1E0;
--crit-bg:#FAEAE8;
--info-bg:#EAF1F6;
--font:"Pretendard Variable", Pretendard, -apple-system, "Apple SD Gothic Neo", "Noto Sans KR", system-ui, sans-serif;
--num:"Pretendard Variable", "JetBrains Mono", ui-monospace, monospace;
--radius:8px;
}
* { box-sizing:border-box; }
body {
margin:0;
background:var(--paper);
color:var(--ink);
font-family:var(--font);
line-height:1.55;
letter-spacing:0;
}
header {
position:sticky;
top:0;
z-index:2;
display:flex;
align-items:center;
justify-content:space-between;
gap:24px;
min-height:58px;
padding:0 32px;
background:rgba(255,255,255,.92);
border-bottom:1px solid var(--hair);
backdrop-filter:blur(10px);
}
.brand { display:flex; align-items:center; gap:10px; font-weight:760; }
.mark { width:24px; height:24px; border:2px solid var(--accent); border-radius:50%; position:relative; }
.mark:after { content:""; width:6px; height:6px; border-radius:50%; background:var(--clay); position:absolute; left:7px; top:6px; }
.stamp { color:var(--ink-3); font:600 12px/1 var(--num); }
main { width:min(1180px, calc(100% - 40px)); margin:0 auto; padding:42px 0 64px; }
.hero { display:grid; grid-template-columns:minmax(0,1.2fr) minmax(280px,.8fr); gap:28px; align-items:start; margin-bottom:34px; }
.kicker { display:flex; align-items:center; gap:8px; color:var(--accent); font:700 12px/1.3 var(--num); letter-spacing:.08em; text-transform:uppercase; }
.kicker:before { content:""; width:6px; height:6px; border-radius:50%; background:var(--accent); }
h1 { margin:12px 0 0; font-size:30px; line-height:1.28; letter-spacing:0; }
h1 em { color:var(--accent-deep); font-style:normal; }
.lead { margin:12px 0 0; color:var(--ink-2); max-width:760px; font-size:15px; }
.summary {
display:grid;
grid-template-columns:repeat(2,minmax(0,1fr));
gap:1px;
overflow:hidden;
border:1px solid var(--hair);
border-radius:12px;
background:var(--hair);
}
.metric { min-width:0; padding:18px; background:var(--surface); }
.metric span { display:block; color:var(--ink-3); font-size:12px; font-weight:650; }
.metric b { display:block; margin-top:6px; color:var(--ink); font:760 28px/1 var(--num); }
section { margin-top:30px; }
.section-head { display:flex; align-items:end; justify-content:space-between; gap:18px; margin-bottom:14px; }
.section-head h2 { margin:0; font-size:20px; line-height:1.3; }
.section-head p { margin:4px 0 0; color:var(--ink-3); font-size:13px; }
.grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:16px; }
.panel {
min-width:0;
background:var(--surface);
border:1px solid var(--hair);
border-radius:12px;
padding:20px;
box-shadow:0 1px 2px rgba(28,42,42,.05);
}
.panel h3 { margin:0 0 10px; font-size:16px; line-height:1.35; }
.panel p { margin:0; color:var(--ink-2); font-size:13px; }
.tag { display:inline-flex; align-items:center; gap:6px; margin-bottom:12px; padding:3px 9px; border-radius:999px; font-size:12px; font-weight:680; }
.tag:before { content:""; width:7px; height:7px; border-radius:50%; }
.done { color:var(--pos); background:var(--pos-bg); }
.done:before { background:var(--pos); }
.doing { color:var(--warn); background:var(--warn-bg); }
.doing:before { background:var(--warn); }
.risk { color:var(--crit); background:var(--crit-bg); }
.risk:before { background:var(--crit); }
.info { color:#2E5A7A; background:var(--info-bg); }
.info:before { background:#3C7299; }
.list { display:grid; gap:10px; margin:0; padding:0; list-style:none; }
.list li { display:grid; grid-template-columns:10px minmax(0,1fr); gap:10px; color:var(--ink-2); font-size:13px; }
.list li:before { content:""; width:6px; height:6px; margin-top:8px; border-radius:50%; background:var(--accent); }
code { font-family:var(--num); font-size:12px; background:var(--paper-2); padding:2px 6px; border-radius:6px; }
.wide { grid-column:1 / -1; }
.commands { display:grid; gap:10px; }
pre { margin:0; overflow:auto; padding:14px; border-radius:8px; background:#1E2724; color:#F6F0EC; font:12px/1.6 var(--num); }
table { width:100%; border-collapse:collapse; background:var(--surface); border:1px solid var(--hair); border-radius:12px; overflow:hidden; display:table; }
th,td { padding:12px 14px; border-bottom:1px solid var(--hair); text-align:left; vertical-align:top; font-size:13px; }
th { color:var(--ink-3); font-weight:700; background:var(--paper-2); }
tr:last-child td { border-bottom:0; }
@media (max-width:900px) {
header { padding:0 20px; }
main { width:min(100% - 28px, 1180px); padding-top:28px; }
.hero { grid-template-columns:1fr; }
.grid { grid-template-columns:1fr; }
}
@media (max-width:520px) {
.summary { grid-template-columns:1fr; }
h1 { font-size:24px; }
.stamp { display:none; }
}
</style>
</head>
<body>
<header>
<div class="brand"><span class="mark" aria-hidden="true"></span><span>Vignette 개발 대시보드</span></div>
<div class="stamp">2026-06-26 15:18 KST</div>
</header>
<main>
<div class="hero">
<div>
<div class="kicker">team lead review</div>
<h1>Phase 0는 <em>검증 가능한 운영 안전장치와 E2E 안정화</em>까지 마무리됐다.</h1>
<p class="lead">
이번 세션은 seed persona 결합 제거, 비개발 fail-closed 설정, compose OAuth 주입,
admin 모바일 overflow 수정, persona 인증 경계 보강, 전체 Playwright 재통과, 공개 API prod 전환까지 완료했다.
이후 로컬 5175 런타임 재기동, admin 사용자 관리 E2E 안정화, 학습자 홈 0회 기록 UX,
active session 실제 SSE 스트림 전환, 텍스트/음성 실패 턴 미저장, 직접 URL persona 우회 차단, Live2D demo 자산 제거,
AI 운영 fail-closed 보강까지 전체 Playwright로 확인했다.
2026-06-26 15:18 KST 기준 공개 API 터널을 복구한 뒤 전체 Playwright 72개가 desktop/mobile/single-run 모두 통과했다.
Cloudflare Pages production 배포 `0fe8f3cc`도 완료했고, 커스텀 도메인이 새 asset을 서빙하는 것을 확인했다.
과거 Mao/Haru Live2D demo 자산은 repo와 번들에서 제거했고, `/live2d/*` 운영 경로는 404로 차단했다.
로컬 프로세스는 prod API 8001, engine gateway 9099, cloudflared tunnel 1개만 남기고 dev API와 임시 Vite preview를 내렸다.
공개 Google OAuth 실제 `/turn` 증명만 storageState 캡처 후 남아 있다.
</p>
</div>
<div class="summary" aria-label="현재 검증 요약">
<div class="metric"><span>Full Playwright</span><b>72 passed</b></div>
<div class="metric"><span>Pages Production</span><b>0fe8f3cc</b></div>
<div class="metric"><span>Public API</span><b>prod OK</b></div>
<div class="metric"><span>Backend unittest</span><b>20 OK</b></div>
<div class="metric"><span>App Commit</span><b>543d48c</b></div>
<div class="metric"><span>Decision</span><b>조건부 GO</b></div>
</div>
</div>
<section>
<div class="section-head">
<div>
<h2>팀장 판정</h2>
<p>이 HTML만 보고 판단해야 하는 항목</p>
</div>
</div>
<div class="grid">
<article class="panel">
<span class="tag done">go</span>
<h3>로컬 회귀와 공개 prod-safe 게이트 통과</h3>
<p>백엔드 unittest 20개, Python compile, 웹 production build, 전체 Playwright 72개가 통과했다. 공개 API도 `environment:"prod"`, `db:true`, `engine:true`로 응답한다.</p>
</article>
<article class="panel">
<span class="tag done">deployed</span>
<h3>커스텀 도메인 운영 반영 완료</h3>
<p>Cloudflare Pages production `0fe8f3cc`이 `543d48c`로 배포됐고, `https://vignette.chanpaca.net`이 `index-D1Z5DH5J.js`와 `index-BA9-xxfn.css`를 서빙한다.</p>
</article>
<article class="panel">
<span class="tag doing">conditional</span>
<h3>운영 반영 전 남은 증거 1개</h3>
<p>실제 Google 로그인 쿠키가 필요한 공개 `/turn` smoke는 아직 storageState가 없어 미완이다. 로그인 가능한 계정으로 한 번 캡처하면 바로 검증 가능하다.</p>
</article>
<article class="panel">
<span class="tag info">mock audit</span>
<h3>런타임 목업 제거 기준</h3>
<p>프론트 페이지는 실제 API/DB 경로를 탄다. 남은 `mock` 표기는 테스트 코드의 `unittest.mock` 또는 의도적 fixture이며, seed fallback은 prod/staging에서 fail-closed 된다.</p>
</article>
</div>
</section>
<section>
<div class="section-head">
<div>
<h2>완료</h2>
<p>새 세션에서 반복하지 않아도 되는 작업</p>
</div>
</div>
<div class="grid">
<article class="panel">
<span class="tag done">E2E persona decoupled</span>
<h3>DB persona 기반 테스트</h3>
<p>`session-persistence`, `teacher`, `voice`는 `/api/personas`에서 실제 DB persona를 가져온다.</p>
</article>
<article class="panel">
<span class="tag done">fail-closed</span>
<h3>비개발 런타임 보호</h3>
<p>prod/staging에서 dev login, seed fallback, OAuth 누락, 기본 secret, localhost CORS를 거부한다.</p>
</article>
<article class="panel">
<span class="tag done">infra</span>
<h3>compose OAuth 주입</h3>
<p>`ENVIRONMENT`, Google OAuth vars, `CORS_ORIGINS`, `OPENAI_BASE_URL`을 API 컨테이너에 명시했다.</p>
</article>
<article class="panel">
<span class="tag done">admin UX</span>
<h3>모바일 overflow 수정</h3>
<p>긴 readiness detail을 안정화했고, 사용자 변경은 DB 저장소가 durable할 때만 허용한다. AI 운영은 실제 어댑터명과 health 상태를 표시한다.</p>
</article>
<article class="panel">
<span class="tag done">test isolation</span>
<h3>global engine config 분리</h3>
<p>`db-persistence`는 전역 engine config를 바꾸므로 `@single-run`으로 분리했다.</p>
</article>
<article class="panel">
<span class="tag done">docs</span>
<h3>handoff 갱신</h3>
<p>`docs/HANDOFF.md`, `docs/dev_dashboard.html`, `apps/web/e2e/README.md`를 최신 상태로 맞췄다.</p>
</article>
<article class="panel">
<span class="tag done">public prod</span>
<h3>공개 API prod 전환</h3>
<p>`cloudflared` ingress를 `127.0.0.1:8001` prod API로 돌렸고 public readiness gate가 통과했다.</p>
</article>
<article class="panel">
<span class="tag done">ops script</span>
<h3>공개 런타임 재기동</h3>
<p>`scripts/start-public-runtime.ps1`와 `scripts/stop-public-runtime.ps1`로 API 8001과 cloudflared를 재현 가능하게 만들었다.</p>
</article>
<article class="panel">
<span class="tag done">auth boundary</span>
<h3>persona catalog 인증화</h3>
<p>`/personas`는 이제 비로그인 401이다. 로컬과 공개 API 모두 직접 확인했고 E2E 회귀를 추가했다.</p>
</article>
<article class="panel">
<span class="tag done">public web</span>
<h3>API base fallback 보강</h3>
<p>public 웹에서 `/api/auth/config`가 SPA HTML로 떨어지지 않도록 public API origin fallback을 추가했다.</p>
</article>
<article class="panel">
<span class="tag done">learner UX</span>
<h3>0회 기록 상태 보강</h3>
<p>학습자 홈은 이제 fake 기록 없이 실제 서버 세션 0개를 `기존 회기` 빈 상태와 0 카운트로 표시한다.</p>
</article>
<article class="panel">
<span class="tag done">local login</span>
<h3>5175 로컬 재기동 검증</h3>
<p>`127.0.0.1:5175` web, `8000` API, `9099` engine을 재기동하고 dev-login 서버 세션과 `/learn` redirect를 확인했다.</p>
</article>
<article class="panel">
<span class="tag done">session UX</span>
<h3>dense viewport 보정</h3>
<p>375px급 active session에서 stage 내부가 1px 넘게 잘리던 문제를 avatar/padding 조정으로 제거했다.</p>
</article>
<article class="panel">
<span class="tag done">real stream</span>
<h3>텍스트 턴 SSE 전환</h3>
<p>`POST /sessions/{id}/stream` token 이벤트로만 내담자 자막을 쌓고, HTTP 거절과 SSE error 모두 실패 턴을 transcript와 저장소에 남기지 않는다.</p>
</article>
<article class="panel">
<span class="tag done">voice turn</span>
<h3>음성 실패 턴 미저장</h3>
<p>음성 회기도 엔진 응답 생성 성공 후에만 learner/client turn을 저장한다. 실패한 transcript는 pending 상태에서 제거한다.</p>
</article>
<article class="panel">
<span class="tag done">ops copy</span>
<h3>비영구 상태 문구 정리</h3>
<p>admin health와 voice degraded reason은 내부 fallback/seed 표현 대신 비영구 런타임 상태를 사용자용 문구로 표시한다.</p>
</article>
<article class="panel">
<span class="tag done">route guard</span>
<h3>직접 URL persona 차단</h3>
<p>`/learn/session/:code`로 직접 들어와도 DB 원본 persona가 아니면 회기 시작을 막는다. degraded/seed fallback 회귀를 추가했다.</p>
</article>
<article class="panel">
<span class="tag done">avatar</span>
<h3>demo Live2D 자산 제거</h3>
<p>Mao/Haru 샘플과 Pixi/Cubism 런타임을 공개 배포물에서 제거했다. 세션 화면은 SVG persona avatar만 렌더링한다.</p>
</article>
</div>
</section>
<section>
<div class="section-head">
<div>
<h2>검증</h2>
<p>마지막으로 직접 실행한 결과</p>
</div>
</div>
<table>
<thead><tr><th>범위</th><th>명령</th><th>결과</th></tr></thead>
<tbody>
<tr><td>Web typecheck</td><td><code>npm run typecheck</code></td><td>Passed</td></tr>
<tr><td>Backend</td><td><code>python -m unittest app.test_runtime_policy app.test_session_turn_persistence engine_gateway.test_gateway_model</code></td><td>20 tests OK</td></tr>
<tr><td>Compile</td><td><code>python -m compileall app engine_gateway</code></td><td>Passed</td></tr>
<tr><td>Web build</td><td><code>npm run build</code></td><td>Passed</td></tr>
<tr><td>Pages production deploy</td><td><code>wrangler pages deploy dist --project-name vignette --branch main</code></td><td>`0fe8f3cc`, source `543d48c`</td></tr>
<tr><td>Custom domain assets</td><td><code>https://vignette.chanpaca.net/</code></td><td>`index-D1Z5DH5J.js`, `index-BA9-xxfn.css`</td></tr>
<tr><td>Legacy Live2D routes</td><td><code>/live2d/mao/* / /live2d/haru/* / /live2d/live2dcubismcore.min.js</code></td><td>404</td></tr>
<tr><td>Compose</td><td><code>docker compose --env-file .env.example config --quiet</code></td><td>Passed with dummy required env</td></tr>
<tr><td>Focused E2E</td><td><code>admin + db-persistence + voice-success</code></td><td>10 passed</td></tr>
<tr><td>Admin manage-users</td><td><code>admin.spec.ts --grep manage real server-known users</code></td><td>desktop/mobile 2 passed</td></tr>
<tr><td>Full E2E</td><td><code>PLAYWRIGHT_HOST=127.0.0.1 npx playwright test</code></td><td>72 passed</td></tr>
<tr><td>Public auth discovery</td><td><code>E2E_PUBLIC_AUTH=1 npx playwright test --list --project=chromium-public-auth</code></td><td>2 tests listed</td></tr>
<tr><td>Public readiness</td><td><code>public-auth-turn.spec.ts --grep production-safe</code></td><td>1 passed</td></tr>
<tr><td>Persona auth boundary</td><td><code>GET /personas</code></td><td>verified local/public unauth 401</td></tr>
<tr><td>Public login</td><td><code>auth.spec.ts --grep public login</code></td><td>1 passed</td></tr>
<tr><td>Public runtime script</td><td><code>scripts\start-public-runtime.ps1</code></td><td>engine 9099 + API 8001 + cloudflared only</td></tr>
<tr><td>Public API health</td><td><code>https://api-vignette.chanpaca.net/health</code></td><td>prod, db true, engine true</td></tr>
<tr><td>Local 5175 login</td><td><code>PLAYWRIGHT_BASE_URL=http://127.0.0.1:5175 auth.spec.ts</code></td><td>desktop/mobile passed</td></tr>
<tr><td>Learner/readiness E2E</td><td><code>learner.spec.ts + readiness.spec.ts desktop/mobile</code></td><td>14 passed</td></tr>
<tr><td>Learner screenshots</td><td><code>learn-empty-desktop.png / learn-empty-mobile-compact.png</code></td><td>document overflow 0, empty history visible</td></tr>
<tr><td>Session layout/turn</td><td><code>session-layout.spec.ts desktop/mobile</code></td><td>8 passed</td></tr>
<tr><td>Voice/session focused</td><td><code>voice + voice-success + session-layout</code></td><td>15 passed</td></tr>
<tr><td>Latest focused E2E</td><td><code>learner + session-layout + settings + admin</code></td><td>37 passed</td></tr>
</tbody>
</table>
</section>
<section>
<div class="section-head">
<div>
<h2>남은 리스크</h2>
<p>다음 세션 첫 작업</p>
</div>
</div>
<div class="grid">
<article class="panel">
<span class="tag risk">blocker</span>
<h3>public `/turn` proof 없음</h3>
<p>2026-06-26 15:18 KST 기준 Google 로그인 storageState가 없어 공개 API `/turn` 실제 성공 증거가 아직 없다.</p>
</article>
<article class="panel">
<span class="tag doing">ops</span>
<h3>공개 운영 프로세스 유지</h3>
<p>현재 실행 프로세스는 `127.0.0.1:9099` engine gateway, `127.0.0.1:8001` prod API, cloudflared tunnel 1개다. 재부팅 후에는 engine gateway와 prod API, cloudflared를 같이 재기동해야 한다.</p>
</article>
<article class="panel">
<span class="tag doing">check before deploy</span>
<h3>engine config 재확인</h3>
<p>E2E가 admin engine config를 건드린다. 배포 전 의도한 model/url/mode인지 다시 확인해야 한다.</p>
</article>
</div>
</section>
<section>
<div class="section-head">
<div>
<h2>다음 명령</h2>
<p>공개 OAuth smoke</p>
</div>
</div>
<div class="commands">
<pre>cd D:\workspace\vignette\apps\web
npx playwright codegen https://vignette.chanpaca.net/login --save-storage=.\node_modules\.tmp\public-auth.json
$env:E2E_PUBLIC_AUTH="1"
$env:E2E_PUBLIC_STORAGE_STATE=".\node_modules\.tmp\public-auth.json"
.\node_modules\.bin\playwright.cmd test e2e/public-auth-turn.spec.ts --project=chromium-public-auth</pre>
<pre>cd D:\workspace\vignette
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-public-runtime.ps1</pre>
</div>
</section>
<section>
<div class="section-head">
<div>
<h2>주의</h2>
<p>작업 방식</p>
</div>
</div>
<ul class="list">
<li>`frontenddesign` 스킬은 현재 세션에 없었다. 다음 세션에서 보이면 먼저 사용하고, 없으면 `docs/DESIGN_CONCEPT.md`를 따른다.</li>
<li>학습자 홈과 active session UX는 사용자가 강하게 불만을 말한 영역이다. fake 기록/통계 금지, 0회 기록은 실제 서버 세션 0개 기반 빈 상태로 유지, active session 전체화면/무문서스크롤 유지.</li>
<li>worktree는 원래부터 dirty다. 관련 없는 변경을 되돌리지 말 것.</li>
<li>OAuth secret은 출력하지 말 것. 실제 secret은 환경 파일/인프라 설정에만 둔다.</li>
</ul>
</section>
</main>
</body>
</html>