Show sync freshness status
This commit is contained in:
@@ -40,6 +40,8 @@ WORKBOOK_ID=your-workbook-id ./update.sh
|
||||
|
||||
- `last_success_at`:UTC ISO-8601 时间,使用 `Z` 表示 UTC。
|
||||
- `last_success_at_shanghai`:`Asia/Shanghai` 时区的 ISO 时间。
|
||||
- `last_data_update_at`:最近一次检测到 `data/merged.html` 内容变化的 UTC ISO-8601 时间,使用 `Z` 表示 UTC;数据内容未变化时保留上次成功记录,首次没有旧值时为 `null`。
|
||||
- `last_data_update_at_shanghai`:`last_data_update_at` 对应的 `Asia/Shanghai` 时区 ISO 时间;没有数据更新时间时为 `null`。
|
||||
- `worksheet_count`:本次读取的工作表数量。
|
||||
- `record_count`:合并结果中的数据记录数量,不含标题行。
|
||||
- `artifact_bytes`:本次生成的 `data/merged.html` 字节数。
|
||||
@@ -57,6 +59,8 @@ WORKBOOK_ID=your-workbook-id ./update.sh
|
||||
- **日期范围筛选**:支持当月、最近 30 天和全部日期;默认选择数据最新月份。
|
||||
- **KPI**:显示 Total Commission 和 Total GMV,并提供记录数、LinkName 数等概览信息。
|
||||
- **原始数据入口**:可从看板打开 `data/merged.html`。
|
||||
- **同步状态提示**:顶部逐行显示最后成功运行时间和数据最后更新时间,均按 `Asia/Shanghai` 格式化到秒;缺少记录时显示“暂无记录”。最后成功运行超过 24 小时会提示检查自动任务。
|
||||
- **数据过期提示**:`last_data_update_at` 超过 48 小时未更新时,顶部 warning 使用红色高亮并提示检查数据同步;没有数据更新时间记录时只提示“暂无记录”和缺少更新时间,不会按 48 小时误判。
|
||||
|
||||
### 汇率
|
||||
|
||||
|
||||
+53
-8
@@ -41,7 +41,8 @@
|
||||
.status { color: var(--muted); text-align: right; }
|
||||
.status[data-state="error"] { color: var(--error-text); }
|
||||
.status[data-state="empty"] { color: var(--muted); }
|
||||
.alert { margin: 0 0 18px; padding: 11px 14px; border: 1px solid var(--warning-border); border-radius: 10px; background: var(--warning-bg); color: var(--warning-text); }
|
||||
.alert { margin: 0 0 18px; padding: 11px 14px; border: 1px solid var(--warning-border); border-radius: 10px; background: var(--warning-bg); color: var(--warning-text); white-space: pre-line; }
|
||||
.alert[data-stale="true"] { border-color: var(--error-border); background: var(--error-bg); color: var(--error-text); }
|
||||
.alert[hidden] { display: none; }
|
||||
.filters { display: grid; gap: 12px; margin: 0 0 20px; padding: 16px 18px; border: 1px solid var(--border); border-radius: 14px; background: var(--surface); box-shadow: var(--shadow); }
|
||||
.filter-row { display: flex; flex-wrap: wrap; align-items: end; gap: 14px 20px; }
|
||||
@@ -283,24 +284,68 @@
|
||||
charts = [];
|
||||
}
|
||||
|
||||
function formatShanghaiTime(value) {
|
||||
if (!value) return null;
|
||||
var timestamp = Date.parse(value);
|
||||
if (Number.isNaN(timestamp)) return null;
|
||||
var parts = new Intl.DateTimeFormat('en-US', {
|
||||
timeZone: 'Asia/Shanghai',
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
||||
hourCycle: 'h23'
|
||||
}).formatToParts(new Date(timestamp));
|
||||
var values = {};
|
||||
parts.forEach(function (part) { values[part.type] = part.value; });
|
||||
return values.year + '-' + values.month + '-' + values.day + ' ' + values.hour + ':' + values.minute + ':' + values.second;
|
||||
}
|
||||
|
||||
function isOlderThan(value, milliseconds) {
|
||||
var timestamp = Date.parse(value);
|
||||
return !Number.isNaN(timestamp) && Date.now() - timestamp > milliseconds;
|
||||
}
|
||||
|
||||
function updateNotice() {
|
||||
var messages = [];
|
||||
if (state.syncLog) messages.push('最后成功运行:' + state.syncLog.displayTime);
|
||||
var log = state.syncLog;
|
||||
var lastSuccessAt = log && log.lastSuccessAt ? formatShanghaiTime(log.lastSuccessAt) : null;
|
||||
var lastDataUpdateAt = log && log.lastDataUpdateAt ? formatShanghaiTime(log.lastDataUpdateAt) : null;
|
||||
var dataIsStale = false;
|
||||
messages.push('最后成功运行:' + (lastSuccessAt || '暂无记录'));
|
||||
messages.push('数据最后更新:' + (lastDataUpdateAt || '暂无记录'));
|
||||
if (state.syncLogUnavailable) messages.push('运行日志暂不可用。');
|
||||
if (log && log.lastSuccessAt && isOlderThan(log.lastSuccessAt, 24 * 60 * 60 * 1000)) {
|
||||
messages.push('最后成功运行已超过24小时,请刷新页面或检查自动任务。');
|
||||
}
|
||||
if (log && log.lastDataUpdateAt) {
|
||||
dataIsStale = isOlderThan(log.lastDataUpdateAt, 48 * 60 * 60 * 1000);
|
||||
if (dataIsStale) messages.push('数据已超过48小时未更新,请检查数据同步。');
|
||||
} else {
|
||||
messages.push('没有数据更新时间记录。');
|
||||
}
|
||||
if (state.dateOnlyRows) messages.push('已过滤 ' + formatNumber(state.dateOnlyRows) + ' 条只有 Date、Commission 和 GMV 均为空的日期行。');
|
||||
if (state.invalidCurrencyRows) messages.push('已排除 ' + formatNumber(state.invalidCurrencyRows) + ' 条币种为空、Unknown 或格式无效且包含金额的记录,请检查原始数据。');
|
||||
if (state.sourceCurrencies.length > 1) messages.push('检测到多个已知源币种(' + state.sourceCurrencies.join('、') + '),已统一换算为 USD,不能切换到 IDR。');
|
||||
elements.warning.textContent = messages.join(' ');
|
||||
elements.warning.textContent = messages.join('\n');
|
||||
elements.warning.dataset.stale = dataIsStale ? 'true' : 'false';
|
||||
elements.warning.hidden = messages.length === 0;
|
||||
}
|
||||
|
||||
function parseSyncLog(payload) {
|
||||
if (!payload || typeof payload.last_success_at !== 'string' || !payload.last_success_at.trim() || Number.isNaN(Date.parse(payload.last_success_at))) {
|
||||
throw new Error('运行日志缺少有效的 last_success_at。');
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
||||
throw new Error('运行日志格式无效。');
|
||||
}
|
||||
var shanghaiTime = typeof payload.last_success_at_shanghai === 'string' ? payload.last_success_at_shanghai.trim() : '';
|
||||
var displayTime = shanghaiTime && !Number.isNaN(Date.parse(shanghaiTime)) ? shanghaiTime : payload.last_success_at;
|
||||
return { displayTime: displayTime };
|
||||
function parseTimestamp(field) {
|
||||
var value = payload[field];
|
||||
if (value === null || value === undefined || value === '') return null;
|
||||
if (typeof value !== 'string' || Number.isNaN(Date.parse(value))) {
|
||||
throw new Error('运行日志缺少有效的 ' + field + '。');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return {
|
||||
lastSuccessAt: parseTimestamp('last_success_at'),
|
||||
lastDataUpdateAt: parseTimestamp('last_data_update_at')
|
||||
};
|
||||
}
|
||||
|
||||
function setRateStatus(message) { elements.rateStatus.textContent = message; }
|
||||
|
||||
@@ -174,6 +174,18 @@ def run_git(args: list[str], env: dict[str, str]) -> subprocess.CompletedProcess
|
||||
)
|
||||
|
||||
|
||||
def read_sync_log() -> dict[str, object]:
|
||||
"""Read the previously published sync log when it is available."""
|
||||
path = ROOT / SYNC_LOG_PATH
|
||||
if not path.exists():
|
||||
return {}
|
||||
try:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return {}
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
|
||||
|
||||
def publish(content: bytes, sync_log: dict[str, object]) -> None:
|
||||
log_content = (json.dumps(sync_log, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
|
||||
targets = {
|
||||
@@ -244,10 +256,28 @@ def sync() -> None:
|
||||
sheets.append((name, headers, rows))
|
||||
headers, rows = merge_sheets(sheets)
|
||||
content = render_html(headers, rows)
|
||||
previous_sync_log = read_sync_log()
|
||||
previous_data_path = ROOT / PUBLISH_PATH
|
||||
data_changed = (
|
||||
not previous_data_path.exists()
|
||||
or previous_data_path.read_bytes() != content
|
||||
)
|
||||
success_at = datetime.now(timezone.utc)
|
||||
if data_changed:
|
||||
last_data_update_at = success_at.isoformat().replace("+00:00", "Z")
|
||||
last_data_update_at_shanghai = success_at.astimezone(ZoneInfo("Asia/Shanghai")).isoformat()
|
||||
else:
|
||||
last_data_update_at = previous_sync_log.get("last_data_update_at")
|
||||
last_data_update_at_shanghai = previous_sync_log.get("last_data_update_at_shanghai")
|
||||
if not isinstance(last_data_update_at, str) or not last_data_update_at:
|
||||
last_data_update_at = None
|
||||
if not isinstance(last_data_update_at_shanghai, str) or not last_data_update_at_shanghai:
|
||||
last_data_update_at_shanghai = None
|
||||
sync_log = {
|
||||
"last_success_at": success_at.isoformat().replace("+00:00", "Z"),
|
||||
"last_success_at_shanghai": success_at.astimezone(ZoneInfo("Asia/Shanghai")).isoformat(),
|
||||
"last_data_update_at": last_data_update_at,
|
||||
"last_data_update_at_shanghai": last_data_update_at_shanghai,
|
||||
"worksheet_count": len(worksheet_names),
|
||||
"record_count": len(rows),
|
||||
"artifact_bytes": len(content),
|
||||
|
||||
Reference in New Issue
Block a user