New Database

This commit is contained in:
喆OvO
2026-08-11 22:07:55 +08:00
parent cbbb2e6d34
commit ceb4404c6a
28 changed files with 1825 additions and 768 deletions
+97 -15
View File
@@ -124,6 +124,7 @@
state.events = data.events || [];
state.alarms = data.alarms || [];
state.countdowns = data.countdowns || [];
state.pomodoro = data.pomodoro || state.pomodoro || {};
state.update_schedule = data.update_schedule || {};
state.minutely = data.minutely || null;
state.warnings = data.warnings || null;
@@ -145,8 +146,79 @@
});
}
// ─── Local due-check fallback ─────────────
// 多 Gunicorn worker 下,后台 scheduler 与显示端 SSE 连接可能落在不同进程
// (进程内 EventBus 无法跨进程投递),SSE 提醒事件可能丢失。这里每秒本地
// 检测一次,保证闹钟/倒计时/番茄钟到点必定提醒;SSE 正常时由 fireOnce 去重。
state._firedLocal = {};
function fireOnce(key) {
if (state._firedLocal[key]) return false;
state._firedLocal[key] = true;
return true;
}
function localDueCheck() {
var nowSec = Date.now() / 1000;
var todayKey = new Date().toDateString();
// 1) 闹钟:按账户时区匹配 HH:MM + 星期
if (state.timezone) {
try {
var parts = new Intl.DateTimeFormat('en-US', {
timeZone: state.timezone, hour: '2-digit', minute: '2-digit',
weekday: 'short', hourCycle: 'h23',
}).formatToParts(new Date());
var hm = '', wd = '';
parts.forEach(function (p) {
if (p.type === 'hour') hm = p.value;
else if (p.type === 'minute') hm += ':' + p.value;
else if (p.type === 'weekday') wd = p.value;
});
var wdNum = { Sun: 7, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 }[wd];
(state.alarms || []).forEach(function (a) {
if (!a.enabled || String(a.time) !== hm) return;
var days = String(a.days || '1,2,3,4,5,6,7').split(',');
if (days.indexOf(String(wdNum)) === -1) return;
if (fireOnce('alarm-' + a.id + '-' + todayKey)) {
showAlarmOverlay({ alarms: [a], ts: new Date().toISOString(), source: 'local' });
}
});
} catch (e) {}
}
// 2) 倒计时:end_time(Unix 秒)到期
(state.countdowns || []).forEach(function (cd) {
if (!cd.enabled || !cd.end_time) return;
if (parseFloat(cd.end_time) > nowSec) return;
if (fireOnce('cd-' + cd.id + '-' + (cd.end_time || ''))) {
showAlarmOverlay({ countdowns: [cd], ts: new Date().toISOString(), source: 'local' });
switchPanel('countdown');
}
});
// 3) 番茄钟:running 且 end_time(Unix 秒)到期
var p = state.pomodoro || {};
if (p.running && p.end_time && parseFloat(p.end_time) <= nowSec) {
if (fireOnce('pomo-' + p.end_time)) {
showPomodoroOverlay();
}
}
}
function startLocalDueCheck() {
if (!state._localCheckTimer) {
state._localCheckTimer = setInterval(localDueCheck, 1000);
}
}
function openSSE(url) {
var es = new EventSource(url);
// SSE 与后台 scheduler 可能不在同一 Gunicorn worker(进程内事件总线无法跨
// 进程投递):本地到期检测 + 周期性重拉完整状态作为兜底,保证最终一致。
startLocalDueCheck();
if (!state._stateTimer) {
state._stateTimer = setInterval(fetchState, 15000);
}
es.onmessage = function (e) {
try {
var msg = JSON.parse(e.data);
@@ -174,7 +246,11 @@
function handleEvent(msg) {
if (msg.type === 'alarm_triggered') {
fetchState();
showAlarmOverlay(msg.data);
var todayKey = new Date().toDateString();
var anyNew = (msg.data.alarms || []).some(function (a) {
return fireOnce('alarm-' + a.id + '-' + todayKey);
});
if (anyNew) showAlarmOverlay(msg.data);
} else if (msg.type === 'pomodoro_updated') {
state.pomodoro = msg.data || {};
renderPomodoro();
@@ -205,8 +281,13 @@
showCustomNotification(msg.data);
} else if (msg.type === 'countdown_triggered') {
fetchState();
showAlarmOverlay(msg.data); // reuse alarm overlay UI
switchPanel('countdown');
var anyCd = (msg.data.countdowns || []).some(function (cd) {
return fireOnce('cd-' + cd.id + '-' + (cd.end_time || ''));
});
if (anyCd) {
showAlarmOverlay(msg.data); // reuse alarm overlay UI
switchPanel('countdown');
}
} else if (msg.type === 'display_mode_changed' || msg.type === 'force_refresh') {
location.reload();
} else if (msg.type === 'theme_changed') {
@@ -224,13 +305,14 @@
if (state.alarmActive) return; // already showing
state.alarmActive = true;
// Build info text (supports both alarm and countdown data)
// Build info text (supports both alarm and countdown data).
// 用户可控文本(label/time/target_time)一律转义,防止存储型 XSS。
var info = '';
(data.alarms || []).forEach(function (a) {
info += a.time + (a.label ? ' ' + a.label : '') + '<br>';
info += _esc(a.time) + (a.label ? ' ' + _esc(a.label) : '') + '<br>';
});
(data.countdowns || []).forEach(function (cd) {
info += cd.target_time + (cd.label ? ' ' + cd.label : '') + '<br>';
info += _esc(cd.target_time) + (cd.label ? ' ' + _esc(cd.label) : '') + '<br>';
});
var overlay = $('alarm-overlay');
@@ -658,7 +740,7 @@
html += '<div class="cd-item" data-cd-id="' + cd.id + '">' +
'<div class="cd-info">' +
'<div class="cd-time">' + durStr + '</div>' +
(cd.label ? '<div class="cd-label">' + cd.label + '</div>' : '') +
(cd.label ? '<div class="cd-label">' + _esc(cd.label) + '</div>' : '') +
'</div>' +
'<div class="cd-remain' + (overdue ? ' overdue' : '') + '">' + remainStr + '</div>' +
'<div class="alarm-actions">' +
@@ -1297,9 +1379,9 @@
}
if (m.error) {
if (m.cooldown) {
content.innerHTML = '<div style="color:var(--accent-red);font-size:13px;padding:8px 0;">' + m.error + '</div>';
content.innerHTML = '<div style="color:var(--accent-red);font-size:13px;padding:8px 0;">' + _esc(m.error) + '</div>';
} else {
content.innerHTML = '<div style="color:var(--text-dim);font-size:13px;padding:8px 0;">' + m.error + '</div>';
content.innerHTML = '<div style="color:var(--text-dim);font-size:13px;padding:8px 0;">' + _esc(m.error) + '</div>';
}
return;
}
@@ -1307,7 +1389,7 @@
var list = m.minutely || [];
var summary = m.summary || '';
var html = '';
if (summary) html += '<div style="font-size:13px;color:var(--text-dim);margin-bottom:8px;">' + summary + '</div>';
if (summary) html += '<div style="font-size:13px;color:var(--text-dim);margin-bottom:8px;">' + _esc(summary) + '</div>';
if (list.length > 0) {
html += '<div class="minutely-bars">';
list.forEach(function (item) {
@@ -1355,11 +1437,11 @@
var fields = '';
for (var key in item) {
if (item.hasOwnProperty(key) && item[key]) {
fields += '<div style="font-size:12px;color:var(--text-dim);margin-top:2px;"><strong>' + key + ':</strong> ' + item[key] + '</div>';
fields += '<div style="font-size:12px;color:var(--text-dim);margin-top:2px;"><strong>' + _esc(key) + ':</strong> ' + _esc(item[key]) + '</div>';
}
}
html += '<div class="warning-card">' +
'<div class="warning-title">' + (item.title || item.type || '预警') + '</div>' +
'<div class="warning-title">' + _esc(item.title || item.type || '预警') + '</div>' +
fields +
'</div>';
});
@@ -1443,9 +1525,9 @@
var days = a.once ? '仅一次' : (a.days || '').split(',').map(function (d) { return dayNames[parseInt(d)]; }).join(' ');
html += '<div class="alarm-item" data-alarm-id="' + a.id + '">' +
'<div class="alarm-info">' +
'<div class="alarm-time">' + a.time + '</div>' +
(a.label ? '<div class="alarm-label">' + a.label + '</div>' : '') +
'<div class="alarm-days">' + days + '</div></div>' +
'<div class="alarm-time">' + _esc(a.time) + '</div>' +
(a.label ? '<div class="alarm-label">' + _esc(a.label) + '</div>' : '') +
'<div class="alarm-days">' + _esc(days) + '</div></div>' +
'<div class="alarm-actions">' +
'<button class="alarm-action-btn edit-btn" data-action="edit-alarm" data-id="' + a.id + '">编辑</button>' +
'<button class="alarm-action-btn del-btn" data-action="delete-alarm" data-id="' + a.id + '">删除</button>' +