docs: 文档/配置/测试同步 NAS :8124 生产现状,清除 Node.js 与甲骨文残留
背景:文档停在 Node.js 时代或甲骨文 8123 部署,与生产(NAS :8124 + Flask + auth-hub + ai-gateway)严重脱节,曾导致凭旧记忆误判'无线上环境'。 - CLAUDE.md 重写:技术栈/结构/命令/部署事实/关键坑(F7 button、UTC 日期、 429 退避以 DB 为准、迁移幂等、AI 生成耗时) - docs/ARCHITECTURE.md 重写为 Flask 蓝图+services+可插拔数据层 + NAS 部署 - docs/DEVELOPMENT.md 重写为 Flask/CRA 开发指南 + push.sh 部署流程 - docs/REQUIREMENTS.md:部署条目改 NAS 8124;补 auth-hub/AI 教练/新修复 - docs/AUTH_HUB_INTEGRATION.md 新增(补 .env.example 悬空引用) - README.md:技术栈/DB/auth-hub/API 清单/部署节修正 - backend/config.py 与 .env.example:AUTH_HUB_REDIRECT_URI 默认 8123→8124, MariaDB 注释 Oracle→NAS - tests:GatewayCourtesy 并发测试对齐 MAX_CONCURRENT(AI_JOB_CONCURRENCY=2); conftest 禁用 create_app 后台队列线程,修整库测试 flaky(585 passed)
This commit is contained in:
@@ -8,12 +8,14 @@ DB_TYPE=sqlite
|
||||
# SQLite file (used when DB_TYPE=sqlite)
|
||||
DATABASE_PATH=./data/health.db
|
||||
|
||||
# MariaDB (used when DB_TYPE=mariadb) — production DB on the Oracle server
|
||||
# (129.146.26.249, local MariaDB 10.3). Dedicated account over TCP 127.0.0.1;
|
||||
# MARIADB_SOCKET is only needed if TCP auth is disabled for the app user.
|
||||
# MariaDB (used when DB_TYPE=mariadb) — production DB on the NAS
|
||||
# (192.168.50.64, MariaDB 10.11). Connection is over the socket
|
||||
# /run/mysqld/mysqld10.sock (or TCP 127.0.0.1:3306) as root; the socket path
|
||||
# only matters when TCP auth is disabled for the app user.
|
||||
# MARIADB_SOCKET=/run/mysqld/mysqld10.sock
|
||||
# MARIADB_HOST=127.0.0.1
|
||||
# MARIADB_PORT=3306
|
||||
# MARIADB_USER=garmin
|
||||
# MARIADB_USER=root
|
||||
# MARIADB_PASSWORD=your_production_mariadb_password
|
||||
# MARIADB_DATABASE=garmin_health_lab
|
||||
|
||||
@@ -35,8 +37,10 @@ AUTH_HUB_BASE_URL=http://129.146.26.249:5300
|
||||
AUTH_HUB_CLIENT_ID=your_client_id
|
||||
AUTH_HUB_CLIENT_SECRET=your_client_secret
|
||||
#
|
||||
# Callback URL (must exactly match what's registered in auth-hub)
|
||||
AUTH_HUB_REDIRECT_URI=http://129.146.26.249:8123/auth/callback
|
||||
# Callback URL (must exactly match what's registered in auth-hub). Production
|
||||
# registers both the public frp address (129.146.26.249:8124) and the LAN
|
||||
# address (192.168.50.64:8124).
|
||||
AUTH_HUB_REDIRECT_URI=http://129.146.26.249:8124/auth/callback
|
||||
|
||||
# --- CORS (comma-separated allowed front-end origins) ---
|
||||
# localhost stays in the production list on purpose: CORS is not an auth
|
||||
@@ -55,7 +59,7 @@ CORS_ORIGIN=http://localhost:3000,http://localhost:5173
|
||||
# can take 2-3 minutes, so set AI_TIMEOUT_SECONDS accordingly.
|
||||
# HTTPS (Caddy, strips the /ai prefix) rather than http://…:5100 — the token
|
||||
# rides in an Authorization header and should not cross the internet in clear.
|
||||
AI_GATEWAY_BASE_URL=https://oracle.zichuan.xyz/ai/v1
|
||||
AI_GATEWAY_BASE_URL=https://ai.zichuan.xyz/v1
|
||||
AI_GATEWAY_TOKEN=
|
||||
AI_GATEWAY_MODEL=ai-gateway-auto
|
||||
|
||||
@@ -91,14 +95,9 @@ AI_MAX_TOKENS=1024
|
||||
AI_COACH_MAX_TOKENS=4000
|
||||
|
||||
# --- AI coach job queue ---
|
||||
# The gateway runs `gunicorn -w 1 --threads 4` and is shared with fam-edge and
|
||||
# the camera project: four concurrent requests for everyone, while one of ours
|
||||
# holds a thread for 2-5 minutes. So this consumer runs one job at a time
|
||||
# across the whole deployment (not one per Gunicorn worker) and waits between
|
||||
# jobs. Raising either of these makes the backfill finish sooner at the cost of
|
||||
# the shared box — a 502 there is a 502 for the other two projects as well.
|
||||
AI_JOB_CONCURRENCY=1
|
||||
AI_JOB_GAP_SECONDS=20
|
||||
# Three projects share the gateway, so going too high causes 502s.
|
||||
AI_JOB_CONCURRENCY=2
|
||||
AI_JOB_GAP_SECONDS=5
|
||||
# Set AI_JOBS=false to stop consuming entirely (screens then show the computed
|
||||
# figures with no model reading).
|
||||
AI_JOBS=true
|
||||
|
||||
@@ -41,9 +41,14 @@ JWT_EXPIRY_DAYS = int(os.environ.get("JWT_EXPIRY_DAYS") or 7)
|
||||
# fallback on purpose — unlike the issuer URL and client id, it must never be
|
||||
# hardcoded in source; put it in backend/.env (gitignored) instead.
|
||||
AUTH_HUB_BASE_URL = os.environ.get("AUTH_HUB_BASE_URL") or "http://129.146.26.249:5300"
|
||||
# Dev fallback client id (auth-hub keeps dev and prod clients in separate
|
||||
# databases). Production always overrides this via backend/.env — the value in
|
||||
# use on the NAS is the registered client for the :8124 callbacks.
|
||||
AUTH_HUB_CLIENT_ID = os.environ.get("AUTH_HUB_CLIENT_ID") or "0asGO0FdX_XYOk6O"
|
||||
AUTH_HUB_CLIENT_SECRET = os.environ.get("AUTH_HUB_CLIENT_SECRET") or ""
|
||||
AUTH_HUB_REDIRECT_URI = os.environ.get("AUTH_HUB_REDIRECT_URI") or "http://129.146.26.249:8123/auth/callback"
|
||||
# Production callback goes through the NAS frp tunnel to the public address
|
||||
# (129.146.26.249:8124); the LAN callback 192.168.50.64:8124 is registered too.
|
||||
AUTH_HUB_REDIRECT_URI = os.environ.get("AUTH_HUB_REDIRECT_URI") or "http://129.146.26.249:8124/auth/callback"
|
||||
|
||||
# --- Static UI --------------------------------------------------------------
|
||||
# Directory holding the built React app. When set and populated, the Flask
|
||||
|
||||
@@ -52,6 +52,19 @@ def _isolate_ai_env(monkeypatch):
|
||||
monkeypatch.delenv(var, raising=False)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _no_ai_jobs_background_thread(monkeypatch):
|
||||
"""create_app() starts a daemon queue-consumer thread that outlives the
|
||||
test that launched it and races the *next* test for jobs on that test's
|
||||
fresh database (with whatever _runner the previous stub left behind) —
|
||||
which made queue tests flaky. Tests drive the queue themselves through
|
||||
jobs.run_once(), so the thread is disabled here.
|
||||
"""
|
||||
from services import jobs
|
||||
|
||||
monkeypatch.setattr(jobs, "ENABLED", False)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_garmin_client_cache():
|
||||
"""Drop cached Garmin sessions between tests.
|
||||
|
||||
@@ -1076,15 +1076,20 @@ class TestHighlightsReadAlone:
|
||||
|
||||
|
||||
class TestGatewayCourtesy:
|
||||
"""The gateway runs one worker with four threads and is shared with two
|
||||
other projects. This consumer must not be able to saturate it."""
|
||||
"""The gateway is shared with two other projects, so this consumer must
|
||||
not saturate it. The cap is MAX_CONCURRENT (AI_JOB_CONCURRENCY; default 2
|
||||
since 2026-09-01) and is counted across the whole deployment, not per
|
||||
Gunicorn worker."""
|
||||
|
||||
def test_only_one_job_runs_at_a_time_across_the_deployment(self, db, user):
|
||||
jobs.enqueue(user["id"], "health", "a")
|
||||
jobs.enqueue(user["id"], "sleep", "b")
|
||||
assert jobs._claim_next() is not None
|
||||
assert jobs._claim_next() is None, \
|
||||
"a second Gunicorn worker must not start a second gateway call"
|
||||
def test_concurrency_is_capped_at_MAX_CONCURRENT(self, db, user):
|
||||
"""More than the cap may queue, but only MAX_CONCURRENT run at once."""
|
||||
limit = jobs.MAX_CONCURRENT
|
||||
for i in range(limit + 2):
|
||||
jobs.enqueue(user["id"], "health", f"job-{i}")
|
||||
claimed = [jobs._claim_next() for _ in range(limit + 1)]
|
||||
assert sum(1 for c in claimed if c is not None) == limit
|
||||
assert claimed[limit] is None, \
|
||||
"a second claim past MAX_CONCURRENT must not start another gateway call"
|
||||
|
||||
def test_a_finished_job_frees_the_slot(self, db, user):
|
||||
jobs.enqueue(user["id"], "health", "a")
|
||||
|
||||
Reference in New Issue
Block a user