"""SP-4 — Windows insta-render → NAS internal webhook. POST /api/internal/insta/update - X-Internal-Key 인증 필수 - task DB row update (status, progress, result_path, error) - result_path는 nginx 서빙 경로 (예: /media/insta/{slate_id}/01.png) - succeeded 시 params에서 slate_id 추출 → result_id 세팅 """ from __future__ import annotations import json import logging import os from typing import Optional from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from . import config, db from .auth import verify_internal_key logger = logging.getLogger(__name__) router = APIRouter() def _register_rendered_assets(slate_id: int) -> int: """워커가 저장한 10장 PNG를 card_assets로 등록. cutover(2026-05-19) 후 렌더는 Windows insta-render 워커가 NAS SMB 볼륨에 직접 쓰지만, NAS DB에 card_assets를 등록하는 단계가 누락됐었다. 이 함수가 그 갭을 메운다. 워커 출력 경로 후보를 순서대로 스캔해 실제 파일만 등록한다 (경로 정합 가드: CARDS_DIR 하위 / INSTA_DATA_PATH 직하 둘 다 수용). 저장하는 file_path는 insta-lab 컨테이너 내부 절대경로 → get_asset(FileResponse) / package(zip)가 그대로 읽는다. """ candidates = [ os.path.join(config.CARDS_DIR, str(slate_id)), # /app/data/insta_cards/{id} os.path.join(config.INSTA_DATA_PATH, str(slate_id)), # /app/data/{id} ] for base in candidates: if not os.path.isdir(base): continue count = 0 for page in range(1, 11): fp = os.path.join(base, f"{page:02d}.png") if os.path.exists(fp) and os.path.getsize(fp) > 0: db.add_card_asset(slate_id, page, fp) count += 1 if count: logger.info("card_assets 등록: slate=%s pages=%d dir=%s", slate_id, count, base) return count logger.warning("렌더 PNG를 찾지 못함: slate=%s (후보=%s)", slate_id, candidates) return 0 class UpdatePayload(BaseModel): task_id: str status: str = Field(..., description="processing|succeeded|failed") progress: int = Field(..., ge=0, le=100) result_path: Optional[str] = None error: Optional[str] = None @router.post( "/api/internal/insta/update", dependencies=[Depends(verify_internal_key)], ) def insta_update(payload: UpdatePayload): task = db.get_task(payload.task_id) if task is None: raise HTTPException(404, f"task not found: {payload.task_id}") result_id = None if payload.status == "succeeded": try: # DB stores params (not input_data) from create_task params_data = json.loads(task.get("params") or "{}") result_id = params_data.get("slate_id") except (ValueError, TypeError): pass db.update_task( payload.task_id, payload.status, payload.progress, message=payload.result_path or "", result_id=result_id, error=payload.error, ) # succeeded 시 slate_status도 'rendered'로 갱신 + card_assets 등록 (cutover 후 NAS가 처리) if payload.status == "succeeded" and result_id is not None: try: db.update_slate_status(result_id, "rendered") except Exception: logger.exception("update_slate_status %s 실패 (무시)", result_id) try: _register_rendered_assets(result_id) except Exception: logger.exception("card_assets 등록 %s 실패 (무시)", result_id) logger.info( "internal/insta/update task=%s status=%s progress=%d", payload.task_id, payload.status, payload.progress, ) return {"ok": True}