diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3aaad50..9a84e20 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,48 +1,32 @@ -name: Deploy WeReadBoard +name: Verify WeReadBoard on: push: branches: - master + - dev + pull_request: + branches: + - master + - dev workflow_dispatch: jobs: - deploy: + verify: runs-on: ubuntu-latest - concurrency: - group: deploy-master - cancel-in-progress: true steps: - - name: Prepare SSH - run: | - mkdir -p ~/.ssh - printf '%s\n' "${{ secrets.SERVER_SSH_KEY }}" > ~/.ssh/deploy_key - chmod 600 ~/.ssh/deploy_key - - - name: Deploy - env: - SERVER_HOST: ${{ secrets.SERVER_HOST }} - SERVER_PORT: ${{ secrets.SERVER_PORT }} - SERVER_USER: ${{ secrets.SERVER_USER }} - DEPLOY_PATH: ${{ vars.DEPLOY_PATH }} - SERVICE_NAME: ${{ vars.SERVICE_NAME }} - DEPLOY_BRANCH: master - run: | - if [ -z "$SERVER_PORT" ]; then - SERVER_PORT="22" - fi - if [ -z "$DEPLOY_PATH" ]; then - DEPLOY_PATH="/opt/weread-board/WeReadBoard" - fi - if [ -z "$SERVICE_NAME" ]; then - SERVICE_NAME="weread.service" - fi - - ssh -i ~/.ssh/deploy_key -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new -p "$SERVER_PORT" "$SERVER_USER@$SERVER_HOST" \ - "cd '$DEPLOY_PATH' && - git fetch origin && - git checkout '$DEPLOY_BRANCH' && - git pull --ff-only origin '$DEPLOY_BRANCH' && - chmod +x ./deploy.sh && - SKIP_GIT_PULL=1 DEPLOY_BRANCH='$DEPLOY_BRANCH' SERVICE_NAME='$SERVICE_NAME' ./deploy.sh" + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install dependencies + run: python -m pip install -r requirements.txt + - name: Run Python tests + run: python -m unittest discover -s tests -v + - name: Run frontend tests + run: node --test tests/goals_frontend.test.cjs tests/weapi_fallback.test.cjs + - name: Check deployment scripts + run: bash tests/deploy_master_only.test.sh && bash -n deploy.sh diff --git a/README.md b/README.md index 6c42184..21f1208 100644 --- a/README.md +++ b/README.md @@ -64,13 +64,13 @@ PORT=8080 ./start_dashboard.sh # 或指定端口 ## 自动部署 -服务器部署使用根目录的 `deploy.sh`: +服务器部署使用根目录的 `deploy.sh`,由 GitHub Webhook 通知服务器触发: ```bash DEPLOY_BRANCH=master ./deploy.sh ``` -脚本只允许拉取 `master` 分支,随后自动创建 `.venv`、安装 `requirements.txt`、执行数据库迁移并重启 `weread.service`。GitHub Actions 配置在 `.github/workflows/deploy.yml`,只监听 `master` 分支,详细配置见 [master 自动部署](docs/deploy-master.md)。 +脚本只允许拉取 `master` 分支,随后自动创建 `.venv`、安装 `requirements.txt`、执行数据库迁移并重启 `weread.service`。GitHub Actions 只做代码验证,不再通过 SSH 登录服务器;详细配置见 [master 自动部署](docs/deploy-master.md)。 运行端口、Python 环境、HTTPS 证书和接口故障的诊断方法见 [日常维护与故障排查](docs/troubleshooting.md)。 @@ -84,7 +84,7 @@ DEPLOY_BRANCH=master ./deploy.sh | 偏好分类 | 阅读分类占比饼图 | | 偏好作者 | 阅读最多的作者排行 | | 读书排行 | 阅读时长最久的书籍 TOP 20 | -| 书架 | 全部书架书籍展示,支持分类筛选和已读完筛选 | +| 书架 | 每页 48 本,底部页码分页,支持按最近阅读、书名或作者排序 | | 笔记概览 | 有笔记的书籍及划线/想法/书签统计 | ## 数据库表结构 diff --git a/deploy/nginx-github-webhook.conf b/deploy/nginx-github-webhook.conf new file mode 100644 index 0000000..b1e3d72 --- /dev/null +++ b/deploy/nginx-github-webhook.conf @@ -0,0 +1,7 @@ +# 放入现有 laohang.space HTTPS server 块中。 +location = /github/webhook { + proxy_pass http://127.0.0.1:9001/github/webhook; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +} diff --git a/deploy/weread-webhook.service b/deploy/weread-webhook.service new file mode 100644 index 0000000..1c58d81 --- /dev/null +++ b/deploy/weread-webhook.service @@ -0,0 +1,15 @@ +[Unit] +Description=WeReadBoard GitHub Webhook Receiver +After=network.target + +[Service] +Type=simple +User=weread +WorkingDirectory=/opt/weread-board/WeReadBoard +EnvironmentFile=/etc/weread-webhook.env +ExecStart=/opt/weread-board/WeReadBoard/.venv/bin/python deploy_webhook.py +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/deploy_webhook.py b/deploy_webhook.py new file mode 100644 index 0000000..3b7a809 --- /dev/null +++ b/deploy_webhook.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""接收 GitHub push Webhook,并触发服务器上的固定部署脚本。""" + +import hmac +import os +import subprocess +import threading +from hashlib import sha256 + +from flask import Flask, jsonify, request + + +DEFAULT_PROJECT_DIR = "/opt/weread-board/WeReadBoard" +DEFAULT_DEPLOY_SCRIPT = "deploy.sh" +DEFAULT_REPOSITORY = "KingHWJ/WeReadBoard" +DEFAULT_BRANCH = "master" + + +def create_app(config=None): + app = Flask(__name__) + app.config.from_mapping( + WEBHOOK_SECRET=os.environ.get("GITHUB_WEBHOOK_SECRET", ""), + REPOSITORY=os.environ.get("GITHUB_REPOSITORY", DEFAULT_REPOSITORY), + BRANCH=DEFAULT_BRANCH, + PROJECT_DIR=os.environ.get("DEPLOY_PROJECT_DIR", DEFAULT_PROJECT_DIR), + DEPLOY_SCRIPT=os.environ.get("DEPLOY_SCRIPT", DEFAULT_DEPLOY_SCRIPT), + ) + if config: + app.config.update(config) + if not app.config["WEBHOOK_SECRET"]: + raise RuntimeError("GITHUB_WEBHOOK_SECRET 未配置") + if app.config["BRANCH"] != DEFAULT_BRANCH: + raise RuntimeError("Webhook 只允许部署 master 分支") + + deployment_lock = threading.Lock() + deployment_process = None + + def signature_valid(body, signature): + if not signature or not signature.startswith("sha256="): + return False + expected = "sha256=" + hmac.new( + app.config["WEBHOOK_SECRET"].encode("utf-8"), body, sha256 + ).hexdigest() + return hmac.compare_digest(expected, signature) + + def start_deployment(): + nonlocal deployment_process + with deployment_lock: + if deployment_process is not None and deployment_process.poll() is None: + return False + env = os.environ.copy() + env["DEPLOY_BRANCH"] = DEFAULT_BRANCH + script = app.config["DEPLOY_SCRIPT"] + if not os.path.isabs(script): + script = os.path.join(app.config["PROJECT_DIR"], script) + deployment_process = subprocess.Popen( + [script], + cwd=app.config["PROJECT_DIR"], + env=env, + start_new_session=True, + ) + return True + + @app.get("/health") + def health(): + return jsonify(ok=True) + + @app.post("/github/webhook") + def github_webhook(): + body = request.get_data(cache=True) + if not signature_valid(body, request.headers.get("X-Hub-Signature-256", "")): + return jsonify(error="签名无效"), 401 + if request.headers.get("X-GitHub-Event") != "push": + return jsonify(error="只接受 push 事件"), 202 + + payload = request.get_json(silent=True) or {} + repository = (payload.get("repository") or {}).get("full_name") + if repository != app.config["REPOSITORY"]: + return jsonify(error="仓库不匹配"), 403 + if payload.get("ref") != f"refs/heads/{app.config['BRANCH']}" or payload.get("deleted"): + return jsonify(error="分支不匹配"), 202 + + if not start_deployment(): + return jsonify(status="already_deploying"), 202 + return jsonify(status="accepted"), 202 + + return app + + +if __name__ == "__main__": + application = create_app() + application.run( + host=os.environ.get("WEBHOOK_HOST", "127.0.0.1"), + port=int(os.environ.get("WEBHOOK_PORT", "9001")), + ) diff --git a/docs/API.md b/docs/API.md index 6c51f65..b2f3399 100644 --- a/docs/API.md +++ b/docs/API.md @@ -7,7 +7,7 @@ | 数据 | 来源 | 说明 | |------|------|------| -| 书架、书目 | 微信读书 `/shelf/sync`(实时,进程内缓存 10 分钟) | 字段含书名、作者、分类、封面、读完标记、最近阅读时间 | +| 书架、书目 | 微信读书 `/shelf/sync`(实时,进程内缓存 10 分钟) | 字段含书名、作者、分类、封面、读完标记、最近阅读时间和置顶标记 | | 阅读统计(周/月/年/总) | 微信读书 `/readdata/detail`(实时,缓存 5 分钟) | 时长趋势、阅读天数、读得最多、偏好分类/作者/出版社/时段/勋章/好友排行 | | 阅读进度 | 本地 `notebooks` / `book_progress` 缓存优先,缺失时并发调用 `/book/getprogress` 补齐(缓存 10 分钟) | 用于书架卡片与详情页进度 | | 笔记本与笔记 | 本地 `notebooks` 表 + 实时 `/book/bookmarklist`、`/review/list/mine` | 划线与想法正文实时获取 | @@ -17,6 +17,8 @@ MySQL 不可用时,阅读数据接口仍可降级运行(返回 `degraded: true`)。阅读目标必须持久化,其接口返回 503,不退回浏览器保存。 +当前书架页一次读取书架电子书,在前端按状态筛选后每页显示 48 本。排序选项为最近阅读(默认)、书名升序、书名降序和作者升序;置顶书籍始终排在前面。接口仍支持 `limit` / `offset` 参数供其他调用者分页,书架页使用全量结果以供书架、阅读目标和发现功能共享。 + ## 接口清单 | 方法 | 路径 | 参数 | 说明 | diff --git a/docs/code-design.md b/docs/code-design.md index 4b09ab5..2a79267 100644 --- a/docs/code-design.md +++ b/docs/code-design.md @@ -43,11 +43,11 @@ ## 部署流程 -GitHub Actions 的 `.github/workflows/deploy.yml` 只监听 `master` 分支。分支 push 后,Actions 使用仓库 Secrets 中的服务器地址、用户和 SSH 私钥登录服务器,进入 `/opt/weread-board/WeReadBoard` 后调用 `deploy.sh`。手动触发也固定部署 `master`,部署脚本会拒绝其他分支。 +GitHub Actions 的 `.github/workflows/deploy.yml` 只做验证,不登录服务器。`master` push 后由 GitHub Webhook 向 `https://laohang.space/github/webhook` 发送 JSON,服务器上的 `deploy_webhook.py` 校验 HMAC-SHA256、仓库和 `master` 分支,再在固定目录调用 `deploy.sh`。Webhook 不接受请求体中的命令,部署脚本路径由服务器环境变量固定。 `deploy.sh` 固定拉取 `master`,收到其他部署分支参数时直接退出,再检查项目 `.venv`。如果 `.venv/bin/python` 不存在,脚本使用服务器上的 `python3` 或 `python` 创建虚拟环境;随后用 `.venv/bin/python -m pip install -r requirements.txt` 安装依赖,按文件名顺序执行 `migrations/*.sql`,最后重启 `weread.service`。 -例:用户将已验收代码合并并推送到 `master`,服务器原来没有 `.venv`。部署脚本会在 `/opt/weread-board/WeReadBoard/.venv` 创建环境,安装 Flask 和 PyMySQL,确认阅读目标表存在,再让 systemd 重新用 `.venv/bin/python app.py` 启动服务。仅推送 `dev` 不会触发部署。 +例:用户将已验收代码合并并推送到 `master`,GitHub 发送签名请求;服务器确认仓库为 `KingHWJ/WeReadBoard` 且 ref 为 `refs/heads/master` 后,在 `/opt/weread-board/WeReadBoard/.venv` 创建环境,安装 Flask 和 PyMySQL,确认阅读目标表存在,再让 systemd 重新用 `.venv/bin/python app.py` 启动服务。仅推送 `dev` 不会触发部署,错误签名也不会执行脚本。 ## 正式看板登录认证 @@ -81,6 +81,14 @@ GitHub Actions 的 `.github/workflows/deploy.yml` 只监听 `master` 分支。 目标介绍区在宽屏上把说明与统计并排,窄屏上改为上下排列;目标卡按可用宽度自动排列,每张卡约 280 px 起步,并缩小封面预览和上下间距。例如,一个包含 6 本书、已读完 2 本的月度目标仍展示原有完成数和进度,只减少卡片占位。页面渲染、筛选、目标计算和数据流保持由现有 JavaScript 实现,CSS 不改变业务口径。 +## 正式看板书架排序与分页 + +正式看板从 `/api/reading/shelf` 读取电子书列表。前端按当前书架状态筛选后排序,每页展示 48 本;用户切换状态或排序时回到第 1 页,点击底部页码时只替换当前书籍网格。排序框右对齐显示在页码列表上方。 + +排序默认按最近阅读时间倒序;还可按书名升序、书名降序或作者升序。四种方式都先将 `is_top` 置顶书籍排在前面。接口把微信读书的 `isTop` 映射为 `is_top`,前端排序只使用筛选结果的副本,避免改变其他页面共享的书架顺序。 + +例:筛选“全部电子书”共 101 本,选择“书名升序”并打开第 2 页。页面只显示排序后第 49 至 96 本,在网格下方显示第 1、2、3 页;切换到“最近阅读”后回到第 1 页,并按最近阅读日期倒序显示前 48 本,置顶书籍仍排在该页最前面。 + ## 正式看板的阅读目标持久化 正式看板使用 `static/js/app.js`、`static/js/weapi.js` 和 `reading_api.py`。本节覆盖正式页面;前文的 Demo 存储方式不适用于正式目标。 diff --git a/docs/deploy-master.md b/docs/deploy-master.md index 5c9ba77..3f8a327 100644 --- a/docs/deploy-master.md +++ b/docs/deploy-master.md @@ -1,28 +1,37 @@ -# master 自动部署 +# master Webhook 自动部署 -GitHub Actions workflow 位于 `.github/workflows/deploy.yml`。每次 `master` 分支 push 后,Actions 会通过 SSH 登录服务器,在部署目录执行根目录 `deploy.sh`,拉取最新 `master`、安装依赖、执行全部数据库迁移并重启服务。手动触发时也固定部署 `master`,不能选择其他分支发布。 +部署链路是:GitHub `master` push → GitHub Webhook → 服务器签名校验 → 服务器本地执行 `deploy.sh`。GitHub Actions 文件 `.github/workflows/deploy.yml` 现在只负责验证,不再通过 SSH 登录服务器。 -## GitHub 配置 +## GitHub Webhook 配置 -已配置的仓库 Secrets: +进入仓库 `Settings -> Webhooks -> Add webhook`,配置 Payload URL `https://laohang.space/github/webhook`、Content type `application/json`、Secret(与服务器 `GITHUB_WEBHOOK_SECRET` 完全一致),事件选择 `Just the push event` 并勾选 `Active`。只允许 `KingHWJ/WeReadBoard` 的 `master` push 触发部署。 -- `SERVER_HOST` -- `SERVER_PORT` -- `SERVER_SSH_KEY` -- `SERVER_USER` +Webhook 服务配置文件 `/etc/weread-webhook.env`: -可选再配置两个仓库 Variables: +```ini +GITHUB_WEBHOOK_SECRET=服务器生成的随机密钥 +GITHUB_REPOSITORY=KingHWJ/WeReadBoard +DEPLOY_PROJECT_DIR=/opt/weread-board/WeReadBoard +DEPLOY_SCRIPT=/opt/weread-board/WeReadBoard/deploy.sh +WEBHOOK_HOST=127.0.0.1 +WEBHOOK_PORT=9001 +``` -- `DEPLOY_PATH`:服务器上的项目目录,例如 `/opt/WeReadBoard` -- `SERVICE_NAME`:systemd 服务名,例如 `weread.service` +复制 `deploy/weread-webhook.service` 到 `/etc/systemd/system/`,并执行: -未配置 Variables 时,workflow 默认使用 `/opt/weread-board/WeReadBoard` 和 `weread.service`。 +```bash +python3 -c 'import secrets; print(secrets.token_urlsafe(48))' +sudo chmod 600 /etc/weread-webhook.env +sudo systemctl daemon-reload +sudo systemctl enable --now weread-webhook.service +sudo journalctl -u weread-webhook.service -f +``` ## 服务器要求 服务器部署目录需要已经是本仓库的 Git checkout,并且能访问 `origin/master`。运行服务的环境变量仍按现有应用配置,例如 `WEREAD_API_KEY`、`WEREAD_DB_HOST`、`WEREAD_DB_PORT`、`WEREAD_DB_USER`、`WEREAD_DB_PASSWORD` 和 `WEREAD_DB_NAME`。 -如果这些变量放在项目目录的 `.env` 文件中,部署脚本会先加载 `.env`,再执行数据库迁移。SSH 用户需要能执行 `systemctl restart <服务名>`;非 root 用户需要配置免密 `sudo systemctl restart/status <服务名>`。 +如果这些变量放在项目目录的 `.env` 文件中,部署脚本会先加载 `.env`,再执行数据库迁移。Webhook 运行用户需要能执行 `systemctl restart <服务名>`;非 root 用户需要配置免密 `sudo systemctl restart/status <服务名>`。 `deploy.sh` 会执行: @@ -72,3 +81,9 @@ WantedBy=multi-user.target 登录认证还需配置 `WEREAD_SESSION_SECRET` 和首次使用的 `WEREAD_AUTH_SETUP_TOKEN`。在服务器分别运行两次 `python3 -c 'import secrets; print(secrets.token_urlsafe(48))'` 生成不同的值。迁移自动创建 `weread_auth` 表;首次访问 `/login` 时输入初始化口令、管理员账号和至少 12 位密码。初始化成功后,从 systemd 配置中删除 `WEREAD_AUTH_SETUP_TOKEN` 并重启服务。数据库中的密码只保存哈希。 `WEREAD_SESSION_COOKIE_SECURE=1` 要求浏览器通过 HTTPS 访问。仅本机 HTTP 验收可临时设为 `0`,不要在公网 HTTP 下传输登录密码。 + +## Nginx HTTPS 转发 + +将 `deploy/nginx-github-webhook.conf` 的 `location` 放入 `laohang.space` 的 HTTPS server 块,然后执行 `sudo nginx -t && sudo systemctl reload nginx`。Webhook 服务只监听 `127.0.0.1:9001`,不要对公网开放 9001 端口。 + +`.github/workflows/deploy.yml` 只执行 Python、前端和部署脚本检查。Webhook 模式不再需要 `SERVER_HOST`、`SERVER_PORT`、`SERVER_USER`、`SERVER_SSH_KEY` 这些 Actions Secrets。 diff --git a/docs/technical-guide.md b/docs/technical-guide.md index 9807551..c5a5d94 100644 --- a/docs/technical-guide.md +++ b/docs/technical-guide.md @@ -109,7 +109,7 @@ python3 -c 'import secrets; print(secrets.token_urlsafe(48))' 应用本身未实现登录尝试限流。公网部署还应在 Nginx 或其他可信入口对 `POST /auth/login` 配置频率限制,不能信任客户端自行传入的转发 IP。 -## GitHub 自动部署 +## GitHub Webhook 自动部署 服务器部署使用根目录的 `deploy.sh`。脚本会先拉取指定分支,再检测 Python 环境;若项目 `.venv` 不存在,会自动用系统 `python3` 或 `python` 创建 `.venv`,随后安装 `requirements.txt`、按顺序执行 `migrations/*.sql`,并重启 systemd 服务。 @@ -121,21 +121,9 @@ SERVICE_NAME=weread.service VENV_DIR=/opt/weread-board/WeReadBoard/.venv ``` -GitHub Actions 文件位于 `.github/workflows/deploy.yml`,只在 `master` 分支 push 后触发,也支持手动触发;手动触发同样固定部署 `master`。仓库需要在 `Settings -> Secrets and variables -> Actions -> Secrets` 配置: +GitHub Actions 文件位于 `.github/workflows/deploy.yml`,现在只负责测试和部署脚本检查。生产部署由 GitHub Webhook 触发:仓库 `Settings -> Webhooks` 的 Payload URL 配置为 `https://laohang.space/github/webhook`,Secret 与服务器 `/etc/weread-webhook.env` 中的 `GITHUB_WEBHOOK_SECRET` 一致,事件只选择 push。 -| Secret | 说明 | -|---|---| -| `SERVER_HOST` | 服务器 IP | -| `SERVER_PORT` | SSH 端口,未配置时按 22 处理 | -| `SERVER_USER` | SSH 登录用户 | -| `SERVER_SSH_KEY` | GitHub Actions 登录服务器用的私钥 | - -可选配置放在 `Settings -> Secrets and variables -> Actions -> Variables`: - -| Variable | 说明 | -|---|---| -| `DEPLOY_PATH` | 可选,默认 `/opt/weread-board/WeReadBoard` | -| `SERVICE_NAME` | 可选,默认 `weread.service` | +服务器 Webhook 服务使用 `/etc/weread-webhook.env`,至少配置 `GITHUB_WEBHOOK_SECRET`、`GITHUB_REPOSITORY=KingHWJ/WeReadBoard`、`DEPLOY_PROJECT_DIR` 和 `DEPLOY_SCRIPT`。代码内固定只允许 `master`,不接受环境变量覆盖。Nginx 将 HTTPS 请求转发至本机 `127.0.0.1:9001`;Webhook 会校验 HMAC-SHA256、仓库和分支,并在已有部署运行时拒绝重复启动。 如果在服务器手动部署,可执行: @@ -171,8 +159,10 @@ node --check static/js/weapi.js 本次样式收紧覆盖正式看板的概览、书架和阅读目标页。顶栏、标题区、筛选区、统计卡、书架网格、右侧目标摘要、目标介绍区和目标卡片均使用覆盖规则压缩占位;书架在窄屏固定两列,避免横向溢出。本地真实数据验收覆盖 1280 px 桌面和 390 px 手机宽度:桌面顶栏 58 px,统计卡从约 200 px 开始,书架一行 7 本,目标页介绍区约 67 px 高,目标卡约 317 px 宽;手机顶栏 56 px,书架两列,目标卡约 298 px 宽,概览、书架和目标页均未出现横向溢出。 -## master 自动部署 +当前书架电子书列表每页显示 48 本,页码导航位于列表下方;翻页会替换当前页内容。右侧排序框提供最近阅读、书名升序、书名降序和作者升序,置顶书籍始终优先;切换书架状态或排序会回到第 1 页。此功能使用已加载的书架数据,不需要数据库迁移。 -`master` 分支 push 后由 `.github/workflows/deploy.yml` 触发部署。Actions 通过仓库 Secrets 登录服务器,在 `DEPLOY_PATH` 指定的目录先拉取 `origin/master`,再执行根目录 `deploy.sh`,完成加载可选 `.env`、安装 Python 依赖、执行全部数据库迁移和重启 systemd 服务。`dev` 和其他分支不会触发部署,部署脚本也会拒绝非 `master` 分支。详细配置见 [master 自动部署](deploy-master.md)。 +## master Webhook 自动部署 + +`master` 分支 push 后由 GitHub Webhook 通知服务器。服务器校验签名后,在固定项目目录执行根目录 `deploy.sh`,完成拉取 `origin/master`、加载可选 `.env`、安装 Python 依赖、执行全部数据库迁移和重启 systemd 服务。`dev` 和其他分支不会部署,部署脚本也会拒绝非 `master` 分支。详细配置见 [master Webhook 自动部署](deploy-master.md)。 本次数据库验收已在 8080 正式看板完成:页面新建目标后刷新仍可读取,编辑后再次刷新仍可读取,并已清理临时验收目标。读取目标列表未完成时,新建和编辑入口只显示目标页的加载或错误状态,不会打开保存表单。 diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index efbe5af..b800cba 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -73,7 +73,18 @@ sudo journalctl -u weread.service -n 100 --no-pager ### 检查部署分支 -自动部署只允许 `master`。如果 Actions 没有启动,先确认提交是否已经合并并推送到 `master`;仅推送 `dev` 不会部署。服务器部署日志中的拉取目标也必须是 `origin/master`,出现其他分支时应停止发布并检查工作流与 `deploy.sh`。 +自动部署只允许 `master`。先检查 GitHub Webhook 的 Recent Deliveries、服务器 `weread-webhook.service` 日志和 Nginx HTTPS 转发;仅推送 `dev` 不会部署。服务器部署日志中的拉取目标必须是 `origin/master`,出现其他分支时应停止发布并检查 Webhook 环境变量与 `deploy.sh`。 + +Webhook 排查命令: + +```bash +sudo systemctl status weread-webhook.service --no-pager +sudo journalctl -u weread-webhook.service -n 100 --no-pager +sudo nginx -t +curl -fsS https://laohang.space/health +``` + +GitHub 显示签名错误时,确认 Webhook Secret 与 `/etc/weread-webhook.env` 完全一致;显示 202 但没有部署时,确认仓库全名、`refs/heads/master` 和部署锁状态。不要通过请求体传入 Shell 命令。 ## 2026-09-14:接口调整后页面请求失败 diff --git a/legacy/classic/static/css/style.css b/legacy/classic/static/css/style.css index 92e4e86..58d9767 100644 --- a/legacy/classic/static/css/style.css +++ b/legacy/classic/static/css/style.css @@ -146,7 +146,7 @@ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", - "Hiragino Sans GB", "Microsoft YaHei", sans-serif; + "Hiragino Sans GB", "Microsoft YaHei", sans-serif; background: var(--bg-primary); color: var(--text-primary); line-height: 1.6; @@ -154,14 +154,34 @@ body { } /* 所有主题相关元素平滑过渡 */ -.navbar, .nav-link, .stat-card, .chart-card, .top-book-card, .book-card, -.book-cover-wrapper, .notebook-card, .note-stat, .discover-card, .discover-tab, -.modal-content, .modal-overlay, .footer, .db-alert-banner, .page-btn, -.btn-primary, .btn-secondary, .search-input, .shelf-search, .highlight-item, -.review-item, .detail-progress-info, .top-book-cover-wrapper, -.theme-toggle, .loading-spinner { +.navbar, +.nav-link, +.stat-card, +.chart-card, +.top-book-card, +.book-card, +.book-cover-wrapper, +.notebook-card, +.note-stat, +.discover-card, +.discover-tab, +.modal-content, +.modal-overlay, +.footer, +.db-alert-banner, +.page-btn, +.btn-primary, +.btn-secondary, +.search-input, +.shelf-search, +.highlight-item, +.review-item, +.detail-progress-info, +.top-book-cover-wrapper, +.theme-toggle, +.loading-spinner { transition: background-color 0.3s ease, border-color 0.3s ease, - color 0.3s ease, box-shadow 0.3s ease, transform 0.2s ease; + color 0.3s ease, box-shadow 0.3s ease, transform 0.2s ease; } .container { @@ -264,12 +284,25 @@ body { transform: scale(1.08); } -.theme-toggle .icon-sun { display: none; } -.theme-toggle .icon-moon { display: block; } +.theme-toggle .icon-sun { + display: none; +} -[data-theme="dark"] .theme-toggle .icon-sun { display: block; } -[data-theme="dark"] .theme-toggle .icon-moon { display: none; } -[data-theme="dark"] .theme-toggle { color: var(--toggle-icon-sun); } +.theme-toggle .icon-moon { + display: block; +} + +[data-theme="dark"] .theme-toggle .icon-sun { + display: block; +} + +[data-theme="dark"] .theme-toggle .icon-moon { + display: none; +} + +[data-theme="dark"] .theme-toggle { + color: var(--toggle-icon-sun); +} /* ===== 顶部常驻搜索框 ===== */ .global-search-bar { @@ -382,7 +415,7 @@ body { } .shelf-cat-parent.active .shelf-cat-count { - background: rgba(255,255,255,0.25); + background: rgba(255, 255, 255, 0.25); color: #fff; } @@ -403,8 +436,15 @@ body { } @keyframes fadeIn { - from { opacity: 0; transform: translateY(-4px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(-4px); + } + + to { + opacity: 1; + transform: translateY(0); + } } .shelf-cat-sub { @@ -443,7 +483,7 @@ body { } .shelf-cat-sub.active .shelf-cat-count-sm { - background: rgba(255,255,255,0.25); + background: rgba(255, 255, 255, 0.25); color: #fff; } @@ -617,8 +657,15 @@ body { } @keyframes fadeInTab { - from { opacity: 0; transform: translateY(8px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(8px); + } + + to { + opacity: 1; + transform: translateY(0); + } } /* ===== 读书排行(竖向列表) ===== */ @@ -1090,7 +1137,7 @@ body { color: var(--alert-text); font-size: 14px; font-weight: 500; - box-shadow: 0 2px 8px rgba(0,0,0,0.1); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); animation: slideDown 0.3s ease-out; } @@ -1111,8 +1158,15 @@ body { } @keyframes slideDown { - from { transform: translateY(-100%); opacity: 0; } - to { transform: translateY(0); opacity: 1; } + from { + transform: translateY(-100%); + opacity: 0; + } + + to { + transform: translateY(0); + opacity: 1; + } } /* ===== 加载动画 ===== */ @@ -1133,7 +1187,9 @@ body { } @keyframes spin { - to { transform: rotate(360deg); } + to { + transform: rotate(360deg); + } } /* ===== 推荐发现区 ===== */ @@ -1756,7 +1812,7 @@ body { } .thought-filter-btn.active .filter-count { - background: rgba(255,255,255,0.25); + background: rgba(255, 255, 255, 0.25); color: #fff; } @@ -1964,78 +2020,100 @@ body { .nav-links { display: none; } + .hero-title { font-size: 24px; } + .hero-stats { gap: 24px; } + .hero-stat-value { font-size: 32px; } + .charts-grid { grid-template-columns: 1fr; } + .chart-card.large { grid-column: auto; } + .book-grid { grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 12px; } + .top-books-list { gap: 8px; } + .top-book-row { padding: 8px 12px; gap: 10px; } + .top-book-mini-cover { width: 36px; height: 48px; } + .top-book-rank-num { width: 28px; height: 28px; font-size: 13px; } + .nav-update { display: none; } + .global-search-wrapper { max-width: 100%; } + .thoughts-toolbar { flex-direction: column; align-items: stretch; } + .thoughts-controls { flex-direction: column; gap: 8px; } + .thoughts-search-input { width: 100%; } + .thoughts-sort-select { width: 100%; } + .shelf-search { width: 140px; } + .shelf-categories { gap: 6px; } + .shelf-cat-parent { font-size: 12px; padding: 4px 10px; } + .shelf-cat-sub { font-size: 11px; padding: 3px 9px; } + .shelf-cat-subs { padding-left: 10px; } + .back-to-top { bottom: 20px; right: 20px; @@ -2043,14 +2121,17 @@ body { height: 40px; font-size: 18px; } + .thoughts-page-header { top: 0; } + .thoughts-book-cover { width: 48px; height: 64px; } + .thoughts-book-title { font-size: 16px; } -} +} \ No newline at end of file diff --git a/legacy/classic/static/js/app.js b/legacy/classic/static/js/app.js index daf6e68..50a57d0 100644 --- a/legacy/classic/static/js/app.js +++ b/legacy/classic/static/js/app.js @@ -476,11 +476,13 @@ async function loadYearlyTrend() { barWidth: "50%", itemStyle: { borderRadius: [8, 8, 0, 0], - color: { type: "linear", x: 0, y: 0, x2: 0, y2: 1, + color: { + type: "linear", x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: c.accentLight }, { offset: 1, color: c.accent }, - ] }, + ] + }, }, label: { show: true, position: "top", color: c.label, fontSize: 12, formatter: "{c}h" }, }], @@ -538,11 +540,13 @@ async function loadTimeDistribution() { lineStyle: { width: 2, color: c.accent }, itemStyle: { color: c.accent }, areaStyle: { - color: { type: "linear", x: 0, y: 0, x2: 0, y2: 1, + color: { + type: "linear", x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: c.accent + "4D" }, { offset: 1, color: c.accent + "05" }, - ] }, + ] + }, }, }], }); @@ -650,11 +654,13 @@ async function loadAuthors() { barWidth: "50%", itemStyle: { borderRadius: [0, 6, 6, 0], - color: { type: "linear", x: 0, y: 0, x2: 1, y2: 0, + color: { + type: "linear", x: 0, y: 0, x2: 1, y2: 0, colorStops: [ { offset: 0, color: c.accentLight }, { offset: 1, color: c.accent }, - ] }, + ] + }, }, label: { show: true, position: "right", color: c.label, fontSize: 12, formatter: "{c}本" }, }], diff --git a/legacy/classic/templates/index.html b/legacy/classic/templates/index.html index 803e317..ceb5c79 100644 --- a/legacy/classic/templates/index.html +++ b/legacy/classic/templates/index.html @@ -1,5 +1,6 @@ +
@@ -8,7 +9,7 @@ + @@ -208,4 +216,5 @@ - + +