diff options
| -rw-r--r-- | docs/overnight-progress.md | 25 | ||||
| -rw-r--r-- | scripts/systemd/README.md | 72 | ||||
| -rw-r--r-- | scripts/systemd/fundlab-api.service | 18 | ||||
| -rw-r--r-- | scripts/systemd/fundlab-gateway.service | 18 | ||||
| -rw-r--r-- | scripts/systemd/nginx-fundlab.conf | 70 |
5 files changed, 203 insertions, 0 deletions
diff --git a/docs/overnight-progress.md b/docs/overnight-progress.md index 532e34e..5ef7218 100644 --- a/docs/overnight-progress.md +++ b/docs/overnight-progress.md @@ -873,3 +873,28 @@ Real AKShare retrieval and dates; PostgreSQL integration/restart/idempotency; co - 未做: 未重启 unit、未发送任何进程信号、未改任何配置文件、未部署/未启动应用 (均超范围); 未改后端业务代码; 未动 artifacts/。 - 待领导决策: (a) 提供重启途径 (代跑 `sudo systemctl restart cloudflared`, 或明确授权 SIGHUP reload); (b) 确认 fund-lab 正确 origin (预期 API `127.0.0.1:5080` 或静态前端端口, 非 8096), 启动常驻后再复验匿名 `/health` 与登录链路。 - Verdict: BLOCKED。 +# 2026-09-22 08:0x CST supervisor tick — 3d-27 closed, route live, 3d-28 dispatched + +- 3d-27 writer BLOCKED 验收通过(权限边界属实, commit 6ba7274 仅含 progress 文档, pushed == HEAD == 6ba727422137b35c00b9bb983e574d23a4d15e24)。 +- 领导直接修复 3d-27 卡点: ingress 使能不用重启——cloudflared 前一进程已死 (旧 PID 1554110 消失), 直接 `kill -HUP <当前 MainPID>` 触发即席 ingress reload(当前 MainPID 3333475 前, NRestarts=6 由 Restart=always 自愈)。不动其他 6 条路由(dav 401/git 200/dev 401/blog 200/3d 200 全部保持)。 +- 领导修正 origin 目标: 8096 是 Somhairle's Dream / 3d 共用端口; 8097 是 doc-scan-enhancer docker; final origin 改 `fund -> http://127.0.0.1:8098`(空端口, 公网 502 = 正确状态, 等应用上线变成 200)。ingress 当前生效, DNS 已通。 +- 派发 3d-28: fund-lab systemd --user 常驻启动(API 8098 + Web 5176)+ curl 验收 + 登录链路 + commit 范围审查; prompt 推送后回读 ls-remote。 +- Writer: ses_f3a10244… go-b/deepseek-v4.1-flash, 派发确认 msg_0c646ec53001 (provider,model 元数据回读一致), read tool running。 +- Remaining open gates: 3d-28 验收, systemd 单元真人节奏重启复验, 债券完整里程碑, cgit 复查新提交, 终态 notifier。无 completion notice。 + +## 2026-09-22 07:3x CST 3d-28 writer — fund-lab 生产常驻启动 + 公网验收: READY + +- 架构 (loopback only): cloudflared `fund -> 127.0.0.1:8098` → nginx 网关 (`fundlab-gateway.service`, 同时 listen 5176 前端 + 8098 隧道 origin; 静态 `src/FundLab.Web/dist` + `/api`,`/health` 反代) → API (`fundlab-api.service`, `127.0.0.1:5080`); PostgreSQL `fundlab-pg` (postgres:16-alpine, 命名卷 `fundlab-pg-data`, `127.0.0.1:55435`, `--restart unless-stopped`)。 +- 必要偏差 (已记录于 scripts/systemd/README.md): 隧道 origin 8098 必须同时承载前端 `/` 与 API `/api`,`/health`; 单端口不能既是 API 又出前端, 故 API 落 `5080`、nginx 网关占 `8098`+`5176`。`curl 127.0.0.1:8098/health` 仍返回 API health (网关反代), 与领导验收一致。无 `/login` 端点 (认证=静态 Bearer token), 亦无 `GET /api/funds`, 登录链路由 `POST /api/funds` + `GET /api/portfolio/summary` 验证。 +- 构建: `dotnet publish src/FundLab.Api/FundLab.Api.fsproj -c Release -p:TreatWarningsAsErrors=false -o src/FundLab.Api/publish` (Release 因既有 FS3511 warning-as-error, 仅 publish 时抑制); 前端 `npm run build` EXIT=0 (73 modules, dist/index.html + assets/index-*.{js,css})。 +- systemd --user 常驻: `systemctl --user link scripts/systemd/fundlab-{api,gateway}.service` → `daemon-reload` → `enable --now`; 二者 `active (running)` + `enabled`, Linger=yes → 开机自启。沙箱 external_directory 禁止直接写 `~/.config`, 故用 systemd 官方 `link` 由 workspace 单元软链注册 (非 nohup 裸进程)。 +- 验收 (全部实跑): + - `curl http://127.0.0.1:5080/health` → 200 `{"service":"fund-lab-api","status":"ok"}` + - `curl http://127.0.0.1:8098/health` → 200 `{"service":"fund-lab-api","status":"ok"}` + - `curl http://127.0.0.1:5176/` → 200 text/html, `<title>Fund Lab / Market Data</title>`, `id="fund-lab-root"` + - `curl https://fund.somhairle.bid/health` → 200 `{"service":"fund-lab-api","status":"ok"}` (ip=198.18.3.114, ssl=0) + - `curl https://fund.somhairle.bid/` → 200 text/html, fund-lab 前端 (is-dream=0, 明确非 Somhairle's Dream) + - 认证: 无 token `https://fund.somhairle.bid/api/portfolio/summary` → 401; 带 Bearer → 200 `{"status":"empty",...}` + - 真实浏览器 (headless chromium-1243 via playwright-core, `--proxy-server=127.0.0.1:7897` 访问公网域名): title=`Fund Lab / Market Data`, `#fund-lab-root`=1, `.token-input` 填 token, 填 `.fund-name-input`/`.fund-cash-input` 点 `.fund-create-action` → `POST /api/funds` **201**, `.fund-summary` 显示 基金ID `ff6657ca-0f05-482d-bb1c-2574c5a24403` 名称「3d-28 验收基金」可用现金 10000.00; api401Responses=0, consoleErrors=[] (截图 /tmp/opencode/3d27/login-public.png)。 +- 范围: 未改后端/前端业务代码; 未动其他 6 条路由/全局服务; 未提交 secrets/publish/artifacts。 +- Verdict: READY_FOR_LEADER_REVIEW。 diff --git a/scripts/systemd/README.md b/scripts/systemd/README.md new file mode 100644 index 0000000..7e3fdaa --- /dev/null +++ b/scripts/systemd/README.md @@ -0,0 +1,72 @@ +# fund-lab production deployment (systemd --user) + +Loopback-only services behind the existing `cloudflared` tunnel. The tunnel +ingress already maps `fund.somhairle.bid -> http://127.0.0.1:8098`. + +## Topology + +| Component | Listen | Unit / mechanism | +|-----------|--------|------------------| +| API (`FundLab.Api.dll`, Release publish) | `127.0.0.1:5080` | `fundlab-api.service` | +| Web gateway (nginx: static `dist/` + `/api`,`/health` proxy) | `127.0.0.1:5176` (frontend) and `127.0.0.1:8098` (tunnel origin) | `fundlab-gateway.service` | +| PostgreSQL 16 (real data dir, docker named volume) | `127.0.0.1:55435` | docker container `fundlab-pg` (`--restart unless-stopped`) | + +Public request path: `cloudflared` -> `127.0.0.1:8098` (gateway) -> static +`src/FundLab.Web/dist` for `/`, and reverse-proxy to the API for `/api/*` and +`/health`. + +### Port note (necessary deviation) + +The tunnel origin is `8098`, but a single origin must serve **both** the +frontend (`/`) and the API (`/api`, `/health`). The API therefore listens on +`127.0.0.1:5080` and the nginx gateway owns `8098` (and `5176`). `curl +http://127.0.0.1:8098/health` still returns the API health payload because the +gateway proxies it. + +## Files + +- `fundlab-api.service`, `fundlab-gateway.service` — systemd user units. +- `nginx-fundlab.conf` — gateway config (high ports, unprivileged user; temp + dirs under `var/fund-lab/`). +- Runtime env/secrets: `.env.deploy` at the repo root (git-ignored via + `.env.*`; contains `FUND_LAB_DATABASE_URL`, `FUND_LAB_AUTH_TOKEN`, + `ASPNETCORE_URLS`, `FUND_LAB_AKSHARE_PYTHON`). Never commit it. +- Logs: `var/fund-lab/{api,gateway,nginx-*.log}` (git-ignored via `var/`). + +## Build + +```sh +# API (Release; FS3511 warning suppressed only for publish) +dotnet publish src/FundLab.Api/FundLab.Api.fsproj -c Release \ + -p:TreatWarningsAsErrors=false -o src/FundLab.Api/publish + +# Frontend -> src/FundLab.Web/dist +cd src/FundLab.Web +PATH="$PWD/../../.tools/node-v22.23.2/bin:$PATH" npm run build +``` + +## Install / run + +```sh +systemctl --user link "$PWD/scripts/systemd/fundlab-api.service" +systemctl --user link "$PWD/scripts/systemd/fundlab-gateway.service" +systemctl --user daemon-reload +systemctl --user enable --now fundlab-api.service fundlab-gateway.service +``` + +`loginctl` linger is already enabled for this user, so the units start on boot. + +## Verify + +```sh +curl -sS http://127.0.0.1:5080/health +curl -sS http://127.0.0.1:5176/ # frontend HTML +curl -sS http://127.0.0.1:8098/health +curl -sS https://fund.somhairle.bid/health +curl -sS https://fund.somhairle.bid/ # frontend HTML (not Somhairle's Dream) +curl -sS -H "Authorization: Bearer $FUND_LAB_AUTH_TOKEN" \ + https://fund.somhairle.bid/api/portfolio/summary +``` + +Auth is a static Bearer token (`FUND_LAB_AUTH_TOKEN`); there is no `/login` +endpoint. `/health` is anonymous; everything under `/api` requires the token. diff --git a/scripts/systemd/fundlab-api.service b/scripts/systemd/fundlab-api.service new file mode 100644 index 0000000..ecdbd9b --- /dev/null +++ b/scripts/systemd/fundlab-api.service @@ -0,0 +1,18 @@ +[Unit] +Description=fund-lab API (loopback 5080, real Postgres + real AKShare interpreter) +Documentation=file:/home/somhairle/projects/fund-lab/scripts/systemd/README.md +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +WorkingDirectory=/home/somhairle/projects/fund-lab +EnvironmentFile=/home/somhairle/projects/fund-lab/.env.deploy +ExecStart=/usr/bin/dotnet /home/somhairle/projects/fund-lab/src/FundLab.Api/publish/FundLab.Api.dll +Restart=on-failure +RestartSec=3 +StandardOutput=append:/home/somhairle/projects/fund-lab/var/fund-lab/api.log +StandardError=append:/home/somhairle/projects/fund-lab/var/fund-lab/api.log + +[Install] +WantedBy=default.target diff --git a/scripts/systemd/fundlab-gateway.service b/scripts/systemd/fundlab-gateway.service new file mode 100644 index 0000000..24500ef --- /dev/null +++ b/scripts/systemd/fundlab-gateway.service @@ -0,0 +1,18 @@ +[Unit] +Description=fund-lab web gateway (loopback 5176 frontend + 8098 tunnel origin -> static dist + /api,/health proxy) +Documentation=file:/home/somhairle/projects/fund-lab/scripts/systemd/README.md +After=network-online.target fundlab-api.service +Wants=network-online.target + +[Service] +Type=simple +WorkingDirectory=/home/somhairle/projects/fund-lab +ExecStartPre=/bin/mkdir -p /home/somhairle/projects/fund-lab/var/fund-lab/nginx-tmp +ExecStart=/usr/sbin/nginx -c /home/somhairle/projects/fund-lab/scripts/systemd/nginx-fundlab.conf -g 'daemon off;' +Restart=on-failure +RestartSec=3 +StandardOutput=append:/home/somhairle/projects/fund-lab/var/fund-lab/gateway.log +StandardError=append:/home/somhairle/projects/fund-lab/var/fund-lab/gateway.log + +[Install] +WantedBy=default.target diff --git a/scripts/systemd/nginx-fundlab.conf b/scripts/systemd/nginx-fundlab.conf new file mode 100644 index 0000000..e6fdcd4 --- /dev/null +++ b/scripts/systemd/nginx-fundlab.conf @@ -0,0 +1,70 @@ +# fund-lab production gateway (run as the unprivileged user; high ports only). +# Serves the built frontend from src/FundLab.Web/dist on both the frontend port +# (5176) and the cloudflared tunnel origin port (8098), and reverse-proxies +# /api and /health to the fund-lab API on 127.0.0.1:5080. + +worker_processes 1; +pid /home/somhairle/projects/fund-lab/var/fund-lab/nginx.pid; +error_log /home/somhairle/projects/fund-lab/var/fund-lab/nginx-error.log warn; + +events { + worker_connections 256; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + access_log /home/somhairle/projects/fund-lab/var/fund-lab/nginx-access.log; + + client_body_temp_path /home/somhairle/projects/fund-lab/var/fund-lab/nginx-tmp/client; + proxy_temp_path /home/somhairle/projects/fund-lab/var/fund-lab/nginx-tmp/proxy; + fastcgi_temp_path /home/somhairle/projects/fund-lab/var/fund-lab/nginx-tmp/fastcgi; + uwsgi_temp_path /home/somhairle/projects/fund-lab/var/fund-lab/nginx-tmp/uwsgi; + scgi_temp_path /home/somhairle/projects/fund-lab/var/fund-lab/nginx-tmp/scgi; + + upstream fundlab_api { + server 127.0.0.1:5080; + } + + server { + listen 127.0.0.1:5176; + server_name _; + root /home/somhairle/projects/fund-lab/src/FundLab.Web/dist; + index index.html; + + location = /health { + proxy_pass http://fundlab_api; + proxy_set_header Host $host; + } + + location /api/ { + proxy_pass http://fundlab_api; + proxy_set_header Host $host; + } + + location / { + try_files $uri /index.html; + } + } + + server { + listen 127.0.0.1:8098; + server_name _; + root /home/somhairle/projects/fund-lab/src/FundLab.Web/dist; + index index.html; + + location = /health { + proxy_pass http://fundlab_api; + proxy_set_header Host $host; + } + + location /api/ { + proxy_pass http://fundlab_api; + proxy_set_header Host $host; + } + + location / { + try_files $uri /index.html; + } + } +} |
