diff --git a/.dockerignore b/.dockerignore index 1956ada..2b065b1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -24,3 +24,5 @@ data .env.local .env.production deploy/.env.production +deploy/.env.local +data-local diff --git a/.gitignore b/.gitignore index c4d9bc1..ee7adc7 100644 --- a/.gitignore +++ b/.gitignore @@ -15,9 +15,11 @@ __pycache__/ .logs/ .pids/ deploy/.env.production +deploy/.env.local deploy/.htpasswd secrets/ .backups/ +data-local/ # api api/.venv/ diff --git a/AGENTS.md b/AGENTS.md index 4982011..c90e71d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,8 @@ - 开发任务结束前必须执行并汇报 `git status -sb` - 功能、修复、规则或部署元数据变更完成后,必须创建人工语义 commit;`auto-save` 只算安全快照 -- Gitea 是主远端,`origin` 必须指向 Gitea;能联网和鉴权时必须推送完成提交 +- 默认先在本地 Docker 完整验证:`./scripts/start-local-docker.sh` 后运行 `./scripts/verify-local-docker.sh`;用户明确确认“可以推送 / 上推 / 部署”前,不要 `git push`,也不要运行生产部署脚本。 +- Gitea 是主远端,`origin` 必须指向 Gitea;只有在用户明确确认推送后,才把已验证的人工语义 commit 推送到 Gitea。 - 当前主分支为 `main`,Gitea 仓库为 `https://git.kang-kang.com/kangwan/20260512-skg-tk` - `.memory/worklog.json` 是辅助日志,不代替人工语义 commit 和 Gitea 远端记录 - 不能推送时,必须说明当前分支、本地领先/落后数量、最新未推送 commit 和失败原因 @@ -26,6 +27,7 @@ ## Completion Gate +- 普通代码修改完成后,默认收口在本地 Docker 验证和本地 commit;生产推送 / 部署必须等用户明确确认。 - 部署完成后,不允许在 `.project.json` 缺少最新公网链接的状态下结束任务 - 部署完成后,必须同步更新 `RULES.md` 的部署事实 - 如果只更新了代码但没回写部署元数据,这个任务不算完成 diff --git a/RULES.md b/RULES.md index 1b88f6b..c7c2f17 100644 --- a/RULES.md +++ b/RULES.md @@ -1,12 +1,16 @@ # SKG 营销内容生产平台 ## 启动 +- 本地 Docker 启动:`./scripts/start-local-docker.sh`(默认 Web `http://localhost:4390`、API `http://localhost:4391`、Postgres 数据在 `data-local/postgres`;首次会从 `deploy/.env.local.example` 生成 gitignored 的 `deploy/.env.local`) +- 本地 Docker 验证:`./scripts/verify-local-docker.sh`(检查容器、`/login/`、未登录 `/api/health`、容器内 `/health` + Postgres) +- 本地 Docker 停止:`./scripts/stop-local-docker.sh` - 后台启动(不弹 Terminal):`./scripts/start-dev-background.sh`(通过 macOS launchd 后台托管;前端 4290 + 后端 4291,日志写入 `.logs/`) - 后台停止:`./scripts/stop-dev-background.sh` - 前端 dev:`cd web && npm run dev`(Next.js 16,端口 4290) - 画布 dev:`cd web && npm run dev:canvas`(Vue / Vite,端口 4292;生产构建会作为根域名工作台输出) - 后端 dev:`cd api && uvicorn main:app --host 127.0.0.1 --port 4291`(FastAPI,端口 4291,重任务用) - 注意:后端不要带 `--reload` 跑长下载 / 抽帧 / 音频任务;reload 会等待后台任务结束,导致 4291 端口占用但新请求卡住。 +- 发布流程新规则(2026-05-26):所有修改默认先在本地 Docker 跑通并验证,确认可用后只保留本地 commit;只有用户明确说“可以推送 / 上推 / 部署”时,才允许 `git push` 或执行生产部署。 ## 立项决策快索引 - 详见 `CLAUDE.md` 立项决策段 + `.memory/plan.md` 七步管线拆解 @@ -85,11 +89,13 @@ - 文档 / 解析:`docs/source-analysis.html`(项目内独立文档,不公开挂主应用路由) - 管理后台:待定 - 服务器目录:`/opt/skg-marketing-studio` +- 本地 Docker 是生产前默认验收口径:`./scripts/start-local-docker.sh` 构建本地 Web/API/Postgres,`./scripts/verify-local-docker.sh` 通过后才允许进入推送/部署讨论。本地 Docker 使用 `docker-compose.local.yml`、`deploy/.env.local` 和 `data-local/`,不能读取或覆盖生产 `deploy/.env.production`、服务器 `data/` 或 `secrets/`。 - 生产部署唯一入口:`./scripts/deploy-prod-safe.sh`(先在服务器备份 `deploy/.env.production`、`data/jobs`、资源库和 `secrets`,如 Postgres 容器存在则额外导出 `pg_dump`,再用受保护 rsync 同步代码,最后 Docker 重建并运行 `verify-prod-docker.sh`) - 生产容器重建命令:`docker compose -f docker-compose.prod.yml --env-file deploy/.env.production up -d --build`;只允许脚本内部或明确只重启容器时使用,不允许再用裸 `rsync --delete` 手动同步。 - 独立预览容器重建命令:服务器 `/opt/skg-marketing-studio` 下执行 `docker compose -f docker-compose.standalone.yml --env-file deploy/.env.production up -d --build`;Web 暴露 `0.0.0.0:4290->80`,后端仅在 compose 内部网络暴露,`/api/` 由 Web 容器 Nginx 反代并复用应用内登录校验。 - 生产架构:`web` 容器用 Nginx 承载 Next 静态导出与根域名 Vue / Vite 画布静态应用;构建时先生成画布,再 Next 静态导出,最后用画布产物覆盖 `web/out/index.html` 和 `/assets/`,使登录后的 `/` 直接进入画布;`/canvas/` 只做 308 兼容跳转到 `/`。`/login/`、`/_next/`、`/assets/`、`/skg-logo-black.svg`、`/oasis-source/` 等登录页必需静态资源公开访问;未登录访问工作台跳转 `/login/`;`/api/` 通过 Nginx `auth_request` 校验 FastAPI 会话 Cookie 后反代到 `skg-marketing-api:4291`;FastAPI 通过内网 `DATABASE_URL` 连接 `skg-marketing-postgres:5432`,Postgres 不对公网暴露;Traefik 通过 `coolify` 外部网络接入 80/443 - Web 验收必须以生产 Docker 形态为准:前端是 `next export` 静态产物 + Nginx,不是 `next dev` / `next start`。任何 Web 改动部署后必须运行 `./scripts/verify-prod-docker.sh`,确认 `/login/`、`/_next/`、`/api/health`、本地 API 地址泄漏和 API 镜像 `.env` 污染检查通过;不能只用本地 `npm run build` 作为上线依据。 +- 未经用户明确确认,不允许推送 Gitea 或部署生产;完成开发任务时报告本地 Docker 验证结果、当前分支、本地领先数量和待推送 commit。 - 当前音频解析:`https://ai.skg.com/azure/v1` 的 `gpt-4o-transcribe` 当前返回 `DeploymentNotFound`,且官方 Azure OpenAI transcription 路径探测也未返回可用部署;生产临时复制本地成功策略,直接使用容器内多语言 `faster-whisper` 真实转写,默认语种为 `auto`,支持中文、英文和其他多语言原文识别,关闭 Gemini 多模态音频兜底。拿到真实 Azure ASR deployment 名后再恢复 `ASR_REMOTE_ENABLED=true`,并保持 `ASR_LANGUAGE` 为空或 `auto`,除非明确只想强制单一语种。 - 持久化目录:服务器 `./data/jobs` 挂载到后端 `/data/jobs`;全局资源中心持久化在 `./data/asset_library`、`./data/prompt_library` 和 `./data/_trash`;Postgres 数据目录为服务器 `./data/postgres`,部署脚本通过 `pg_dump` 产出 `/opt/skg-marketing-studio-backups/skg-marketing-postgres-*.sql.gz` - TikTok 下载登录态:公开视频默认不带 cookies 直接下载,生产环境变量必须显式保持 `YTDLP_COOKIES_FILE=`、`YTDLP_COOKIES_FROM_BROWSER=` 为空,防止容器读取不存在的浏览器 cookies。只有 TikTok 明确要求登录态时,才使用服务器私有 cookies 文件 `./secrets/tiktok_cookies.txt` 挂载到 API 容器 `/run/secrets/tiktok_cookies.txt` 并配置 `YTDLP_COOKIES_FILE=/run/secrets/tiktok_cookies.txt`;`yt-dlp` 会在任务结束时回写 cookies,因此不要把该挂载设为只读;不要使用云端浏览器读取方案,也不要把 cookies 入库。生产容器严禁使用 `YTDLP_COOKIES_FROM_BROWSER=chrome`。 @@ -118,7 +124,7 @@ - Gitea 网页仓库:`https://git.kang-kang.com/kangwan/20260512-skg-tk` - 每次开发结束前必须执行并汇报 `git status -sb` 和变更范围 - 代码、规则、部署或元数据变更必须形成 `feat:`、`fix:`、`docs:`、`chore:`、`release:` 等人工语义 commit;`auto-save` 只算安全快照 -- 能联网和鉴权时必须 `git push origin main`;如果不能推送,最终回复必须写清楚当前分支、领先/落后数量、最新未推送 commit 和失败原因 +- 用户明确确认“可以推送 / 上推 / 部署”前,不允许 `git push` 或生产部署;用户确认后,能联网和鉴权时再 `git push origin main`,如果届时不能推送,最终回复必须写清楚当前分支、领先/落后数量、最新未推送 commit 和失败原因 ## 环境变量 - `LLM_BASE_URL` / `LLM_API_KEY`:OpenAI 兼容网关,用于翻译、文案改写、音频分析等文本/多模态理解模型调用 diff --git a/deploy/.env.local.example b/deploy/.env.local.example new file mode 100644 index 0000000..01579d3 --- /dev/null +++ b/deploy/.env.local.example @@ -0,0 +1,91 @@ +# Local Docker only. Copy to deploy/.env.local or run scripts/start-local-docker.sh. +# Real production secrets stay in deploy/.env.production on the VPS. + +# Local ports +LOCAL_WEB_PORT=4390 +LOCAL_API_PORT=4391 + +# Local Postgres +POSTGRES_DB=skg_marketing_local +POSTGRES_USER=skg_marketing +POSTGRES_PASSWORD=skg_marketing_local_password + +# Local password login for Docker smoke tests +PASSWORD_AUTH_ENABLED=true +WEB_AUTH_USERNAME=skg +WEB_AUTH_PASSWORD=local-skg +WEB_AUTH_SESSION_SECRET=local-docker-session-secret-change-me +WEB_AUTH_COOKIE_NAME=skg_marketing_local_session +WEB_AUTH_COOKIE_SECURE=false +AUTH_DATA_ISOLATION_ENABLED=true + +# Feishu can be filled locally if OAuth needs to be tested from localhost. +FEISHU_APP_ID= +FEISHU_APP_SECRET= +FEISHU_REDIRECT_URI=http://localhost:4390/api/auth/feishu/callback +FEISHU_OAUTH_SCOPE= +FEISHU_ALLOWED_EMAIL_DOMAINS= +FEISHU_ALLOWED_EMAILS= +FEISHU_ALLOWED_TENANT_KEYS= + +# SKG AI gateway. Leave blank when only testing UI/login/database locally. +LLM_BASE_URL=https://ai.skg.com/ezlink/v1 +LLM_API_KEY= +IMAGE_BASE_URL=https://ai.skg.com/ezlink/v1 +IMAGE_API_KEY= +IMAGE_MODEL=gpt-image-2 +IMAGE_REQUEST_TIMEOUT_SECONDS=60 +IMAGE_FALLBACK_ENABLED=true +IMAGE_FALLBACK_MODEL=gemini-3-pro-image-preview +IMAGE_CIRCUIT_FAILURE_THRESHOLD=2 +IMAGE_CIRCUIT_COOLDOWN_SECONDS=600 +GPT_IMAGE_MODEL=gpt-image-2 +SUBJECT_ASSET_IMAGE_MODEL=gpt-image-2 +SUBJECT_ASSET_IMAGE_MODELS=gpt-image-2,gemini-3-pro-image-preview +AI_HTTP_PROXY= + +# Text/vision/audio model names +GPT_TEXT_MODEL=gpt-4o +REWRITE_MODEL=gpt-4o +VISION_MODEL=gpt-4o +TRANSLATE_MODEL=gemini-2.5-flash +ASR_BASE_URL=https://ai.skg.com/azure/v1 +ASR_API_KEY= +ASR_MODEL=gpt-4o-transcribe +ASR_LANGUAGE=auto +ASR_REMOTE_ENABLED=false +ASR_LOCAL_FALLBACK_ENABLED=true +ASR_AUDIO_FALLBACK_ENABLED=false +ASR_FALLBACK_MODEL=gemini-2.5-flash +ASR_TIMEOUT_SECONDS=45 +FASTER_WHISPER_MODEL=tiny +FASTER_WHISPER_DEVICE=cpu +FASTER_WHISPER_COMPUTE_TYPE=int8 + +# Video generation. Fill VIDEO_API_KEY only when testing real video generation locally. +VIDEO_API_BASE_URL=https://ai.skg.com/doubao +VIDEO_API_KEY= +VIDEO_MODEL=seedance +VIDEO_MODEL_SEEDANCE=doubao-seedance-2-0-fast-260128 +VIDEO_MODEL_KLING=kling-omni +VIDEO_MODEL_VEO3=veo-3.1-fast +VIDEO_CREATE_PATHS=/api/v3/contents/generations/tasks +VIDEO_STATUS_PATH=/api/v3/contents/generations/tasks/{id} +VIDEO_CONTENT_PATH=/api/v3/contents/generations/tasks/{id}/content +VIDEO_DURATION_FIELD=seconds +VIDEO_POLL_TIMEOUT_SECONDS=900 + +# Azure OpenAI TTS. Leave blank unless testing voice locally. +AUDIO_REWRITE_MODEL=gemini-2.5-pro +VOICE_PROVIDER=azure_openai +AZURE_OPENAI_BASE_URL=https://ai.skg.com/azure +AZURE_OPENAI_API_KEY= +AZURE_TTS_MODEL=gpt-4o-mini-tts +AZURE_TTS_VOICE_ID=alloy +AZURE_TTS_VOICE_POOL=alloy,verse,shimmer +AZURE_TTS_PATH=/audio/speech +AZURE_TTS_PATHS=/audio/speech,/v1/audio/speech + +# Optional TikTok cookies. Keep files local and out of git. +YTDLP_COOKIES_FILE= +YTDLP_COOKIES_FROM_BROWSER= diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 0000000..f757ffc --- /dev/null +++ b/docker-compose.local.yml @@ -0,0 +1,85 @@ +name: skg-marketing-local + +services: + postgres: + image: postgres:16-alpine + container_name: skg-marketing-local-postgres + environment: + POSTGRES_DB: ${POSTGRES_DB:-skg_marketing_local} + POSTGRES_USER: ${POSTGRES_USER:-skg_marketing} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-skg_marketing_local_password} + volumes: + - ./data-local/postgres:/var/lib/postgresql/data + restart: unless-stopped + networks: + - skg-marketing-local + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-skg_marketing} -d ${POSTGRES_DB:-skg_marketing_local}"] + interval: 5s + timeout: 5s + retries: 20 + + api: + image: skg-marketing-local-api:latest + build: + context: . + dockerfile: Dockerfile.api + container_name: skg-marketing-local-api + env_file: + - ./deploy/.env.local + environment: + JOBS_DIR: /data/jobs + AGENT_RUNS_DIR: /data/agent_runs + ASSET_LIBRARY_DIR: /data/asset_library + PROMPT_LIBRARY_DIR: /data/prompt_library + DATABASE_URL: postgresql://${POSTGRES_USER:-skg_marketing}:${POSTGRES_PASSWORD:-skg_marketing_local_password}@postgres:5432/${POSTGRES_DB:-skg_marketing_local} + CORS_ORIGINS: http://localhost:${LOCAL_WEB_PORT:-4390},http://127.0.0.1:${LOCAL_WEB_PORT:-4390} + PASSWORD_AUTH_ENABLED: ${PASSWORD_AUTH_ENABLED:-true} + WEB_AUTH_USERNAME: ${WEB_AUTH_USERNAME:-skg} + WEB_AUTH_PASSWORD: ${WEB_AUTH_PASSWORD:-local-skg} + WEB_AUTH_SESSION_SECRET: ${WEB_AUTH_SESSION_SECRET:-local-docker-session-secret-change-me} + WEB_AUTH_COOKIE_SECURE: "false" + WEB_AUTH_COOKIE_NAME: ${WEB_AUTH_COOKIE_NAME:-skg_marketing_local_session} + AUTH_DATA_ISOLATION_ENABLED: "true" + FEISHU_REDIRECT_URI: http://localhost:${LOCAL_WEB_PORT:-4390}/api/auth/feishu/callback + KEYFRAME_COUNT: ${KEYFRAME_COUNT:-12} + VIDEO_QUEUE_MAX_CONCURRENT: ${VIDEO_QUEUE_MAX_CONCURRENT:-2} + VIDEO_QUEUE_MAX_CONCURRENT_PER_USER: ${VIDEO_QUEUE_MAX_CONCURRENT_PER_USER:-1} + YTDLP_COOKIES_FILE: ${YTDLP_COOKIES_FILE:-} + YTDLP_COOKIES_FROM_BROWSER: ${YTDLP_COOKIES_FROM_BROWSER:-} + volumes: + - ./data-local/jobs:/data/jobs + - ./data-local/agent_runs:/data/agent_runs + - ./data-local/asset_library:/data/asset_library + - ./data-local/prompt_library:/data/prompt_library + - ./data-local/_trash:/data/_trash + ports: + - "127.0.0.1:${LOCAL_API_PORT:-4391}:4291" + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + networks: + skg-marketing-local: + aliases: + - skg-marketing-api + + web: + image: skg-marketing-local-web:latest + build: + context: . + dockerfile: Dockerfile.web + args: + NEXT_PUBLIC_API_BASE: /api + container_name: skg-marketing-local-web + depends_on: + - api + ports: + - "127.0.0.1:${LOCAL_WEB_PORT:-4390}:80" + restart: unless-stopped + networks: + - skg-marketing-local + +networks: + skg-marketing-local: + name: skg-marketing-local diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 7808c9b..975dcae 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -523,6 +523,16 @@ 项目命令 / 入口说明 + + 本地 Docker 启动 + ./scripts/start-local-docker.sh + 生产前默认本地验收入口;使用 docker-compose.local.yml 构建 Web / API / Postgres,默认 Web 为 http://localhost:4390,API 为 http://localhost:4391,本地数据写入 data-local/。首次启动会从 deploy/.env.local.example 生成不入库的 deploy/.env.local,本地登录默认 skg / local-skg。 + + + 本地 Docker 验证 + ./scripts/verify-local-docker.sh + 检查本地 Docker 容器、登录页、未登录 API 保护和容器内 /health + Postgres 连接。以后代码改动先通过本地 Docker 验证并形成本地 commit,只有用户明确确认后才推送 Gitea 或执行生产部署。 + 本地后台启动 ./scripts/start-dev-background.sh @@ -546,7 +556,7 @@ 生产部署 ./scripts/deploy-prod-safe.sh - 服务器目录为 /opt/skg-marketing-studio;后端任务文件挂载到 ./data/jobs,全局资源中心挂载到 ./data/asset_library./data/prompt_library./data/_trash,Postgres 数据目录为 ./data/postgres,真实 Key 和数据库密码只放服务器 deploy/.env.production。生产部署唯一入口是 deploy-prod-safe.sh:先备份服务器 env、案例、资源库和 secrets,如 Postgres 容器存在则额外导出 pg_dump,再用 protect/exclude 保护 data/jobs/secrets/deploy/.env.production 后同步代码,最后 Docker 重建并跑 verify-prod-docker.sh。禁止再用裸 rsync --delete 手动同步。 + 服务器目录为 /opt/skg-marketing-studio;后端任务文件挂载到 ./data/jobs,全局资源中心挂载到 ./data/asset_library./data/prompt_library./data/_trash,Postgres 数据目录为 ./data/postgres,真实 Key 和数据库密码只放服务器 deploy/.env.production。生产部署唯一入口是 deploy-prod-safe.sh:先备份服务器 env、案例、资源库和 secrets,如 Postgres 容器存在则额外导出 pg_dump,再用 protect/exclude 保护 data/jobs/secrets/deploy/.env.production 后同步代码,最后 Docker 重建并跑 verify-prod-docker.sh。禁止再用裸 rsync --delete 手动同步;未得到用户明确确认前,不推送 Gitea、不部署生产。 前端开发服务 @@ -1391,6 +1401,18 @@ ProductRefStateItem {

