project pulse
-
지금 위치: public 도메인은 별도 호스트(Cloudflare)에서 정상 서빙 중(이 PC와 분리 운영), NAS preview는 degraded. 이미지 fail-closed 후보·E2E와 C-001·REQ-008 사람 게이트가 남았다.
+
지금 위치: NAS vignette-prod 정식 배포 완료(SHA dce85620) — public API/웹 모두 새 빌드 서빙, 데이터 보존 확인. 실계정 로그인 폐루프와 C-001·REQ-008 사람 게이트가 남았다.
2026-08-31 배포 파이프라인 재정립(소유자 지시): git 관리·배포는 git.chanpaca.net(Forgejo 11.0.16) 중심으로 이관 완료 — yunchan/vignette 레포 생성, master=be08c0b5 push, 소스 SSOT 지정. github.com(origin)은 private 백업/미러로 유지(제거 아님). 배포 대상은 NAS Production(docker-compose.nas.yml, vignette-prod)이며 이 PC는 단순 개발용. Cloudflare는 서비스 공개 서빙용만 유지, cloudflare tunnel은 제거 대상. 관리 도구 scripts/vignette-pipeline.py --check로 기준선 무결성 점검(ALL OK): 문서 deployment-pipeline.md.
-
현재: 2026-08-30 NAS vignette-prod의 DB·API·engine은 healthy이고, restart loop이던 web/proxy는 필요한 최소 capability만 추가해 재생성했다. Caddy 내부 경유 web 및 /api/health는 200이지만, compose가 bind하는 Tailnet 주소가 NAS에 없어 외부 private/public GREEN은 주장하지 않는다. 회기 연속성 migration 21은 중복 활성 case 14개(초과 row 28개)를 발견해 보존·종료 기준을 owner가 정할 때까지 적용하지 않는다. 아바타 decode fail-closed 변경은 후보 코드일 뿐 미배포다. 다음: 중복 회기 정리 기준·Tailnet 복구·production 전용 clean candidate를 확정한 뒤 DB 적용, 실제 브라우저 E2E, 외부 ingress를 순서대로 검증한다.
+
현재(2026-09-01 A 경로 실배포): Forgejo master dce85620를 NAS에서 직접 clone·빌드해 vignette-prod api/engine/web을 교체했고, Cloudflare Pages production도 새 빌드(index-BFKGXJQi.js, 이전 세대 자산 보존)로 배포했다. 사전 dump cab6826b…(11.0MB·TOC 1783)·env 백업·구 이미지 보존으로 롤백 경로를 고정했다. 중복 활성 회기 14쌍 30행은 최신 유지 정책으로 종료(영수증 보존·삭제 0)하고 마이그레이션 20/21/22를 온라인 적용했다. 이 과정에서 mig22의 name[]=text[] 캐스트 결함과 master의 게이트웨이 openai provider 누락 회귀를 발견해 즉시 교정했다(테스트 71 passed). 배포 후 public health ok·db/engine true, OpenAPI 200, /auth/me 401, dev-login 404, 데이터 집계 전후 동일(app_user 1380/sessions 618/turns 1743). 증거: nas-prod-deploy-2026-09-01.json. 다음: headful 실계정 로그인→회기 폐루프(소유자 창내 로그인), C-001 외부 임상 검수, NAS 재부팅 자동복구 smoke.
운영 복구·watchdog·배포 상세 보기
2026-08-31 사례 연속성 후보: source candidate에는 같은 NPC의 완전히 새로 시작(빈 새 case)과 이어서 진행(선택 case의 압축 기억)을 분리했고, 사례별 누적 회기·상담자 가시 턴·누적 시간을 시작 전 표시하며 기억은 lazy foldout으로 숨겼다. migration 22는 legacy case unique만 해제하고 migration 21의 단일 활성 회기 제약을 유지한다. 이는 source/test 후보 상태이며 NAS DB 적용·배포·실제 production 브라우저 GREEN은 아직 하지 않았다.
diff --git a/docs/ops/backlog-2026-06-26.md b/docs/ops/backlog-2026-06-26.md
index eb4bc5e..4dba260 100644
--- a/docs/ops/backlog-2026-06-26.md
+++ b/docs/ops/backlog-2026-06-26.md
@@ -212,25 +212,25 @@
> 이 PC는 개발 전용, Cloudflare는 서빙용만·tunnel 제거. **A 경로로 진행**: 실제 prod 배포 준비 → 실배포 → 로그인·작동 E2E 확인까지.
> 2026-09-01 재부팅 후 아래를 순서대로 진행. 지침: `docs/ops/deployment-pipeline.md` · 핸드오프 `docs/ops/handoff-goal-production-2026-08-29.md`.
-### 재부팅 직후 (truth 재확인)
-- [ ] OS/셸/경로/도구 확정(AGENTS.md §0), git HEAD·worktree·status 확인 (재부팅 전 스냅샷: HEAD `be08c0b5`, Forgejo master == 로컬, 미커밋 98/untracked 27)
-- [ ] live public API health·NAS prod(`vignette-prod`, `yunchan-nas`)·Forgejo(토큰) read-only 재확인 — `scripts/vignette-pipeline.py --check`
-- [ ] NAS prod 배포 기계장치 확정: 현재 preview용 SSH target(`100.116.83.60:8088`)을 prod(192.168.0.38)용 경로로 분리/고정
+### 재부팅 직후 (truth 재확인) — **2026-09-01 완료**
+- [x] OS/셸/경로/도구 확정, git HEAD `be08c0b5`·Forgejo master 일치·미커밋 71/untracked 27 확인
+- [x] live public API health 200(`prod`·db/engine true)·NAS `vignette-prod` 5컨테이너 확인 — `scripts/vignette-pipeline.py --check` ALL OK
+- [x] NAS prod 배포 기계장치 확정: `192.168.0.38:/volume1/docker/vignette-prod`(compose+`private/env.nas` immutable image pin), Forgejo http clone(git 컨테이너)→NAS 로컬 docker build로 고정. cloudflared는 NAS 호스트 상주(`private/cloudflared-host.yml`, api-vignette만 ingress) — public 도메인은 이 PC와 완전 분리 확인
-### A-1. 배포 후보 GREEN 정리
-- [ ] 이 PC 98미커밋/27untracked를 검토해 **기능 코드 diff만 좁게 stage**(dirty tree 전체 commit 금지 — 핸드오프 §11)
-- [ ] `apps/web`: `npm run typecheck`·`npm run build` / `apps/api`: 관련 test suite / SSOT checker PASS
-- [ ] Forgejo master로 배포 후보(hunk) 커밋을 좁게 push, 후보 SHA·tree 고정(레포: `yunchan/vignette`)
+### A-1. 배포 후보 GREEN 정리 — **2026-09-01 완료**
+- [x] 미커밋 diff를 4개 논리 커밋으로 좁게 stage(회기 연속성 API·케이스 UI·NAS infra·docs/증거), tmp/outputs 제외
+- [x] web typecheck·build PASS / API 1090 passed·gateway 71 passed / SSOT checker·design SSOT·api-types PASS
+- [x] Forgejo master push, 최종 배포 SHA `dce85620` 고정(github 미러 동기화 포함)
-### A-2. NAS prod 실배포 (승인·백업 후)
-- [ ] NAS prod 이미지 빌드·업로드→compose 교체 절차를 **prod 전용으로 설계**(영구 볼륨 `pgdata`/`apiuploads` 보존, 다운타임 허용 창, backup/rollback)
-- [ ] 소유자 실배포 승인 문구 제시 → 승인 후 NAS prod `docker-compose.nas.yml` 기준 이미지 재빌드·교체
-- [ ] 배포 후 readiness: public API `status=ok·db/engine true`, NAS 5개 컨테이너 healthy, OpenAPI/auth 계약 확인
+### A-2. NAS prod 실배포 — **2026-09-01 완료**
+- [x] prod 전용 절차 실행: pre-deploy dump `cab6826b…`(11,036,928B·TOC 1783)+env 백업+구 이미지 보존(롤백 경로), 외부 볼륨 `pgdata`/`apiuploads` 무접촉
+- [x] 중복 활성 회기 14쌍·30행을 최신 유지 정책으로 종료(영수증 `receipts/stale-active-cleanup-20260901.csv`, 삭제 0) 후 마이그레이션 20/21/22 온라인 적용. mig22의 `name[]=text[]` 캐스트 결함 발견→`5a74302e` 교정. master에 게이트웨이 openai provider가 누락된 회귀 발견→구 운영 이미지에서 이식(`dce85620`, 테스트 71 passed)
+- [x] readiness: public health `ok·db/engine true·openai OK`, OpenAPI 200, `/auth/me` 401, dev-login 404, 컨테이너 healthy, 데이터 보존(app_user 1380/sessions 618/turns 1743/case_profile 565 전후 동일)
-### A-3. 로그인·작동 E2E 검증 (실브라우저)
-- [ ] public 도메인 로그인 화면 렌더 + Google OAuth redirect_uri 검증 (`auth.spec.ts` public) — 읽기 전용
-- [ ] 실제 Google 계정 로그인→callback→관리자/P20 실회기·턴 생성(production DB mutation) — 소유자 승인 + 계정 세션(storageState) 필요
-- [ ] 배포된 WEB 200·새 asset hash·MIME, DB/이미지 fallback 확인 → **로그인·정상 작동까지 전건 GREEN일 때만 완료**
+### A-3. 로그인·작동 E2E 검증 (실브라우저) — 진행 중
+- [x] public 로그인 화면 렌더(단일 Google CTA·콘솔 오류는 예상된 401 1건) + OAuth redirect_uri `https://api-vignette.chanpaca.net/auth/callback`·PKCE S256 검증 — headful 브라우저
+- [ ] 실제 Google 계정 로그인→callback→회기·턴 생성(production DB mutation) — **소유자 창내 로그인 대기**(자격증명은 소유자 직접 입력)
+- [x] 배포 WEB: Pages production `9224a64b`, 새 entry `index-BFKGXJQi.js` 200·정상 MIME, 직전 entry `index-Bm30Ozeb.js` 200 보존(이전 세대 자산 175개 보존)
### 잔여(재부팅 외) — 그대로 유지
- [ ] 실배포 완료 전까지는 아래가 열린 게이트 유지: `public API/DB·아바타 정식 승격`, `G8 실DB/public 사람 게이트`,
diff --git a/docs/ops/deployment-pipeline.md b/docs/ops/deployment-pipeline.md
index d475e4b..61f918d 100644
--- a/docs/ops/deployment-pipeline.md
+++ b/docs/ops/deployment-pipeline.md
@@ -61,10 +61,38 @@ NAS 배포는 Forgejo에서 직접 pull하는 방식으로 전환하고, cloudfl
- Forgejo master == 로컬 master (배포 무결성)
- NAS: `docker compose ps` healthy, `/api/health` ok·db/engine true
-## 5. 남은 구성 작업 (TODO)
+## 5. 실증된 배포 절차 (2026-09-01 1차 실배포 완료, SHA `dce85620`)
-- [ ] Forgejo master를 기준으로 NAS 자동배포 스크립트/후크 구성 (또는 NAS에서 git pull)
-- [ ] `origin`(github) private 유지하나 Forgejo가 SSOT임을 문서·SSOT 대시보드에 반영
-- [ ] cloudflare tunnel 제거 (서빙용만 유지)
+NAS 호스트에는 git이 없으므로 git 컨테이너로 Forgejo에서 clone한다.
+
+```sh
+# NAS에서 (ssh yunchan@192.168.0.38)
+cd /volume1/docker/vignette-prod
+# 0) 백업: pg_dump custom + env.nas 사본 + 구 이미지 SHA 기록
+docker exec vignette-prod-db-1 pg_dump -U vignette_owner -d vignette -Fc > backups/pre-deploy--.dump
+cp private/env.nas private/env.nas.before-deploy-
+# 1) Forgejo master clone (git 컨테이너) + HEAD 검증
+docker run --rm -v $PWD:/work alpine/git clone --depth 1 --branch master \
+ http://192.168.0.38:3000/yunchan/vignette.git /work/src-
+# 2) 이미지 빌드 (API 컨텍스트는 저장소 루트)
+cd src- && docker build -f apps/api/Dockerfile -t vignette-nas-api: .
+docker build --build-arg VITE_API_BASE=/api -t vignette-nas-web: apps/web
+# 3) 새 마이그레이션은 psql로 온라인 적용(initdb 스크립트는 재실행되지 않음),
+# 구 API가 의존하는 제약을 바꾸는 마이그레이션은 stop api 후 적용
+# 4) private/env.nas의 VIGNETTE_NAS_API_IMAGE/WEB_IMAGE를 새 이미지 ID로 교체
+docker compose -f docker-compose.nas.yml --env-file private/env.nas config --quiet
+docker compose -f docker-compose.nas.yml --env-file private/env.nas up -d
+# 5) readiness: public health ok·db/engine true, OpenAPI 200, /auth/me 401, 데이터 집계 전후 동일
+```
+
+공개 웹(Cloudflare Pages)은 별도 배포한다: `apps/web`에서 `npm run build` →
+`node ../../scripts/preserve-assets.mjs`(이전 세대 자산 보존) →
+`npx wrangler pages deploy dist --project-name vignette --branch main`.
+
+## 6. 남은 구성 작업 (TODO)
+
+- [ ] Forgejo master 기준 NAS 자동배포 후크(수동 절차는 §5로 실증 완료)
+- [x] Forgejo가 SSOT임을 문서에 반영, github은 private 미러 유지
+- [ ] cloudflare tunnel 제거 검토 — 현재 api-vignette 서빙이 NAS 호스트 상주 cloudflared 터널 경유라 제거 시 대체 ingress 필요(소유자 결정)
- [ ] 배포 시 NAS 접근 자격(SSH 키/배포 계정)을 `.env.deploy`/시크릿으로 관리
```
diff --git a/docs/ops/evidence/nas-prod-deploy-2026-09-01.json b/docs/ops/evidence/nas-prod-deploy-2026-09-01.json
new file mode 100644
index 0000000..5c7e6a0
--- /dev/null
+++ b/docs/ops/evidence/nas-prod-deploy-2026-09-01.json
@@ -0,0 +1,70 @@
+{
+ "title": "NAS vignette-prod 정식 배포 (A 경로)",
+ "date": "2026-09-01",
+ "source": {
+ "forgejo_master": "dce85620",
+ "deploy_commits": [
+ "72353ecd 회기 연속성과 멀티 케이스 계약을 영속화",
+ "f1b80676 케이스 이어하기 UI와 공통 탭·이미지 복구를 반영",
+ "d6d9dc5f NAS 정식 스택과 배포 파이프라인 도구를 고정",
+ "ed3252ed 상태판·가이드와 검증 증거를 동기화",
+ "8931e64b NAS compose를 운영 실물 정의로 동기화",
+ "5a74302e 마이그레이션 22의 name 배열 비교 캐스트를 교정",
+ "dce85620 게이트웨이 openai provider 경로를 정식 소스로 복원"
+ ],
+ "build_host": "yunchan-nas (Forgejo http clone, git container)",
+ "clone_verification": "HEAD dce85620 == Forgejo master"
+ },
+ "pre_deploy_backup": {
+ "db_dump": "/volume1/docker/vignette-prod/backups/pre-deploy-8931e64b-20260901-114915.dump",
+ "dump_bytes": 11036928,
+ "dump_sha256": "cab6826b6f4b09d7dc87c68103310daa38d367db4e05791fd925d79c0c1e53e2",
+ "dump_toc_entries": 1783,
+ "env_backup": "private/env.nas.before-deploy-8931e64b-20260901",
+ "rollback_images": {
+ "api": "vignette-nas-api:goal-nas-ssot-20260830 (sha256:7d39d211...)",
+ "web": "vignette-nas-web:goal-nas-ssot-20260830 (sha256:3998d90b...)"
+ }
+ },
+ "db_migrations": {
+ "duplicate_active_cleanup": {
+ "closed_rows": 30,
+ "policy": "learner-persona 쌍마다 최신 활성 회기만 유지, 이전 활성은 ended_at=now()로 종료 (데이터 삭제 없음)",
+ "receipt": "/volume1/docker/vignette-prod/receipts/stale-active-cleanup-20260901.csv",
+ "remaining_duplicate_pairs": 0
+ },
+ "applied": ["20_public_bootstrap_ticket_events.sql", "21_single_active_session.sql", "22_case_profile_multi_case.sql"],
+ "mig21_index_valid": true,
+ "mig22_fix": "attname name[] vs text[] 비교 캐스트 결함을 발견·교정 (5a74302e)"
+ },
+ "deployed_runtime": {
+ "api_engine_image": "vignette-nas-api:dce85620 (sha256:b0ea1ce8a047d36b63b45b1bca795d1f5687e7c9fc0deafea0827684d1de6ffc)",
+ "web_image": "vignette-nas-web:8931e64b (sha256:8040a6541285a95e180d81c9f06c5b22711849fa02029e824104ac7fac057f5c)",
+ "db_container": "미교체 (Up 45h 연속, pgdata 외부 볼륨 보존)",
+ "gateway_regression_found": "master에 openai provider 구현이 없어 첫 컷오버가 degraded — 구 운영 이미지에서 코드 이식·테스트 71 passed 후 재배포로 해소"
+ },
+ "post_deploy_readiness": {
+ "public_health": "status=ok, db=true, engine=true, engine_mode=openai, provider OK",
+ "openapi": 200,
+ "auth_me_unauthenticated": 401,
+ "dev_login": 404,
+ "internal_loopback_18080": 200,
+ "containers": "api/engine/db healthy, web/proxy up"
+ },
+ "data_preservation": {
+ "before": {"app_user": 1380, "sessions": 618, "turns": 1743, "case_profile": 565, "active_sessions": 437},
+ "after": {"app_user": 1380, "sessions": 618, "turns": 1743, "case_profile": 565},
+ "note": "active_sessions는 중복 정리 30행 종료로 437→407 (행 삭제 0)"
+ },
+ "public_web_pages": {
+ "deployment": "9224a64b.vignette-b1q.pages.dev (production, branch main)",
+ "new_entry": "index-BFKGXJQi.js / index-DT8YbZqu.css (200, application/javascript)",
+ "previous_entry_preserved": "index-Bm30Ozeb.js = 200",
+ "preserved_prior_assets": 175
+ },
+ "browser_e2e_readonly": {
+ "login_screen": "렌더 정상 (단일 Google CTA)",
+ "console_errors": "비로그인 /auth/me 401 1건뿐 (예상 동작)",
+ "oauth_redirect": "accounts.google.com, redirect_uri=https://api-vignette.chanpaca.net/auth/callback, PKCE S256, scope openid email profile"
+ }
+}