Merge branch 'dev-0.2.1' into dev-0.2
# Conflicts: # generated/prisma/internal/class.ts # generated/prisma/internal/prismaNamespace.ts # generated/prisma/internal/prismaNamespaceBrowser.ts # generated/prisma/models/BoardColumn.ts # generated/prisma/models/Project.ts # generated/prisma/models/ProjectMember.ts # generated/prisma/models/Task.ts # generated/prisma/models/TaskMember.ts # generated/prisma/models/TaskTag.ts # generated/prisma/models/User.ts # server/routes/projects.js # server/routes/tasks.js # src/api/task.js
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
# 复制为 .env (服务器上 chmod 600, 已 gitignore)。
|
||||
# POWERSYNC_JWT_KEY_B64 = base64url(POWERSYNC_JWT_SECRET 的 UTF-8 字节)
|
||||
# 生成: printf %s "$POWERSYNC_JWT_SECRET" | openssl base64 -A | tr '+/' '-_' | tr -d '='
|
||||
|
||||
PG_PASSWORD=change-me
|
||||
PS_STORAGE_PASSWORD=change-me
|
||||
JWT_SECRET_021=change-me
|
||||
POWERSYNC_JWT_SECRET=change-me
|
||||
POWERSYNC_JWT_KEY_B64=change-me
|
||||
POWERSYNC_URL=https://bh.vps.honor3.com:13443
|
||||
OTA_PUBLIC_BASE=http://bh.vps.honor3.com:13001
|
||||
@@ -0,0 +1,61 @@
|
||||
# 0.2.1 独立验证部署 (baihu /app/iboard-0.2.1)
|
||||
|
||||
与现网 `/app/iboard`(3001/宿主 MySQL) 完全隔离:本栈使用独立 PG 容器、端口 13443/13001/13002,不写原库。
|
||||
|
||||
**浏览器入口: `https://bh.vps.honor3.com:13443`**(自签证书,首次访问需在浏览器"高级→继续访问"接受一次;PowerSync 网页端必须运行在安全上下文,http://13001 入口仅供接口调试,任务模块会报 Navigator locks 错误)。
|
||||
正式启用需替换受信证书:把 `bh.pem/bh.key` 换成受信证书后 `docker compose restart tls-front`(例如 Let's Encrypt DNS-01,80/443 已被 gateway 占用无法 HTTP-01)。
|
||||
|
||||
## 栈
|
||||
|
||||
| 服务 | 说明 |
|
||||
| --- | --- |
|
||||
| `pg` | postgres:16-alpine,`wal_level=logical`(供 PowerSync 逻辑复制),数据在 `./data/pg`,**不对外暴露端口** |
|
||||
| `app` | 本仓库 Dockerfile 构建,13001 对外;启动时先 `prisma migrate deploy`;`DISABLE_CRON=1` 关闭定时任务 |
|
||||
| `powersync` | journeyapps/powersync-service,13002 对外;源库=pg,bucket storage=同实例 PG 的 powersync schema |
|
||||
|
||||
## 首次部署
|
||||
|
||||
```bash
|
||||
# 服务器上 (源码已上传到 /app/iboard-0.2.1)
|
||||
cd /app/iboard-0.2.1/deploy/0.2.1
|
||||
|
||||
# 1. 生成 .env (参照 .env.example; POWERSYNC_JWT_KEY_B64 由 secret 派生)
|
||||
umask 077
|
||||
PG_PASSWORD=$(openssl rand -hex 16)
|
||||
PS_STORAGE_PASSWORD=$(openssl rand -hex 16)
|
||||
JWT_SECRET_021=$(openssl rand -hex 32)
|
||||
POWERSYNC_JWT_SECRET=$(openssl rand -hex 32)
|
||||
POWERSYNC_JWT_KEY_B64=$(printf %s "$POWERSYNC_JWT_SECRET" | openssl base64 -A | tr '+/' '-_' | tr -d '=')
|
||||
cat > .env <<EOF
|
||||
PG_PASSWORD=$PG_PASSWORD
|
||||
PS_STORAGE_PASSWORD=$PS_STORAGE_PASSWORD
|
||||
JWT_SECRET_021=$JWT_SECRET_021
|
||||
POWERSYNC_JWT_SECRET=$POWERSYNC_JWT_SECRET
|
||||
POWERSYNC_JWT_KEY_B64=$POWERSYNC_JWT_KEY_B64
|
||||
POWERSYNC_URL=http://bh.vps.honor3.com:13002
|
||||
OTA_PUBLIC_BASE=http://bh.vps.honor3.com:13001
|
||||
EOF
|
||||
|
||||
# 2. 构建并启动
|
||||
docker compose build app
|
||||
docker compose up -d
|
||||
|
||||
# 3. 验证
|
||||
docker compose ps
|
||||
curl -s http://127.0.0.1:13001/api/health
|
||||
docker compose logs powersync --tail 50 # 应看到连接源库/建 publication/sync config 部署成功
|
||||
```
|
||||
|
||||
## 日常更新
|
||||
|
||||
```bash
|
||||
cd /app/iboard-0.2.1
|
||||
git archive 方式重新上传源码 tar 后解包覆盖 (data/.env 不受影响)
|
||||
cd deploy/0.2.1 && docker compose build app && docker compose up -d app
|
||||
```
|
||||
|
||||
## 注意
|
||||
|
||||
- `./data/pg` 为 0.2.1 独立数据卷;`./.env` 为密钥,两者均已 gitignore,更新源码时不会覆盖 `.env`(tar 解包不产生它)。
|
||||
- PowerSync 的 publication / replication slot 由 service 自动管理(账号 iboard021 为容器内 superuser)。
|
||||
- 同步规则改动:编辑 `powersync/sync-config.yaml` 后 `docker compose restart powersync`。
|
||||
@@ -0,0 +1,84 @@
|
||||
name: iboard-021
|
||||
|
||||
# 0.2.1 独立验证栈: PG (逻辑复制) + PowerSync + App, 与现网 /app/iboard 完全隔离
|
||||
# 见 docs/V0.2/离线编辑模块/0.2.1实施方案.md; 端口 13001 (app) / 13002 (powersync), PG 不对外
|
||||
|
||||
services:
|
||||
pg:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
command: ["postgres", "-c", "wal_level=logical"]
|
||||
environment:
|
||||
POSTGRES_USER: iboard021
|
||||
POSTGRES_PASSWORD: ${PG_PASSWORD:?PG_PASSWORD missing}
|
||||
POSTGRES_DB: iboard021
|
||||
# initdb 脚本用: 创建 powersync bucket storage 用户
|
||||
PS_STORAGE_PASSWORD: ${PS_STORAGE_PASSWORD:?PS_STORAGE_PASSWORD missing}
|
||||
volumes:
|
||||
- ./data/pg:/var/lib/postgresql/data
|
||||
- ./initdb:/docker-entrypoint-initdb.d:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U iboard021 -d iboard021"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
|
||||
app:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
pg:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DATABASE_URL: postgresql://iboard021:${PG_PASSWORD}@pg:5432/iboard021
|
||||
PORT: "13001"
|
||||
JWT_SECRET: ${JWT_SECRET_021:?JWT_SECRET_021 missing}
|
||||
POWERSYNC_JWT_SECRET: ${POWERSYNC_JWT_SECRET:?POWERSYNC_JWT_SECRET missing}
|
||||
POWERSYNC_URL: ${POWERSYNC_URL:-http://bh.vps.honor3.com:13002}
|
||||
OTA_PUBLIC_BASE: ${OTA_PUBLIC_BASE:-http://bh.vps.honor3.com:13001}
|
||||
# 0.2.1 验证部署关闭定时任务 (新闻/油价/米家等), 避免重复外呼
|
||||
DISABLE_CRON: "1"
|
||||
command: ["sh", "-c", "npx prisma migrate deploy && node --experimental-strip-types --no-warnings server/index.js"]
|
||||
ports:
|
||||
- "13001:13001"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:13001/api/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
|
||||
# TLS 前置 (自签证书): PowerSync 网页端必须在安全上下文运行 (navigator.locks),
|
||||
# 因此浏览器入口统一走 https://bh.vps.honor3.com:13443 (证书接受一次即可, app+sync 同源同证书)
|
||||
tls-front:
|
||||
image: nginx:alpine
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- app
|
||||
- powersync
|
||||
volumes:
|
||||
- ./tls/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
- ./data/tls:/etc/nginx/certs:ro
|
||||
ports:
|
||||
- "13443:443"
|
||||
|
||||
powersync:
|
||||
image: journeyapps/powersync-service:latest
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
pg:
|
||||
condition: service_healthy
|
||||
command: ["start", "-r", "unified"]
|
||||
volumes:
|
||||
- ./powersync/service.yaml:/config/service.yaml:ro
|
||||
- ./powersync/sync-config.yaml:/config/sync-config.yaml:ro
|
||||
environment:
|
||||
POWERSYNC_CONFIG_PATH: /config/service.yaml
|
||||
# service.yaml 内 !env PS_* 引用 (整值替换; URI 在此组装)
|
||||
PS_SOURCE_URI: postgresql://iboard021:${PG_PASSWORD}@pg:5432/iboard021?sslmode=disable
|
||||
PS_STORAGE_URI: postgresql://powersync_storage:${PS_STORAGE_PASSWORD}@pg:5432/iboard021?sslmode=disable
|
||||
PS_JWT_KEY_B64: ${POWERSYNC_JWT_KEY_B64:?POWERSYNC_JWT_KEY_B64 missing}
|
||||
ports:
|
||||
- "13002:8080"
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
# 一次性 ETL: 清空 0.2.1 库 -> 从现网 MariaDB 只读快照导入
|
||||
# 现网连接串从 iboard 容器环境变量读取 (不回显), 源库仅 SELECT
|
||||
set -e
|
||||
cd /app/iboard-0.2.1/deploy/0.2.1
|
||||
SRC=$(docker inspect iboard --format '{{range .Config.Env}}{{println .}}{{end}}' | grep '^DATABASE_URL=' | cut -d= -f2-)
|
||||
[ -n "$SRC" ] || { echo '未取到现网 DATABASE_URL'; exit 1; }
|
||||
|
||||
echo '--- truncate 0.2.1 库 ---'
|
||||
docker compose exec -T pg psql -U iboard021 -d iboard021 -c "TRUNCATE TABLE \"User\",\"Project\",\"ProjectMember\",\"BoardColumn\",\"Task\",\"TaskMember\",\"TaskTag\",\"Habit\",\"CheckIn\",\"CheckList\",\"CheckListItem\",\"CheckListTemplate\",\"Article\",\"Strategy\",\"Indicator\",\"IndicatorPoint\",\"News\",\"Alert\",\"ServerNode\",\"ServerNodeHistory\",\"ServiceCheck\",\"Econ_SowInventory\",\"ScraperCache\",\"OilPriceHistory\",\"MihomeAccount\",\"MihomeHome\",\"MihomeDevice\",\"MihomeEnvSample\",\"MihomeEnvHour\" RESTART IDENTITY CASCADE;"
|
||||
|
||||
echo '--- etl run ---'
|
||||
docker compose exec -T -e SRC_DATABASE_URL="$SRC" app node scripts/etl-mariadb-to-pg.mjs
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
# 一次性初始化 (仅首次建库时执行): 创建 powersync bucket storage 专用用户
|
||||
# PG >= 14 允许源库与 bucket storage 同实例 (powersync schema 由 service 自建)
|
||||
set -e
|
||||
: "${PS_STORAGE_PASSWORD:?PS_STORAGE_PASSWORD missing}"
|
||||
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||
DO \$\$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'powersync_storage') THEN
|
||||
CREATE USER powersync_storage WITH PASSWORD '$PS_STORAGE_PASSWORD';
|
||||
END IF;
|
||||
END
|
||||
\$\$;
|
||||
GRANT CREATE ON DATABASE $POSTGRES_DB TO powersync_storage;
|
||||
EOSQL
|
||||
|
||||
echo "[initdb] powersync_storage user ready."
|
||||
@@ -0,0 +1,10 @@
|
||||
-- 一次性初始化: PowerSync 逻辑复制所需 publication (服务不自建, 见 PSYNC_S1141)
|
||||
-- 仅发布任务域 + User; 新增同步表时需 ALTER PUBLICATION powersync ADD TABLE ...
|
||||
CREATE PUBLICATION powersync FOR TABLE
|
||||
"Project",
|
||||
"ProjectMember",
|
||||
"BoardColumn",
|
||||
"Task",
|
||||
"TaskMember",
|
||||
"TaskTag",
|
||||
"User";
|
||||
@@ -0,0 +1,41 @@
|
||||
# PowerSync Service 主配置 (0.2.1 验证部署)
|
||||
# 参考: https://docs.powersync.com/configuration/powersync-service/self-hosted-instances
|
||||
# 注意: !env 是 YAML 整值标签, 只能整个值替换变量, 不能内嵌在 URI 字符串中;
|
||||
# 完整连接串由 docker-compose 以 PS_* 环境变量注入。
|
||||
|
||||
replication:
|
||||
connections:
|
||||
- type: postgresql
|
||||
# 源库: 本栈 pg 容器 (0.2.1 独立实例), 复制走 iboard021 账号
|
||||
uri: !env PS_SOURCE_URI
|
||||
sslmode: disable
|
||||
|
||||
# bucket storage: 同实例 PG (16 >= 14 允许同库不同 schema, service 自建 powersync schema)
|
||||
storage:
|
||||
type: postgresql
|
||||
uri: !env PS_STORAGE_URI
|
||||
sslmode: disable
|
||||
|
||||
port: 8080
|
||||
|
||||
sync_config:
|
||||
path: /config/sync-config.yaml
|
||||
|
||||
# 客户端鉴权: app 后端 /api/powersync/token 签发 HS256 JWT (aud=powersync)
|
||||
# oct 密钥 k = base64url(POWERSYNC_JWT_SECRET)
|
||||
client_auth:
|
||||
jwks:
|
||||
keys:
|
||||
- kty: oct
|
||||
k: !env PS_JWT_KEY_B64
|
||||
alg: HS256
|
||||
audience:
|
||||
- powersync
|
||||
|
||||
telemetry:
|
||||
disable_telemetry_sharing: true
|
||||
|
||||
system:
|
||||
logging:
|
||||
level: info
|
||||
format: text
|
||||
@@ -0,0 +1,25 @@
|
||||
# Sync Streams (edition 3): 任务模块 6 表 + User (展示用)
|
||||
# Sequential ID Mapping (见 docs/V0.2/离线编辑模块/0.2.1实施方案.md §3):
|
||||
# - 客户端主键 = uuid 列 (离线新建行同步回填后 id 稳定); int 自增 id 不下发
|
||||
# - 外键输出 uuid 镜像列, 别名回 projectId/columnId/parentId/taskId (客户端字段名不变)
|
||||
# - 引号保留 PG PascalCase 标识; 可见性按 ProjectMember 过滤
|
||||
|
||||
config:
|
||||
edition: 3
|
||||
|
||||
streams:
|
||||
iboard_users:
|
||||
auto_subscribe: true
|
||||
query: SELECT CAST("id" AS TEXT) AS "id", "name", "email" FROM "User"
|
||||
|
||||
iboard_task_data:
|
||||
auto_subscribe: true
|
||||
with:
|
||||
my_projects: SELECT "projectId" FROM "ProjectMember" WHERE CAST("userId" AS TEXT) = auth.user_id()
|
||||
queries:
|
||||
- SELECT "uuid" AS "id", "uuid", "code", "name", CAST("ownerId" AS TEXT) AS "ownerId", "isPersonal", "archived", "notes", "taskKinds", "icon", "autoArchive", "createdAt", "updatedAt" FROM "Project" WHERE "id" IN my_projects
|
||||
- SELECT "uuid" AS "id", "uuid", "projectUuid" AS "projectId", CAST("userId" AS TEXT) AS "userId", "permission", "createdAt" FROM "ProjectMember" WHERE "projectId" IN my_projects
|
||||
- SELECT "uuid" AS "id", "uuid", "projectUuid" AS "projectId", "name", "sortOrder", "boundStatus", "retention", "createdAt", "updatedAt" FROM "BoardColumn" WHERE "projectId" IN my_projects
|
||||
- SELECT "uuid" AS "id", "uuid", "projectUuid" AS "projectId", "columnUuid" AS "columnId", "parentUuid" AS "parentId", "title", "description", "kind", "markDate", "status", "doneAt", CAST("assigneeId" AS TEXT) AS "assigneeId", CAST("creatorId" AS TEXT) AS "creatorId", "extras", "sortOrder", "number", "archived", "archivedAt", "archiveSource", "createdAt", "updatedAt" FROM "Task" WHERE "projectId" IN my_projects
|
||||
- SELECT "uuid" AS "id", "uuid", "taskUuid" AS "taskId", CAST("userId" AS TEXT) AS "userId" FROM "TaskMember" WHERE "taskId" IN (SELECT "id" FROM "Task" WHERE "projectId" IN my_projects)
|
||||
- SELECT "uuid" AS "id", "uuid", "taskUuid" AS "taskId", "name" FROM "TaskTag" WHERE "taskId" IN (SELECT "id" FROM "Task" WHERE "projectId" IN my_projects)
|
||||
@@ -0,0 +1,37 @@
|
||||
# 0.2.1 TLS 前置: 单端口 13443, 按路径分流 (自签证书, 见 README)
|
||||
# PowerSync 页面必须在安全上下文运行 (navigator.locks 仅 HTTPS/localhost), 故 app 与 sync 统一走 TLS
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name bh.vps.honor3.com;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/bh.pem;
|
||||
ssl_certificate_key /etc/nginx/certs/bh.key;
|
||||
|
||||
# PowerSync (HTTP 流式 + WebSocket 兜底)
|
||||
location /sync/ {
|
||||
proxy_pass http://powersync:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection '';
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
}
|
||||
location /socket/ {
|
||||
proxy_pass http://powersync:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 3600s;
|
||||
}
|
||||
location /health {
|
||||
proxy_pass http://powersync:8080;
|
||||
}
|
||||
|
||||
# 应用 (静态前端 + API)
|
||||
location / {
|
||||
proxy_pass http://app:13001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user