影响:画布项目开始具备跨浏览器、跨设备恢复的服务端主存储;默认仍按 owner 私有隔离,后续可在同一表上扩展 team/company 可见性。完整 job state 和媒体文件仍保留在原有文件目录,避免把大文件一次性搬进数据库。

+
+
+

2026-05-26 · 本地 Docker 先验收再上推

+ Ops + Docs +
+
+

问题:此前代码修改常直接走生产部署验证,容易把还在反复调整的模型、尺寸或界面配置带到线上,影响正在使用的团队成员。

+

改动:新增 docker-compose.local.ymldeploy/.env.local.examplescripts/start-local-docker.shscripts/verify-local-docker.shscripts/stop-local-docker.sh。本地 Docker 默认暴露 localhost:4390,使用独立 data-local/ 和本地 Postgres,不读取生产 deploy/.env.production

+

影响:后续开发流程改为“本地 Docker 启动 → 本地验证 → 本地 commit → 用户确认后才推送/部署”。AGENTS.mdRULES.md 已同步该约束,避免后续接手会话自动推送或直接上生产。

+
+

2026-05-26 · 恢复最初生图配置

diff --git a/scripts/start-local-docker.sh b/scripts/start-local-docker.sh new file mode 100755 index 0000000..62f1224 --- /dev/null +++ b/scripts/start-local-docker.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +if [ ! -f deploy/.env.local ]; then + cp deploy/.env.local.example deploy/.env.local + echo "created deploy/.env.local from deploy/.env.local.example" +fi + +docker build -f Dockerfile.api -t skg-marketing-local-api:latest . +docker build -f Dockerfile.web -t skg-marketing-local-web:latest --build-arg NEXT_PUBLIC_API_BASE=/api . +docker compose -f docker-compose.local.yml --env-file deploy/.env.local up -d --no-build "$@" + +WEB_PORT="$(grep -E '^LOCAL_WEB_PORT=' deploy/.env.local | tail -1 | cut -d= -f2-)" +WEB_PORT="${WEB_PORT:-4390}" + +echo "local Docker is starting: http://localhost:${WEB_PORT}" +echo "login: skg / local-skg unless deploy/.env.local overrides it" diff --git a/scripts/stop-local-docker.sh b/scripts/stop-local-docker.sh new file mode 100755 index 0000000..b68c9a9 --- /dev/null +++ b/scripts/stop-local-docker.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +docker compose -f docker-compose.local.yml --env-file deploy/.env.local down "$@" diff --git a/scripts/verify-local-docker.sh b/scripts/verify-local-docker.sh new file mode 100755 index 0000000..f2d3b4c --- /dev/null +++ b/scripts/verify-local-docker.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +if [ ! -f deploy/.env.local ]; then + echo "deploy/.env.local is missing. Run ./scripts/start-local-docker.sh first." >&2 + exit 1 +fi + +WEB_PORT="$(grep -E '^LOCAL_WEB_PORT=' deploy/.env.local | tail -1 | cut -d= -f2-)" +WEB_PORT="${WEB_PORT:-4390}" +WEB_URL="http://127.0.0.1:${WEB_PORT}" +AUTH_USERNAME="$(grep -E '^WEB_AUTH_USERNAME=' deploy/.env.local | tail -1 | cut -d= -f2-)" +AUTH_USERNAME="${AUTH_USERNAME:-skg}" +AUTH_PASSWORD="$(grep -E '^WEB_AUTH_PASSWORD=' deploy/.env.local | tail -1 | cut -d= -f2-)" +AUTH_PASSWORD="${AUTH_PASSWORD:-local-skg}" +COMPOSE=(docker compose -f docker-compose.local.yml --env-file deploy/.env.local) + +"${COMPOSE[@]}" ps + +login_status="$(curl --noproxy '*' -sS -o /tmp/skg-local-login.html -w '%{http_code}' "${WEB_URL}/login/")" +if [ "$login_status" != "200" ]; then + echo "ERROR: unexpected /login/ status ${login_status}" >&2 + head -40 /tmp/skg-local-login.html >&2 || true + exit 1 +fi +echo "web:/login/ 200" + +root_status="$(curl --noproxy '*' -sS -o /tmp/skg-local-root.html -w '%{http_code}' "${WEB_URL}/")" +if [ "$root_status" != "302" ] && [ "$root_status" != "200" ]; then + echo "ERROR: unexpected / status ${root_status}" >&2 + head -40 /tmp/skg-local-root.html >&2 || true + exit 1 +fi +echo "web:/ ${root_status}" + +api_status="$(curl --noproxy '*' -sS -o /tmp/skg-local-api-health.json -w '%{http_code}' "${WEB_URL}/api/health")" +if [ "$api_status" != "401" ]; then + echo "ERROR: unexpected unauthenticated /api/health status ${api_status}" >&2 + cat /tmp/skg-local-api-health.json >&2 || true + exit 1 +fi +echo "web:/api/health 401" + +login_api_status="$(curl --noproxy '*' -sS -o /tmp/skg-local-login-api.json -w '%{http_code}' -c /tmp/skg-local-cookie.jar -X POST "${WEB_URL}/api/auth/login" -H 'content-type: application/json' --data "{\"username\":\"${AUTH_USERNAME}\",\"password\":\"${AUTH_PASSWORD}\"}")" +if [ "$login_api_status" != "200" ]; then + echo "ERROR: unexpected /api/auth/login status ${login_api_status}" >&2 + cat /tmp/skg-local-login-api.json >&2 || true + exit 1 +fi +echo "web:/api/auth/login 200" + +"${COMPOSE[@]}" exec -T api python - <<'PY' +import json +import main + +data = main.health() +database = data.get("database") or {} +if not data.get("ok") or not database.get("connected"): + raise SystemExit(json.dumps(data, ensure_ascii=False)[:1000]) +print("api:health ok db connected") +PY