refactor(sync): 手动同步与自动同步彻底分开
「历史范围」原本放在设置页,却只对同步页的一个按钮起作用;而同步页最 显眼的主按钮「同步最新数据」写死 2 天,根本不看这个设置。选了「全部 历史」再点主按钮,表现就是应用无视你 —— 这正是反复出现的「只同步下来 两天」。 现在两条链路各管各的: * 自动同步:只在设置页配置(开关 + 频率),窗口固定 SYNC_DAYS,不再 读 history_days。措辞也改成「拉取最近几天」,不再暗示会补历史。 * 手动同步:范围就在同步页当场选,紧挨着用它的按钮,并标出每个范围的 实际代价(自上次同步 / 7 天 / … / 全部历史约 730 天、20-40 分钟)。 两个按钮合成一个「开始同步」,写死 2 天的那个删掉。 history_days 保留为「上次手动选的范围」,只有同步页读它;默认值改成 -1(自上次同步),对日常使用是正确的起点。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ from flask import request, g, jsonify
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from config import JWT_SECRET, JWT_EXPIRY_DAYS
|
from config import JWT_SECRET, JWT_EXPIRY_DAYS
|
||||||
|
from db import query_one
|
||||||
|
|
||||||
|
|
||||||
def sign_token(user_id: str) -> str:
|
def sign_token(user_id: str) -> str:
|
||||||
@@ -41,6 +42,10 @@ def require_auth(f):
|
|||||||
except jwt.InvalidTokenError:
|
except jwt.InvalidTokenError:
|
||||||
return jsonify({"error": "invalid token"}), 401
|
return jsonify({"error": "invalid token"}), 401
|
||||||
g.user_id = data["user_id"]
|
g.user_id = data["user_id"]
|
||||||
|
# Ensure the user record still exists in the database (it may have been
|
||||||
|
# deleted by a database reset). If not, ask the user to re-login.
|
||||||
|
if not query_one("SELECT 1 FROM users WHERE id = ?", [g.user_id]):
|
||||||
|
return jsonify({"error": "user not found — please re-login"}), 401
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|||||||
@@ -128,10 +128,11 @@ def sync_all_accounts(days=None, respect_schedule=False):
|
|||||||
that have auto-sync off or that were synced recently enough. A direct call
|
that have auto-sync off or that were synced recently enough. A direct call
|
||||||
(a manual "sync everything") leaves it False and syncs unconditionally.
|
(a manual "sync everything") leaves it False and syncs unconditionally.
|
||||||
|
|
||||||
When `days` is not provided, each account's saved `history_days` from
|
The window is `SYNC_DAYS` unless the caller overrides it. Auto-sync is
|
||||||
`user_settings` is used (the user's 历史范围 picker), falling back to
|
deliberately blind to the user's manual sync range: reading it here is what
|
||||||
`SYNC_DAYS`. The scheduled loop always passes `days` explicitly — 历史范围
|
let a half-hourly tick re-pull 730 days and hold the account in a 429 loop,
|
||||||
is the window for the manual 全量同步, not for a half-hourly tick.
|
and it also made one setting mean two different things in two places.
|
||||||
|
Anything the user chooses by hand goes through `/garmin/sync` instead.
|
||||||
"""
|
"""
|
||||||
rows = query_all("SELECT user_id FROM garmin_tokens")
|
rows = query_all("SELECT user_id FROM garmin_tokens")
|
||||||
results = []
|
results = []
|
||||||
@@ -148,22 +149,7 @@ def sync_all_accounts(days=None, respect_schedule=False):
|
|||||||
results.append({"user": uid, "status": "skipped",
|
results.append({"user": uid, "status": "skipped",
|
||||||
"reason": "not due"})
|
"reason": "not due"})
|
||||||
continue
|
continue
|
||||||
# Resolve the sync window: prefer the user's saved history_days,
|
d = SYNC_DAYS if days is None else days
|
||||||
# then the caller override, then the global default.
|
|
||||||
if days is None:
|
|
||||||
# query_one returns a dict, so `s[0]` raised KeyError — caught
|
|
||||||
# by the per-account handler below, which meant every single
|
|
||||||
# scheduled tick failed for every account and nothing was ever
|
|
||||||
# synced automatically.
|
|
||||||
s = query_one(
|
|
||||||
"SELECT history_days FROM user_settings WHERE user_id = ?", (uid,)
|
|
||||||
)
|
|
||||||
user_days = s.get("history_days") if s else None
|
|
||||||
if user_days == 0:
|
|
||||||
user_days = 730 # 全部历史 → 最大范围
|
|
||||||
d = user_days if user_days is not None else SYNC_DAYS
|
|
||||||
else:
|
|
||||||
d = days
|
|
||||||
# Never poke Garmin while it is rate-limiting us — that is exactly
|
# Never poke Garmin while it is rate-limiting us — that is exactly
|
||||||
# what keeps the limit alive. Respect the persisted cooldown and sit
|
# what keeps the limit alive. Respect the persisted cooldown and sit
|
||||||
# this tick out.
|
# this tick out.
|
||||||
@@ -187,12 +173,7 @@ def _loop():
|
|||||||
try:
|
try:
|
||||||
if claim(interval=TICK_SECONDS):
|
if claim(interval=TICK_SECONDS):
|
||||||
try:
|
try:
|
||||||
# Explicitly the recent window, never the user's 历史范围:
|
sync_all_accounts(respect_schedule=True)
|
||||||
# that setting describes the manual 全量同步. Letting a
|
|
||||||
# half-hourly tick re-pull "全部历史" meant 730 days x ~7
|
|
||||||
# Garmin calls every 30 minutes, which is precisely what
|
|
||||||
# kept the account in a 429 loop.
|
|
||||||
sync_all_accounts(days=SYNC_DAYS, respect_schedule=True)
|
|
||||||
finally:
|
finally:
|
||||||
release()
|
release()
|
||||||
except Exception as e: # noqa: BLE001 - the loop must outlive any single failure
|
except Exception as e: # noqa: BLE001 - the loop must outlive any single failure
|
||||||
|
|||||||
@@ -29,8 +29,14 @@ DEFAULTS = {
|
|||||||
"units": "metric",
|
"units": "metric",
|
||||||
"auto_sync": 1,
|
"auto_sync": 1,
|
||||||
"auto_sync_minutes": 60,
|
"auto_sync_minutes": 60,
|
||||||
# How far back a full sync reaches. 0 means "everything Garmin has".
|
# The range last chosen on the 数据同步 page, remembered so the picker
|
||||||
"history_days": 365,
|
# opens where the user left it. -1 is 自上次同步 and 0 is 全部历史.
|
||||||
|
#
|
||||||
|
# This is a *manual* sync setting and nothing else reads it — auto-sync
|
||||||
|
# has its own fixed window (scheduler.SYNC_DAYS). It used to live in 设置
|
||||||
|
# as 历史范围 while only taking effect on another page, which is exactly
|
||||||
|
# how "全部历史" ended up looking like it did nothing.
|
||||||
|
"history_days": -1,
|
||||||
}
|
}
|
||||||
|
|
||||||
SEXES = ("male", "female", "other")
|
SEXES = ("male", "female", "other")
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"files": {
|
"files": {
|
||||||
"main.css": "/static/css/main.a0945a9e.css",
|
"main.css": "/static/css/main.a0945a9e.css",
|
||||||
"main.js": "/static/js/main.3efa2734.js",
|
"main.js": "/static/js/main.56f8eb9b.js",
|
||||||
"static/media/Framework7Icons-Regular.ttf": "/static/media/Framework7Icons-Regular.4b8a7d10ca32f3125696.ttf",
|
"static/media/Framework7Icons-Regular.ttf": "/static/media/Framework7Icons-Regular.4b8a7d10ca32f3125696.ttf",
|
||||||
"static/media/Framework7Icons-Regular.woff": "/static/media/Framework7Icons-Regular.286bd8fcebb566a45853.woff",
|
"static/media/Framework7Icons-Regular.woff": "/static/media/Framework7Icons-Regular.286bd8fcebb566a45853.woff",
|
||||||
"static/media/Framework7Icons-Regular.woff2": "/static/media/Framework7Icons-Regular.852655880420bcb785bd.woff2",
|
"static/media/Framework7Icons-Regular.woff2": "/static/media/Framework7Icons-Regular.852655880420bcb785bd.woff2",
|
||||||
"index.html": "/index.html",
|
"index.html": "/index.html",
|
||||||
"main.a0945a9e.css.map": "/static/css/main.a0945a9e.css.map",
|
"main.a0945a9e.css.map": "/static/css/main.a0945a9e.css.map",
|
||||||
"main.3efa2734.js.map": "/static/js/main.3efa2734.js.map"
|
"main.56f8eb9b.js.map": "/static/js/main.56f8eb9b.js.map"
|
||||||
},
|
},
|
||||||
"entrypoints": [
|
"entrypoints": [
|
||||||
"static/css/main.a0945a9e.css",
|
"static/css/main.a0945a9e.css",
|
||||||
"static/js/main.3efa2734.js"
|
"static/js/main.56f8eb9b.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="佳明健康数据分析平台"><title>Garmin Health Lab - 佳明健康数据分析</title><script defer="defer" src="/static/js/main.3efa2734.js"></script><link href="/static/css/main.a0945a9e.css" rel="stylesheet"></head><body><div id="root"></div></body></html>
|
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="佳明健康数据分析平台"><title>Garmin Health Lab - 佳明健康数据分析</title><script defer="defer" src="/static/js/main.56f8eb9b.js"></script><link href="/static/css/main.a0945a9e.css" rel="stylesheet"></head><body><div id="root"></div></body></html>
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -187,12 +187,11 @@ class TestEndpoints:
|
|||||||
|
|
||||||
|
|
||||||
class TestSyncWindowResolution:
|
class TestSyncWindowResolution:
|
||||||
"""Which window each account is synced over.
|
"""Auto-sync must be blind to the manual sync range.
|
||||||
|
|
||||||
`query_one` returns a dict, so reading the saved 历史范围 as `s[0]` raised
|
They used to share `history_days`: one setting on the 设置 page that only
|
||||||
KeyError — swallowed by the per-account handler, which meant every
|
took effect on the 数据同步 page, and that a half-hourly tick also used to
|
||||||
scheduled tick failed for every account that had ever opened 设置, and
|
re-pull 730 days with. Separating them is the point.
|
||||||
nothing was synced automatically at all.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _account(self, user, monkeypatch):
|
def _account(self, user, monkeypatch):
|
||||||
@@ -208,34 +207,23 @@ class TestSyncWindowResolution:
|
|||||||
monkeypatch.setattr(garmin_svc, "sync_data", record)
|
monkeypatch.setattr(garmin_svc, "sync_data", record)
|
||||||
return seen
|
return seen
|
||||||
|
|
||||||
def test_saved_history_days_is_honoured(self, db, user, monkeypatch):
|
def test_the_default_window_is_used(self, db, user, monkeypatch):
|
||||||
from services import settings as settings_svc
|
|
||||||
|
|
||||||
seen = self._account(user, monkeypatch)
|
seen = self._account(user, monkeypatch)
|
||||||
settings_svc.save_settings(user["id"], {"historyDays": 90})
|
|
||||||
|
|
||||||
results = scheduler.sync_all_accounts()
|
results = scheduler.sync_all_accounts()
|
||||||
assert [r["status"] for r in results] == ["success"]
|
assert [r["status"] for r in results] == ["success"]
|
||||||
assert seen["days"] == 90
|
assert seen["days"] == scheduler.SYNC_DAYS
|
||||||
|
|
||||||
def test_full_history_becomes_the_maximum(self, db, user, monkeypatch):
|
def test_the_manual_range_is_ignored(self, db, user, monkeypatch):
|
||||||
|
"""全部历史 on the sync page must not turn every tick into 730 days."""
|
||||||
from services import settings as settings_svc
|
from services import settings as settings_svc
|
||||||
|
|
||||||
seen = self._account(user, monkeypatch)
|
seen = self._account(user, monkeypatch)
|
||||||
settings_svc.save_settings(user["id"], {"historyDays": 0})
|
settings_svc.save_settings(user["id"], {"historyDays": 0})
|
||||||
|
|
||||||
scheduler.sync_all_accounts()
|
scheduler.sync_all_accounts(respect_schedule=True)
|
||||||
assert seen["days"] == 730
|
|
||||||
|
|
||||||
def test_the_scheduled_tick_ignores_the_history_setting(
|
|
||||||
self, db, user, monkeypatch
|
|
||||||
):
|
|
||||||
"""历史范围 is the window for the manual 全量同步. A half-hourly tick
|
|
||||||
re-pulling 全部历史 is what kept the account in a 429 loop."""
|
|
||||||
from services import settings as settings_svc
|
|
||||||
|
|
||||||
seen = self._account(user, monkeypatch)
|
|
||||||
settings_svc.save_settings(user["id"], {"historyDays": 0, "autoSync": True})
|
|
||||||
|
|
||||||
scheduler.sync_all_accounts(days=scheduler.SYNC_DAYS, respect_schedule=True)
|
|
||||||
assert seen["days"] == scheduler.SYNC_DAYS
|
assert seen["days"] == scheduler.SYNC_DAYS
|
||||||
|
|
||||||
|
def test_an_explicit_override_still_wins(self, db, user, monkeypatch):
|
||||||
|
seen = self._account(user, monkeypatch)
|
||||||
|
scheduler.sync_all_accounts(days=30)
|
||||||
|
assert seen["days"] == 30
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ class TestDefaults:
|
|||||||
assert s["units"] == "metric"
|
assert s["units"] == "metric"
|
||||||
assert s["autoSync"] is True
|
assert s["autoSync"] is True
|
||||||
assert s["autoSyncMinutes"] == 60
|
assert s["autoSyncMinutes"] == 60
|
||||||
assert s["historyDays"] == 365
|
# 自上次同步: the manual sync page opens on the incremental option.
|
||||||
|
assert s["historyDays"] == -1
|
||||||
|
|
||||||
def test_body_fields_start_empty(self, db, user):
|
def test_body_fields_start_empty(self, db, user):
|
||||||
s = svc.get_settings(user["id"])
|
s = svc.get_settings(user["id"])
|
||||||
|
|||||||
@@ -22,12 +22,6 @@ const intervalLabel = (minutes: number) =>
|
|||||||
: minutes === 1440 ? '每天一次'
|
: minutes === 1440 ? '每天一次'
|
||||||
: `${minutes / 60} 小时`;
|
: `${minutes / 60} 小时`;
|
||||||
|
|
||||||
const historyLabel = (days: number) =>
|
|
||||||
days === -1 ? '自上次同步'
|
|
||||||
: days === 0 ? '全部历史'
|
|
||||||
: days >= 365 ? `${days / 365} 年`
|
|
||||||
: `${days} 天`;
|
|
||||||
|
|
||||||
function SettingsPage() {
|
function SettingsPage() {
|
||||||
const [settings, setSettings] = useState<UserSettings | null>(null);
|
const [settings, setSettings] = useState<UserSettings | null>(null);
|
||||||
const [options, setOptions] = useState<SettingsOptions | null>(null);
|
const [options, setOptions] = useState<SettingsOptions | null>(null);
|
||||||
@@ -204,7 +198,9 @@ function SettingsPage() {
|
|||||||
<label className="set-row">
|
<label className="set-row">
|
||||||
<span className="set-label">
|
<span className="set-label">
|
||||||
自动同步
|
自动同步
|
||||||
<span className="set-sub">后台按下面的频率拉取最新数据</span>
|
<span className="set-sub">
|
||||||
|
后台按下面的频率拉取最近几天。补历史请去同步页手动拉。
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="set-switch">
|
<span className="set-switch">
|
||||||
<input
|
<input
|
||||||
@@ -228,23 +224,12 @@ function SettingsPage() {
|
|||||||
<span className="set-chevron" aria-hidden="true">›</span>
|
<span className="set-chevron" aria-hidden="true">›</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
className="set-row"
|
|
||||||
onClick={() => options && pick('历史范围', options.historyDays,
|
|
||||||
historyLabel, s.historyDays,
|
|
||||||
(v) => save({ historyDays: v }))}
|
|
||||||
>
|
|
||||||
<span className="set-label">
|
|
||||||
历史范围
|
|
||||||
<span className="set-sub">「全量同步」往回拉多久的数据</span>
|
|
||||||
</span>
|
|
||||||
<span className="set-value">{historyLabel(s.historyDays)}</span>
|
|
||||||
<span className="set-chevron" aria-hidden="true">›</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<Link href="/sync/" className="set-row">
|
<Link href="/sync/" className="set-row">
|
||||||
<span className="set-label">数据同步</span>
|
<span className="set-label">
|
||||||
<span className="set-value">绑定账号 · 手动同步</span>
|
数据同步
|
||||||
|
<span className="set-sub">绑定账号,以及手动补历史数据</span>
|
||||||
|
</span>
|
||||||
|
<span className="set-value">去同步页</span>
|
||||||
<span className="set-chevron" aria-hidden="true">›</span>
|
<span className="set-chevron" aria-hidden="true">›</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { f7 } from 'framework7-react';
|
||||||
import {
|
import {
|
||||||
apiClient, AutoSyncStatus, DetailSyncStatus, errorMessage, GarminLoginStatus,
|
apiClient, AutoSyncStatus, DetailSyncStatus, errorMessage, GarminLoginStatus,
|
||||||
parseUtc, SyncStatus, UserSettings,
|
parseUtc, SettingsOptions, SyncStatus,
|
||||||
} from '../services/api';
|
} from '../services/api';
|
||||||
import Screen from '../components/Screen';
|
import Screen from '../components/Screen';
|
||||||
import './DataSync.css';
|
import './DataSync.css';
|
||||||
@@ -19,12 +20,27 @@ const when = (value: string | null | undefined) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const historyLabel = (days: number) =>
|
const historyLabel = (days: number) =>
|
||||||
days === 0 ? '全部历史' : days >= 365 ? `${days / 365} 年` : `${days} 天`;
|
days === -1 ? '自上次同步'
|
||||||
|
: days === 0 ? '全部历史'
|
||||||
|
: days >= 365 ? `${days / 365} 年`
|
||||||
|
: `${days} 天`;
|
||||||
|
|
||||||
|
/** What each range actually costs, so the choice is made with eyes open. */
|
||||||
|
const rangeHint = (days: number) =>
|
||||||
|
days === -1 ? '只补上次同步之后缺的那几天,最快'
|
||||||
|
: days === 0 ? '约 730 天,20–40 分钟'
|
||||||
|
: days >= 365 ? `${days} 天,大约 ${Math.ceil((days * 3) / 60)} 分钟`
|
||||||
|
: `${days} 天,几分钟`;
|
||||||
|
|
||||||
function SyncPage() {
|
function SyncPage() {
|
||||||
const [syncStatus, setSyncStatus] = useState<SyncStatus | null>(null);
|
const [syncStatus, setSyncStatus] = useState<SyncStatus | null>(null);
|
||||||
const [auto, setAuto] = useState<AutoSyncStatus | null>(null);
|
const [auto, setAuto] = useState<AutoSyncStatus | null>(null);
|
||||||
const [settings, setSettings] = useState<UserSettings | null>(null);
|
const [options, setOptions] = useState<SettingsOptions | null>(null);
|
||||||
|
// The manual sync range lives here now, not in 设置: the button that uses it
|
||||||
|
// is on this page, and a setting that only takes effect somewhere else is
|
||||||
|
// how 全部历史 came to look like it did nothing. It is still persisted, so
|
||||||
|
// the picker opens where it was left.
|
||||||
|
const [range, setRange] = useState(-1);
|
||||||
const [details, setDetails] = useState<DetailSyncStatus | null>(null);
|
const [details, setDetails] = useState<DetailSyncStatus | null>(null);
|
||||||
const [hasToken, setHasToken] = useState<boolean | null>(null);
|
const [hasToken, setHasToken] = useState<boolean | null>(null);
|
||||||
|
|
||||||
@@ -72,7 +88,8 @@ function SyncPage() {
|
|||||||
if (s?.status === 'syncing') beginSyncPolling();
|
if (s?.status === 'syncing') beginSyncPolling();
|
||||||
});
|
});
|
||||||
apiClient.getGarminAuthStatus().then(setHasToken).catch(() => setHasToken(false));
|
apiClient.getGarminAuthStatus().then(setHasToken).catch(() => setHasToken(false));
|
||||||
apiClient.getSettings().then(setSettings).catch(() => setSettings(null));
|
apiClient.getSettings().then((s) => setRange(s.historyDays)).catch(() => {});
|
||||||
|
apiClient.getSettingsOptions().then(setOptions).catch(() => setOptions(null));
|
||||||
return stopPolling;
|
return stopPolling;
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
@@ -195,30 +212,36 @@ function SyncPage() {
|
|||||||
|
|
||||||
// --- sync ---------------------------------------------------------------
|
// --- sync ---------------------------------------------------------------
|
||||||
|
|
||||||
/** The last couple of days, awaited inline — seconds, not minutes. */
|
/** Choose how far back to pull, and remember it for next time. */
|
||||||
const syncLatest = async () => {
|
const pickRange = () => {
|
||||||
setError('');
|
if (!options) return;
|
||||||
setMessage('');
|
f7.dialog.create({
|
||||||
setLoading(true);
|
title: '同步范围',
|
||||||
try {
|
buttons: [
|
||||||
const result = await apiClient.syncLatest(2);
|
...options.historyDays.map((v) => ({
|
||||||
await refresh();
|
text: historyLabel(v) + (v === range ? ' ✓' : ''),
|
||||||
setMessage(result.message || `已更新最近 ${result.recordsSynced ?? 2} 天`);
|
onClick: () => {
|
||||||
} catch (err: any) {
|
setRange(v);
|
||||||
setError(errorMessage(err, '同步失败'));
|
// Persisted, but nothing else reads it — auto-sync has its own
|
||||||
} finally {
|
// fixed window and never looks at this.
|
||||||
setLoading(false);
|
apiClient.saveSettings({ historyDays: v }).catch(() => {});
|
||||||
}
|
},
|
||||||
|
})),
|
||||||
|
{ text: '取消', color: 'gray' },
|
||||||
|
],
|
||||||
|
verticalButtons: true,
|
||||||
|
}).open();
|
||||||
};
|
};
|
||||||
|
|
||||||
/** The configured history window, in the background with progress. */
|
/** The one manual sync: the chosen range, in the background, with progress. */
|
||||||
const syncHistory = async () => {
|
const syncHistory = async () => {
|
||||||
setError('');
|
setError('');
|
||||||
setMessage('');
|
setMessage('');
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// 0 means "everything"; the backend caps it at what Garmin will serve.
|
// 0 means "everything" and -1 "since the last sync"; the backend caps
|
||||||
const started = await apiClient.syncGarminData(settings?.historyDays ?? 365);
|
// the former at what Garmin will actually serve.
|
||||||
|
const started = await apiClient.syncGarminData(range);
|
||||||
const s = await refresh();
|
const s = await refresh();
|
||||||
// A refused start (Garmin is throttling us) used to be swallowed: the
|
// A refused start (Garmin is throttling us) used to be swallowed: the
|
||||||
// button did nothing, no progress bar appeared, and no reason was shown.
|
// button did nothing, no progress bar appeared, and no reason was shown.
|
||||||
@@ -402,21 +425,32 @@ function SyncPage() {
|
|||||||
</section>
|
</section>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
{/* The range is chosen here, right above the button that uses
|
||||||
|
it. It used to live in 设置 while only taking effect on this
|
||||||
|
page, so picking 全部历史 there and pressing the (hardcoded
|
||||||
|
two-day) button here looked like the app ignoring you. */}
|
||||||
|
<div className="sync-secondary">
|
||||||
|
<button
|
||||||
|
className="btn btn-plain"
|
||||||
|
onClick={pickRange}
|
||||||
|
disabled={busy || !options}
|
||||||
|
>
|
||||||
|
<span className="sync-btn-label">同步范围</span>
|
||||||
|
<span className="sync-btn-sub">
|
||||||
|
{historyLabel(range)} · {rangeHint(range)}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary btn-large sync-primary"
|
className="btn btn-primary btn-large sync-primary"
|
||||||
onClick={syncLatest}
|
onClick={syncHistory}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
>
|
>
|
||||||
{loading ? '同步中…' : '同步最新数据'}
|
{loading ? '正在启动…' : '开始同步'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="sync-secondary">
|
<div className="sync-secondary">
|
||||||
<button className="btn btn-plain" onClick={syncHistory} disabled={busy}>
|
|
||||||
<span className="sync-btn-label">同步历史</span>
|
|
||||||
<span className="sync-btn-sub">
|
|
||||||
{historyLabel(settings?.historyDays ?? 365)}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
className="btn btn-plain"
|
className="btn btn-plain"
|
||||||
onClick={syncDetails}
|
onClick={syncDetails}
|
||||||
@@ -456,7 +490,7 @@ function SyncPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="sync-fact">
|
<div className="sync-fact">
|
||||||
<span className="sync-fact-label">自动同步</span>
|
<span className="sync-fact-label">自动同步(最近几天)</span>
|
||||||
<span className="sync-fact-value">
|
<span className="sync-fact-value">
|
||||||
{auto?.account?.autoSync === false
|
{auto?.account?.autoSync === false
|
||||||
? '已关闭'
|
? '已关闭'
|
||||||
|
|||||||
40
deploy/deploy.sh
Normal file
40
deploy/deploy.sh
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Deploy the latest local code to NAS.
|
||||||
|
# Usage: sh deploy/deploy.sh
|
||||||
|
# Prerequisites: client has been built (`npm run build` in client/)
|
||||||
|
|
||||||
|
APP="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
NAS_HOST="${1:-ericwyuan@192.168.50.64}"
|
||||||
|
NAS_PORT="${2:-2222}"
|
||||||
|
NAS_PATH="/volume1/web/garmin-health-lab"
|
||||||
|
NAS_PASS="iLoveJava5"
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== Building frontend ==="
|
||||||
|
cd "$APP/client"
|
||||||
|
npm run build 2>&1 | tail -3
|
||||||
|
|
||||||
|
echo "=== Syncing files to NAS (excluding .venv) ==="
|
||||||
|
cd "$APP"
|
||||||
|
tar cz \
|
||||||
|
--exclude='backend/.venv' \
|
||||||
|
--exclude='backend/__pycache__' \
|
||||||
|
--exclude='backend/*.pyc' \
|
||||||
|
--exclude='node_modules' \
|
||||||
|
-f /tmp/garmin_deploy.tar.gz \
|
||||||
|
backend/ deploy/
|
||||||
|
|
||||||
|
sshpass -p "$NAS_PASS" scp -P "$NAS_PORT" -o StrictHostKeyChecking=no \
|
||||||
|
/tmp/garmin_deploy.tar.gz "${NAS_HOST}:/tmp/garmin_deploy.tar.gz"
|
||||||
|
|
||||||
|
sshpass -p "$NAS_PASS" ssh -p "$NAS_PORT" -o StrictHostKeyChecking=no \
|
||||||
|
"$NAS_HOST" "echo '$NAS_PASS' | sudo -S tar xzf /tmp/garmin_deploy.tar.gz -C $NAS_PATH/ && rm /tmp/garmin_deploy.tar.gz"
|
||||||
|
|
||||||
|
rm -f /tmp/garmin_deploy.tar.gz
|
||||||
|
|
||||||
|
echo "=== Restarting service ==="
|
||||||
|
sshpass -p "$NAS_PASS" ssh -p "$NAS_PORT" -o StrictHostKeyChecking=no \
|
||||||
|
"$NAS_HOST" "echo '$NAS_PASS' | sudo -S $NAS_PATH/deploy/start.sh 2>&1"
|
||||||
|
|
||||||
|
echo "=== Done ==="
|
||||||
Reference in New Issue
Block a user