返回键
- 根因是 Framework7 的页面过渡由动画事件驱动:push 时把 allowPageChange
置 false,等动画报告结束再恢复。这个报告不来,路由就永久卡住,之后每次
导航都被静默丢弃,back() 还会把上一页重建一份而不是弹出。
实测对照:navigate({animate:false}) 前后状态完全正确,带动画则必卡。
因此关掉页面过渡动画——导航同步完成,处处正确。动效改由内容承担
(卡片入场、hero 揭示、顶部进度条),这个取舍里正确性优先。
- Screen 的返回改为显式 handler,先清掉残留过渡状态再 back(),
不依赖路由自己的闸门。注意只清视图上的 router-transition 类:
页面自身的 page-previous 是 F7 判断「回到哪一页」的依据,
一并清掉会导致重建出一个重复的页面(中途踩过这个坑)。
同步页
- .btn 系列样式原本只定义在 pages/Pages.css,而那个文件只被一个没有路由的
遗留页面引用,所以真实页面上按钮全都退化成 Framework7 的默认样式——
就是你看到的三条链接。样式移进每个界面都会加载的 Screen.css。
- 主次分明:一个填充主按钮 + 两个带副标题的次按钮;补上「同步会取哪些数据」
说明,页面不再是一大片空白。
- 进度条显示当前阶段(每日数据 2026-08-01 / 运动详情 12/174 / 身体成分…),
原来只有「0 / 730 天」,几分钟里完全看不出在做什么。
后端
- MariaDB 连接池:_mariadb_release 用的是阻塞 put(),而队列 maxsize=10,
_mariadb_acquire 在池空时又会新建连接。并发超过 10 之后,归还的线程会
永久停在 put() 上,请求就此挂死。改为 put_nowait,多出来的连接直接关闭。
- 进程重启会带走同步线程却留下 status=syncing 的行,界面上是一个永远不动
的进度条,还拒绝开始新同步。启动时清理。
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
83 lines
2.9 KiB
Python
83 lines
2.9 KiB
Python
"""
|
|
Flask application factory for Garmin Health Lab.
|
|
|
|
Run directly (`python app.py`) for development, or serve with Gunicorn:
|
|
gunicorn wsgi:app -b 0.0.0.0:5000
|
|
"""
|
|
import os
|
|
|
|
from flask import Flask, jsonify, send_from_directory
|
|
from flask_cors import CORS
|
|
|
|
import db
|
|
from config import CORS_ORIGINS, PORT, STATIC_DIR
|
|
from routes import auth, garmin, health, analysis, settings
|
|
from services import scheduler
|
|
from services import garmin as garmin_svc
|
|
|
|
|
|
def create_app():
|
|
app = Flask(__name__)
|
|
CORS(app, resources={r"/api/*": {"origins": CORS_ORIGINS}}, supports_credentials=True)
|
|
|
|
# Create tables once at startup (idempotent).
|
|
db.init_db()
|
|
|
|
# A sync that was running when the previous process stopped left its
|
|
# status behind; clear it before anything reads it.
|
|
garmin_svc.reset_stale_syncs()
|
|
|
|
# Keeps the database current without the user pressing anything. Safe to
|
|
# call in every worker: the job is claimed through the database, so only
|
|
# one of them actually runs a given tick.
|
|
scheduler.start()
|
|
|
|
# In production the built React app is served by this same process, so the
|
|
# deployment is a single port with no reverse proxy to configure. In
|
|
# development STATIC_DIR does not exist and the CRA dev server serves the
|
|
# UI instead — hence the guard rather than an unconditional route.
|
|
has_ui = bool(STATIC_DIR) and os.path.isfile(os.path.join(STATIC_DIR, "index.html"))
|
|
|
|
@app.route("/")
|
|
def index():
|
|
if has_ui:
|
|
return send_from_directory(STATIC_DIR, "index.html")
|
|
return jsonify({"name": "Garmin Health Lab API", "version": "1.0.0"})
|
|
|
|
@app.route("/api/health/status")
|
|
def health_status():
|
|
return jsonify({"status": "ok", "db": db.DB_TYPE})
|
|
|
|
app.register_blueprint(auth.bp, url_prefix="/api/auth")
|
|
app.register_blueprint(garmin.bp, url_prefix="/api/garmin")
|
|
app.register_blueprint(health.bp, url_prefix="/api/health")
|
|
app.register_blueprint(analysis.bp, url_prefix="/api/analysis")
|
|
app.register_blueprint(settings.bp, url_prefix="/api/settings")
|
|
|
|
@app.errorhandler(404)
|
|
def not_found(_e):
|
|
# API paths always answer in JSON. Everything else falls through to the
|
|
# SPA so client-side routes (/settings, /recommendations, ...) survive a
|
|
# page reload instead of 404-ing.
|
|
from flask import request
|
|
|
|
if has_ui and not request.path.startswith("/api/"):
|
|
asset = request.path.lstrip("/")
|
|
if asset and os.path.isfile(os.path.join(STATIC_DIR, asset)):
|
|
return send_from_directory(STATIC_DIR, asset)
|
|
return send_from_directory(STATIC_DIR, "index.html")
|
|
return jsonify({"error": "not found"}), 404
|
|
|
|
@app.errorhandler(500)
|
|
def server_error(_e):
|
|
return jsonify({"error": "internal error"}), 500
|
|
|
|
return app
|
|
|
|
|
|
app = create_app()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app.run(host="0.0.0.0", port=PORT, debug=False)
|