diff --git a/CLAUDE.md b/CLAUDE.md index 276e22b..84f9730 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,8 +16,8 @@ **Framework7 9**(`framework7-react`,iOS 主题)+ Recharts 图表。根目录 `package.json` 是 npm workspaces(含 `client`)。 - **数据库**:可插拔数据层(`backend/db.py`),`DB_TYPE` 切换—— - 本地开发 SQLite(`backend/data/health.db`,默认);生产 NAS MariaDB - (`garmin_health_lab` 库,连接配置见 NAS 上 `backend/.env`)。 + 本地开发 SQLite(`backend/data/health.db`,默认);生产 MariaDB(甲骨文云主机 + 本机,`garmin_health_lab` 库,连接配置见该机 `/opt/garmin-health-lab/backend/.env`)。 - **认证**:**auth-hub SSO**(OAuth2/OIDC,`services/auth_hub_client.py`), 本地邮箱密码注册/登录已移除;应用内 JWT 由后端签发。 - **AI**:自建 ai-gateway(OpenAI 兼容,`https://ai.zichuan.xyz/v1`)为唯一上游 @@ -57,7 +57,8 @@ GarminHealthLab/ │ ├── src/lib/ # day.ts(本地日期)/ metrics.ts(指标注册表) │ ├── src/features.ts # 功能开关(FEATURES.ai) │ └── .env.production # REACT_APP_API_URL=/api(同源,防 Network Error) -├── deploy/ # NAS 部署脚本族(S99garmin/start/stop/push/deploy) +├── deploy/ # push_oracle.sh 现役;其余(S99garmin/start/stop/ +│ # push/deploy)是 NAS 时代脚本,已废弃保留参考 ├── docs/ # 架构 / 开发 / 需求文档 ├── PROGRESS.md # 进度与部署事实(排查必读) └── README.md # 项目说明 + API 文档 @@ -83,24 +84,42 @@ npm run build # client/react-scripts build → client/build/ cd backend && .venv/bin/python tests/smoke.py ``` -## 部署(生产 = NAS,端口 8124) +## 部署(生产 = 甲骨文云主机,2026-09-12 起;此前是 NAS,已下线) -- **位置**:NAS `192.168.50.64` `/volume1/web/garmin-health-lab`,root 用户跑 - gunicorn `0.0.0.0:8124`(2 workers / 4 threads / --timeout 300),开机自启走 - DSM 任务调度器执行 `deploy/S99garmin.sh`。 -- **一键部署**:本地 `./deploy/push.sh`(tar 经 ssh 同步 backend + deploy + - 清空重推 client/build + sudo 重启,**校验 gunicorn pid 变化 + health 200**)。 - 前置:先 `npm run build`。 -- **公网**:NAS frpc → 甲骨文 `http://129.146.26.249:8124`(frp 重连需几秒)。 -- **DB**:NAS MariaDB 10.11,root 经 socket `/run/mysqld/mysqld10.sock`(或 - TCP 127.0.0.1:3306),库 `garmin_health_lab`。10.11 dump 含 `/*M!999999` - 注释、旧客户端会报错,且须 `--default-character-set=utf8mb4` 防中文丢失。 -- **auth-hub**:client `996aLPw4T5gl-rYZ`;回调注册了 LAN - `http://192.168.50.64:8124/auth/callback` 与公网 - `http://129.146.26.249:8124/auth/callback` 两个地址。 +- **位置**:`129.146.26.249` `/opt/garmin-health-lab`,**`ubuntu` 用户**跑 + gunicorn `127.0.0.1:5500`(2 workers / 4 threads / --timeout 300), + systemd 单元 `garmin-health-lab.service`(`enabled`,随机器开机自启, + `Restart=always`)。和这台机器上其它服务(ai-gateway / auth-hub / fam-edge) + 同一套约定:`/opt/<项目>` 下独立部署 + 独立 venv + Caddy 按子域名反代。 +- **公网**:`https://garmin.zichuan.xyz` → Caddy → `127.0.0.1:5500`。 + DNS(腾讯云 DNSPod,A 记录)与 TLS(Caddy 自动签发)都已配好。 + ~~旧的 `http://129.146.26.249:8124`~~ 已下线(走 NAS frpc 转发,隧道已拆)。 +- **一键部署**:本地 `./deploy/push_oracle.sh`(tar 经 ssh key 认证同步 + backend + 清空重推 client/build + pip install + systemctl restart, + **校验 main PID 变化 + health 200**)。前置:先 `npm run build`。 + `deploy/push.sh` / `start.sh` / `stop.sh` / `S99garmin.sh` / `deploy.sh` + 是 NAS 时代的脚本,已废弃保留仅供参考。 +- **DB**:甲骨文本机 MariaDB 10.3.39(`127.0.0.1:3306`),独立账号 + `garmin`(**注意**:`garmin@localhost` 与 `garmin@127.0.0.1` 是两个不同账号, + 必须同密码建两份,否则 TCP 连接用的是另一个密码),库 `garmin_health_lab`。 + 和其它项目(`chat_relay`、`zhongyuan`)共用同一个 MariaDB 实例,各自独立库 + 独立账号。**这台机器磁盘 96% 已满**,改动前留意剩余空间。 +- **AI 网关**:`AI_GATEWAY_BASE_URL` 现在是**本地回环** `http://127.0.0.1:5100/v1` + (不再经 Caddy/公网 —— 网关和这个服务同机了)。 +- **auth-hub**:client `996aLPw4T5gl-rYZ`;回调只保留 + `https://garmin.zichuan.xyz/auth/callback` 一条(NAS/旧公网端口那几条已用 + `manage_clients remove-redirect-uri` 清掉)。改注册用 + `/opt/auth-hub` 下 `PYTHONPATH=/opt/auth-hub/src venv/bin/python -m + auth_hub.manage_clients`。 +- **NAS 现状**:`garmin_health_lab` 库已 `DROP DATABASE`(备份在本地 + `~/Desktop/Work/backups/garmin_health_lab_nas_backup_20260912.sql.gz`, + 20 张表逐条精确计数核对过一致后才删的),`S99garmin.sh` 开机项已移除, + frpc 配置里 `garmin-health` 转发段已删(`gitea`/`wordpress`/`fam-core`/ + `nexusai` 那几条没动——**Gitea 还在 NAS 上,这个仓库的 git remote 仍然指 + 向它**,这次迁移只搬了应用和数据,没搬源码托管)。 - **部署/排障前**:先读 `PROGRESS.md` 与 `deploy/` 脚本确认事实(曾经凭旧记忆 - 断言"无线上环境"而误判)。服务以 root 运行:重启用 sudo,日志 - `logs/error.log`、`logs/access.log` 是 root 所有。 + 断言"无线上环境"而误判,后来又把"生产在 NAS"当成默认事实——**两次都错在 + 没有先查,部署位置是会变的**)。 ## 关键约定与坑(写代码/改样式前看) diff --git a/PROGRESS.md b/PROGRESS.md index 479f334..c1d0ef6 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -10,13 +10,55 @@ - [x] Gunicorn 生产服务器配置(2 workers / 4 threads) -- [x] 部署到本地 NAS(`/volume1/web/garmin-health-lab`),端口 8124 +- [x] ~~部署到本地 NAS(`/volume1/web/garmin-health-lab`),端口 8124~~ 已下线,见下 -- [x] frp 隧道配置,通过甲骨文公网 IP 外网访问(`http://129.146.26.249:8124`) +- [x] ~~frp 隧道配置,通过甲骨文公网 IP 外网访问(`http://129.146.26.249:8124`)~~ 已拆除 -- [x] 开机自启脚本(`deploy/S99garmin.sh`) +- [x] ~~开机自启脚本(`deploy/S99garmin.sh`)~~ 已移除,脚本保留仅供参考 -- [x] 甲骨文 iptables 放行 8124 端口 +- [x] ~~甲骨文 iptables 放行 8124 端口~~ 该端口不再使用 + +### 迁移到甲骨文云主机(2026-09-12) + +NAS 的局域网 IP 因 DHCP 重启换过两次(`.64` → `.65` → 又变回 `.64`),且磁盘/ +网络都不如已经稳定跑着好几个服务的甲骨文机器可靠。整体搬迁,NAS 只保留 Gitea +(这个仓库的源码托管,未动)。 + +- [x] 甲骨文本机新建 MariaDB 库 `garmin_health_lab` + 独立账号 `garmin`(同时建 + `garmin@localhost` 与 `garmin@127.0.0.1` 两个 host 变体、同密码——MariaDB 视 + 为两个不同账号,只建一个会导致 TCP 连接失败) + +- [x] NAS `mysqldump`(10.11 源库,处理了 `/*M!999999` 注释兼容 10.3 目标)→ + 导入甲骨文 MariaDB 10.3.39,**20 张表逐条精确 `COUNT(*)` 比对完全一致**才继续 + +- [x] 应用部署到 `/opt/garmin-health-lab`,systemd 单元 `garmin-health-lab.service` + (`ubuntu` 用户,非 root;和 ai-gateway/auth-hub 同一套约定) + +- [x] DNS:腾讯云 DNSPod API 新增 `garmin.zichuan.xyz` A 记录;Caddy 反代 + + 自动签发 TLS,实测已生效 + +- [x] auth-hub 回调地址改为 `https://garmin.zichuan.xyz/auth/callback`,清掉 + NAS/旧公网端口那几条历史回调 + +- [x] AI 网关配置从公网域名改本地回环 `http://127.0.0.1:5100/v1`(网关和本服务 + 现在同机),**触发真实生成验证通过**(`upstream: gemini`) + +- [x] 迁移窗口期加了安全阀:甲骨文这边先 `AUTO_SYNC=false`,避免和 NAS 的调度 + 器同时刷新 Garmin 令牌撞上按账号计算的 SSO 限流(见下方"同步逻辑修复"一节 + 09-03 那次事故)。确认 NAS 已冻结、数据无缺口后才在甲骨文打开 + +- [x] 冻结 NAS(停 gunicorn)→ 补一次终态 dump 核对无数据差异 → 移除开机自启 + → `DROP DATABASE garmin_health_lab`(备份在本地 + `~/Desktop/Work/backups/garmin_health_lab_nas_backup_20260912.sql.gz`) + +- [x] NAS frpc 配置精确删除 `garmin-health` 转发段,`gitea`/`wordpress`/ + `fam-core`/`nexusai` 几条未动,改后逐条复检确认其它站点未受影响 + +- [x] 新部署脚本 `deploy/push_oracle.sh`(key 认证 + systemd,校验 PID 变化 + + health 200),落地当场跑通一次真实部署 + +- [x] `CLAUDE.md` 部署章节整体重写;NAS 时代的 `deploy/` 脚本加了废弃说明保留 + 参考,未删除 ### 认证 diff --git a/README.md b/README.md index 2d21e71..f0d2261 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,9 @@ ### 后端 - Python 3.10+ + Flask(应用工厂) -- Gunicorn(生产运行,NAS :8124) -- 可插拔数据层:SQLite(本地开发)/ MariaDB(生产,NAS 10.11 库 - `garmin_health_lab`,经 PyMySQL/socket) +- Gunicorn(生产运行,甲骨文云主机 :5500,Caddy 反代出 `garmin.zichuan.xyz`) +- 可插拔数据层:SQLite(本地开发)/ MariaDB(生产,与应用同机的 10.3.39 库 + `garmin_health_lab`,经 PyMySQL/TCP) - JWT 鉴权;登录走 auth-hub 统一 SSO(OAuth2/OIDC,本地密码登录已移除) - garminconnect(Garmin API 集成) @@ -93,8 +93,8 @@ CORS_ORIGIN=http://localhost:3000,http://localhost:5173 数据层通过 `DB_TYPE` 环境变量切换后端,**业务代码无需改动**: - **SQLite(默认,本地开发)**:零配置,由 `DATABASE_PATH` 指定文件位置。 -- **MariaDB(生产)**:运行于 NAS(192.168.50.64)本地 MariaDB 10.11,root 经 - socket `/run/mysqld/mysqld10.sock`(或 TCP `127.0.0.1:3306`)连接(PyMySQL), +- **MariaDB(生产)**:运行于甲骨文云主机(`129.146.26.249`)本地 MariaDB + 10.3.39,独立账号 `garmin` 经 TCP `127.0.0.1:3306` 连接(PyMySQL), 独立库 `garmin_health_lab`。完整配置见 `backend/.env.example`: ```env @@ -195,15 +195,17 @@ python tests/smoke.py > 轮询,趋势归因与 Copilot 走显式触发;任一模型失败时降级为规则引擎, > `meta.source` 会说明本次由谁作答。 -## 🚀 部署(生产 = NAS) +## 🚀 部署(生产 = 甲骨文云主机,2026-09-12 起) -- **位置**:NAS `192.168.50.64` `/volume1/web/garmin-health-lab`,root 跑 - gunicorn `0.0.0.0:8124`(2 workers / 4 threads / --timeout 300),开机自启 - DSM 任务调度器执行 `deploy/S99garmin.sh`。 -- **公网**:NAS frpc → `http://129.146.26.249:8124`。 -- **一键部署**:先 `npm run build`,再 `./deploy/push.sh`(同步 backend + - deploy + 清空重推 `client/build` + sudo 重启 + pid/health 双校验)。 -- **部署排障前**:先读 `PROGRESS.md` 与 `deploy/` 脚本确认事实。 +- **位置**:`129.146.26.249` `/opt/garmin-health-lab`,`ubuntu` 用户跑 + gunicorn `127.0.0.1:5500`(2 workers / 4 threads / --timeout 300), + systemd 单元 `garmin-health-lab.service`。 +- **公网**:`https://garmin.zichuan.xyz` → Caddy → `127.0.0.1:5500`。 +- **一键部署**:先 `npm run build`,再 `./deploy/push_oracle.sh`(ssh key 同步 + backend + 清空重推 `client/build` + pip install + systemctl restart + + PID/health 双校验)。 +- **部署排障前**:先读 `PROGRESS.md` 与 `deploy/` 脚本确认事实——部署位置换过 + 一次(NAS → 甲骨文),凭记忆断言曾经出过错。 ## 🔐 安全说明 diff --git a/backend/.env.example b/backend/.env.example index ac53126..d66eac3 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -8,14 +8,15 @@ 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 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 (used when DB_TYPE=mariadb) — production DB is co-located with the +# app on the Oracle box (129.146.26.249, MariaDB 10.3.39), TCP 127.0.0.1:3306, +# dedicated account (not root). MariaDB treats `user@localhost` and +# `user@127.0.0.1` as two different accounts — if you create this user by +# hand, create both host variants with the same password, or TCP connections +# fail with a password that looks right but is not the one that account has. # MARIADB_HOST=127.0.0.1 # MARIADB_PORT=3306 -# MARIADB_USER=root +# MARIADB_USER=garmin # MARIADB_PASSWORD=your_production_mariadb_password # MARIADB_DATABASE=garmin_health_lab @@ -38,9 +39,10 @@ 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). 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 +# registers https://garmin.zichuan.xyz/auth/callback — add/remove redirect +# URIs on that client with /opt/auth-hub's manage_clients CLI, not by editing +# auth-hub's own database directly. +AUTH_HUB_REDIRECT_URI=http://127.0.0.1:5500/auth/callback # --- CORS (comma-separated allowed front-end origins) --- # localhost stays in the production list on purpose: CORS is not an auth @@ -57,9 +59,12 @@ CORS_ORIGIN=http://localhost:3000,http://localhost:5173 # absorbs single-vendor quota limits. Reached directly, bypassing any local # HTTP proxy. NOTE: its NVIDIA upstream is a large reasoning model — replies # 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://ai.zichuan.xyz/v1 +# Production (co-located with ai-gateway on the same Oracle box) uses the +# loopback address — skips Caddy and the public hop entirely, and the token +# never leaves localhost either way. Use the HTTPS domain instead only when +# this app runs somewhere else: the token rides in an Authorization header +# and must not cross the public internet in the clear. +AI_GATEWAY_BASE_URL=http://127.0.0.1:5100/v1 AI_GATEWAY_TOKEN= AI_GATEWAY_MODEL=ai-gateway-auto diff --git a/backend/config.py b/backend/config.py index 0845a0a..e4260ef 100644 --- a/backend/config.py +++ b/backend/config.py @@ -42,13 +42,13 @@ JWT_EXPIRY_DAYS = int(os.environ.get("JWT_EXPIRY_DAYS") or 7) # 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. +# databases). Production always overrides this via backend/.env — the value +# in use is the client registered for https://garmin.zichuan.xyz/auth/callback. 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 "" -# 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" +# Production sets this via backend/.env to https://garmin.zichuan.xyz/auth/callback +# — the fallback below is a dev-only placeholder, not a live production route. +AUTH_HUB_REDIRECT_URI = os.environ.get("AUTH_HUB_REDIRECT_URI") or "http://127.0.0.1:5500/auth/callback" # --- Static UI -------------------------------------------------------------- # Directory holding the built React app. When set and populated, the Flask diff --git a/deploy/S99garmin.sh b/deploy/S99garmin.sh index 69b4b97..946829a 100755 --- a/deploy/S99garmin.sh +++ b/deploy/S99garmin.sh @@ -1,4 +1,6 @@ #!/bin/sh +# RETIRED 2026-09-12: production moved off the NAS to the Oracle box +# (systemd unit garmin-health-lab.service there). Kept for reference only. # Garmin Health Lab — DSM boot script. # Mirrors the convention already used by S99frpc.sh on this NAS. APP=/volume1/web/garmin-health-lab diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 7e38aec..4c034e2 100644 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -1,4 +1,6 @@ #!/bin/sh +# RETIRED 2026-09-12: production moved off the NAS to the Oracle box +# (systemd unit garmin-health-lab.service there). Kept for reference only. # Deploy the latest local code to NAS. # Usage: sh deploy/deploy.sh # Prerequisites: client has been built (`npm run build` in client/) diff --git a/deploy/push.sh b/deploy/push.sh index 8608693..2b81caa 100755 --- a/deploy/push.sh +++ b/deploy/push.sh @@ -1,4 +1,13 @@ #!/bin/sh +# RETIRED 2026-09-12: production moved off the NAS to the Oracle box — see +# deploy/push_oracle.sh. The NAS's garmin_health_lab database has been +# DROPPED and its S99garmin.sh boot entry removed; this script now has +# nothing live to deploy to. Kept for reference (the tar-over-ssh dance for +# a password-only host may be useful again) — do not run it expecting it to +# reach a running service. +# +# --- original header below --- +# # Push this working tree to the NAS and restart it. # # The NAS only accepts password auth, so one ssh master connection is opened diff --git a/deploy/push_oracle.sh b/deploy/push_oracle.sh new file mode 100755 index 0000000..187f9d0 --- /dev/null +++ b/deploy/push_oracle.sh @@ -0,0 +1,65 @@ +#!/bin/sh +# Push this working tree to the Oracle box and restart it. +# +# Unlike deploy/push.sh (the retired NAS deployment), this target uses key +# auth and systemd — matching the convention every other service on that box +# already follows (ai-gateway, auth-hub, fam-edge, ...): code lives under +# /opt/, gunicorn is a systemd unit, Caddy reverse-proxies a subdomain +# to a local port. Nothing here needs a password or sudo for the deploy +# itself; only the one-time systemd/Caddy setup did, and that is already done. +# +# ./deploy/push_oracle.sh [user@host] [ssh-key] +# +# Never touches backend/.env, .venv or the database on the far side. +set -e + +HOST="${1:-ubuntu@129.146.26.249}" +KEY="${2:-$HOME/.ssh/oracle_new}" +APP=/opt/garmin-health-lab +REPO="$(cd "$(dirname "$0")/.." && pwd)" + +sh_() { ssh -i "$KEY" -o BatchMode=yes "$HOST" "$@"; } + +if [ ! -f "$REPO/client/build/index.html" ]; then + echo "client/build is missing — run 'npm run build' first" >&2 + exit 1 +fi + +# macOS bsdtar writes com.apple.provenance xattrs and ._ resource forks that +# the Linux side cannot read; --no-xattrs plus COPYFILE_DISABLE strip them. +TAR="tar czf - --no-xattrs" +export COPYFILE_DISABLE=1 + +echo "==> backend" +$TAR --exclude .venv --exclude .env --exclude __pycache__ \ + --exclude '*.db' --exclude tests --exclude .pytest_cache --exclude static \ + -C "$REPO/backend" . | sh_ "tar xzf - -C '$APP/backend'" + +echo "==> static (cleared first, so stale JS chunks do not pile up)" +sh_ "rm -rf '$APP/backend/static' && mkdir -p '$APP/backend/static'" +$TAR -C "$REPO/client/build" . | sh_ "tar xzf - -C '$APP/backend/static'" + +echo "==> pip install (in case requirements.txt changed)" +sh_ "$APP/backend/.venv/bin/pip install -q -r $APP/backend/requirements.txt" + +echo "==> restart" +BEFORE=$(sh_ "systemctl show garmin-health-lab -p MainPID --value" || true) +sh_ "sudo systemctl restart garmin-health-lab" +sleep 2 +AFTER=$(sh_ "systemctl show garmin-health-lab -p MainPID --value" || true) +if [ -z "$AFTER" ] || [ "$AFTER" = "0" ]; then + echo "service failed to come back up — check journalctl -u garmin-health-lab" >&2 + exit 1 +fi +if [ "$BEFORE" = "$AFTER" ]; then + echo "the main PID did not change ($AFTER) — the old process may still be" >&2 + echo "serving and your changes are NOT live." >&2 + exit 1 +fi +echo "==> restarted: $BEFORE -> $AFTER" + +echo "==> health" +sh_ "curl -sf -m 5 -o /dev/null -w 'local: %{http_code}\n' http://127.0.0.1:5500/api/health/status" \ + || echo "local: unreachable" +curl -sf -m 8 -o /dev/null -w "public (https://garmin.zichuan.xyz): %{http_code}\n" \ + https://garmin.zichuan.xyz/api/health/status || echo "public: unreachable" diff --git a/deploy/start.sh b/deploy/start.sh index ae2f553..fc92423 100755 --- a/deploy/start.sh +++ b/deploy/start.sh @@ -1,4 +1,6 @@ #!/bin/sh +# RETIRED 2026-09-12: production moved off the NAS to the Oracle box +# (systemd unit garmin-health-lab.service there). Kept for reference only. # Start Garmin Health Lab. Safe to run repeatedly: an already-running # instance is stopped first. Intended for DSM Task Scheduler (boot-up). APP="$(cd "$(dirname "$0")/.." && pwd)" diff --git a/deploy/stop.sh b/deploy/stop.sh index f8da08d..8fdabbb 100755 --- a/deploy/stop.sh +++ b/deploy/stop.sh @@ -1,4 +1,6 @@ #!/bin/sh +# RETIRED 2026-09-12: production moved off the NAS to the Oracle box +# (systemd unit garmin-health-lab.service there). Kept for reference only. APP="$(cd "$(dirname "$0")/.." && pwd)" GUNICORN="$APP/backend/.venv/bin/gunicorn"