summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/overnight-progress.md25
-rw-r--r--scripts/systemd/README.md72
-rw-r--r--scripts/systemd/fundlab-api.service18
-rw-r--r--scripts/systemd/fundlab-gateway.service18
-rw-r--r--scripts/systemd/nginx-fundlab.conf70
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;
+ }
+ }
+}