New Database
This commit is contained in:
@@ -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>' +
|
||||
|
||||
Reference in New Issue
Block a user