mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-15 19:32:21 +02:00
* refactor(notes): decompose Notes into feature-owned modules * fix(notes): avoid HTML interpolation in reminder picker * docs(notes): drop checkpoint archives, session logs and publication drafts The N0-N6 tarballs and raw pytest/node logs are not read by any script or test, and the logs carry absolute home-directory paths. Once squashed into dev they would stay in history for good. notes-pr.md and notes-issue.md were the drafts used to open #6712 and #6713, not documentation. notes-validation.md keeps the counts, the checkpoint hashes and implementation.sha256 as the record; local interpreter paths and the local Docker image name are replaced with portable placeholders. * fix(notes): keep the state store raw and stop masonry re-observing after close - state: writes built from views (filter, spread, splice) stored nested read-only views in the store, so structuredClone on a stored note threw DataCloneError. Store writes now unwrap nested views. - layout: a save that resolves after closePanel re-renders the leaving pane and re-created the masonry ResizeObserver that teardown had disposed. Skip creating it once Notes is closed. - index.js: drop 30 imported or declared names that were never used and group the imports at the top. Import order is unchanged. - Revert unrelated punctuation edits in .gitattributes and keep the header comment and docstring first in compare/vote.js and test_notes_dom_xss_helpers.py. Both fixes have a node test that fails on the previous head. * test(notes): make script rejection assertion case-insensitive Address CodeQL js/bad-tag-filter alert #1150 in test-only assertion and refresh Notes evidence manifest checksum. --------- Co-authored-by: Léo <leograndcontact@gmail.com>
270 lines
9.1 KiB
JavaScript
270 lines
9.1 KiB
JavaScript
// Immutable N0 oracle copied from F1 commit 1cc56cb9; used only for differential tests.
|
|
const _ORDINALS = ['1st', '2nd', '3rd', '4th', '5th'];
|
|
const _DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
function _hasTimeComponent(dateStr) {
|
|
return typeof dateStr === 'string' && /T\d{2}:\d{2}/.test(dateStr);
|
|
}
|
|
|
|
function _formatDueDate(dateStr) {
|
|
if (!dateStr) return '';
|
|
const d = new Date(dateStr);
|
|
if (isNaN(d)) return '';
|
|
const now = new Date();
|
|
const hasTime = _hasTimeComponent(dateStr);
|
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
const due = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
const diffDays = Math.round((due - today) / 86400000);
|
|
const timeStr = hasTime ? d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) : '';
|
|
if (hasTime && d < now) return 'overdue';
|
|
if (!hasTime && diffDays < 0) return 'overdue';
|
|
if (diffDays === 0) return hasTime ? timeStr : 'today';
|
|
if (diffDays === 1) return hasTime ? `tmrw ${timeStr}` : 'tomorrow';
|
|
const dateLabel = d.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
|
return hasTime ? `${dateLabel} ${timeStr}` : dateLabel;
|
|
}
|
|
|
|
function _isDueOverdue(dateStr) {
|
|
if (!dateStr) return false;
|
|
const d = new Date(dateStr);
|
|
if (isNaN(d)) return false;
|
|
if (_hasTimeComponent(dateStr)) return d < new Date();
|
|
return d < new Date(new Date().toDateString());
|
|
}
|
|
|
|
function _isDueTodayOrOverdue(dateStr) {
|
|
if (!dateStr) return false;
|
|
const d = new Date(dateStr);
|
|
const now = new Date();
|
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
const due = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
return due <= today;
|
|
}
|
|
|
|
function _laterTodayDate() {
|
|
const now = new Date();
|
|
const eight = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 18, 0); // 6pm today
|
|
// If less than 1 hour before 6pm, push to "in 3 hours" instead
|
|
if (eight - now < 60 * 60 * 1000) return new Date(now.getTime() + 3 * 60 * 60 * 1000);
|
|
return eight;
|
|
}
|
|
|
|
function _tomorrowDate() {
|
|
const t = new Date();
|
|
t.setDate(t.getDate() + 1);
|
|
t.setHours(8, 0, 0, 0);
|
|
return t;
|
|
}
|
|
|
|
function _nextWeekDate() {
|
|
const t = new Date();
|
|
const daysUntilMon = (8 - t.getDay()) % 7 || 7;
|
|
t.setDate(t.getDate() + daysUntilMon);
|
|
t.setHours(8, 0, 0, 0);
|
|
return t;
|
|
}
|
|
|
|
function _toLocalDatetimeStr(d) {
|
|
// Format as YYYY-MM-DDTHH:MM (local, no TZ)
|
|
const pad = n => String(n).padStart(2, '0');
|
|
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
}
|
|
|
|
function _formatReminderTag(dateStr) {
|
|
if (!dateStr) return '';
|
|
const d = new Date(dateStr);
|
|
if (isNaN(d)) return '';
|
|
const now = new Date();
|
|
const sameDay = d.toDateString() === now.toDateString();
|
|
const tomorrow = new Date(now); tomorrow.setDate(tomorrow.getDate() + 1);
|
|
const isTomorrow = d.toDateString() === tomorrow.toDateString();
|
|
const time = d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
|
|
if (sameDay) return `Today, ${time}`;
|
|
if (isTomorrow) return `Tomorrow, ${time}`;
|
|
const dateLabel = d.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
|
return `${dateLabel}, ${time}`;
|
|
}
|
|
|
|
function _nthWeekdayLabel(d) {
|
|
const n = Math.ceil(d.getDate() / 7); // 1..5
|
|
return `${_ORDINALS[n - 1] || `${n}th`} ${_DAYS[d.getDay()]}`;
|
|
}
|
|
|
|
function _isLastWeekdayOfMonth(d) {
|
|
const test = new Date(d);
|
|
test.setDate(d.getDate() + 7);
|
|
return test.getMonth() !== d.getMonth();
|
|
}
|
|
|
|
function _nthWeekdayOfMonth(year, month, weekday, n) {
|
|
const first = new Date(year, month, 1);
|
|
const offset = (weekday - first.getDay() + 7) % 7;
|
|
let day = 1 + offset + (n - 1) * 7;
|
|
// Last day of month
|
|
const lastDay = new Date(year, month + 1, 0).getDate();
|
|
if (day > lastDay) day -= 7;
|
|
return new Date(year, month, day, 0, 0, 0);
|
|
}
|
|
|
|
function _lastWeekdayOfMonth(year, month, weekday) {
|
|
const lastDay = new Date(year, month + 1, 0);
|
|
const back = (lastDay.getDay() - weekday + 7) % 7;
|
|
return new Date(year, month, lastDay.getDate() - back, 0, 0, 0);
|
|
}
|
|
|
|
function _snapToRepeat(currentDate, normRepeat) {
|
|
const hh = currentDate.getHours();
|
|
const mm = currentDate.getMinutes();
|
|
const now = Date.now();
|
|
const anchor = currentDate.getTime() > now ? currentDate : new Date();
|
|
const parts = normRepeat.split(':');
|
|
const kind = parts[0];
|
|
if (kind === 'weekly') {
|
|
const targetWd = parseInt(parts[1], 10);
|
|
if (isNaN(targetWd)) return null;
|
|
const d = new Date(anchor.getFullYear(), anchor.getMonth(), anchor.getDate(), hh, mm, 0, 0);
|
|
const delta = (targetWd - d.getDay() + 7) % 7;
|
|
d.setDate(d.getDate() + delta);
|
|
if (d.getTime() <= now) d.setDate(d.getDate() + 7);
|
|
return d;
|
|
}
|
|
if (kind === 'monthly') {
|
|
const sub = parts[1];
|
|
let y = anchor.getFullYear();
|
|
let m = anchor.getMonth();
|
|
// Walk forward up to 14 months to find the next matching slot.
|
|
for (let tries = 0; tries < 14; tries++) {
|
|
let target;
|
|
if (sub === 'day') {
|
|
const wantDay = parseInt(parts[2], 10);
|
|
if (isNaN(wantDay)) return null;
|
|
const lastDay = new Date(y, m + 1, 0).getDate();
|
|
target = new Date(y, m, Math.min(wantDay, lastDay));
|
|
} else if (sub === 'nth') {
|
|
const n = parseInt(parts[2], 10);
|
|
const wd = parseInt(parts[3], 10);
|
|
if (isNaN(n) || isNaN(wd)) return null;
|
|
target = _nthWeekdayOfMonth(y, m, wd, n);
|
|
} else if (sub === 'last') {
|
|
const wd = parseInt(parts[2], 10);
|
|
if (isNaN(wd)) return null;
|
|
target = _lastWeekdayOfMonth(y, m, wd);
|
|
} else {
|
|
return null;
|
|
}
|
|
target.setHours(hh, mm, 0, 0);
|
|
if (target.getTime() > now && target.getTime() >= anchor.getTime()) return target;
|
|
m++;
|
|
if (m > 11) { m = 0; y++; }
|
|
}
|
|
return null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function _formatRepeatLabel(repeat, originalDate) {
|
|
if (!repeat || repeat === 'none') return '';
|
|
const norm = _normalizeRepeat(repeat, originalDate);
|
|
if (norm === 'daily') return 'Daily';
|
|
if (norm === 'yearly') return 'Yearly';
|
|
const parts = norm.split(':');
|
|
if (parts[0] === 'weekly') {
|
|
const wd = parseInt(parts[1], 10);
|
|
if (isNaN(wd)) return 'Weekly';
|
|
return `Weekly on ${_DAYS[wd]}s`;
|
|
}
|
|
if (parts[0] === 'monthly') {
|
|
if (parts[1] === 'day') return `Monthly on day ${parts[2]}`;
|
|
if (parts[1] === 'nth') {
|
|
const n = parseInt(parts[2], 10);
|
|
const wd = parseInt(parts[3], 10);
|
|
return `Monthly on ${_ORDINALS[n - 1] || `${n}th`} ${_DAYS[wd]}`;
|
|
}
|
|
if (parts[1] === 'last') {
|
|
const wd = parseInt(parts[2], 10);
|
|
return `Monthly on last ${_DAYS[wd]}`;
|
|
}
|
|
}
|
|
return norm;
|
|
}
|
|
|
|
function _normalizeRepeat(repeat, originalDate) {
|
|
if (!repeat || repeat === 'none') return 'none';
|
|
if (repeat === 'daily' || repeat === 'yearly') return repeat;
|
|
if (/^(weekly|monthly):/.test(repeat)) return repeat;
|
|
// Legacy bare values - derive params from the original date
|
|
const wd = originalDate.getDay();
|
|
const n = Math.ceil(originalDate.getDate() / 7);
|
|
if (repeat === 'weekly') return `weekly:${wd}`;
|
|
if (repeat === 'monthly') return `monthly:day:${originalDate.getDate()}`;
|
|
if (repeat === 'monthly_nth_weekday') return `monthly:nth:${n}:${wd}`;
|
|
if (repeat === 'monthly_last_weekday') return `monthly:last:${wd}`;
|
|
return repeat;
|
|
}
|
|
|
|
function _advanceRecurring(dateStr, repeat) {
|
|
const orig = new Date(dateStr);
|
|
const hh = orig.getHours();
|
|
const mm = orig.getMinutes();
|
|
let d = new Date(orig);
|
|
const norm = _normalizeRepeat(repeat, orig);
|
|
if (norm === 'none') return null;
|
|
|
|
function step() {
|
|
if (norm === 'daily') {
|
|
d.setDate(d.getDate() + 1);
|
|
return;
|
|
}
|
|
if (norm === 'yearly') {
|
|
d.setFullYear(d.getFullYear() + 1);
|
|
return;
|
|
}
|
|
const parts = norm.split(':');
|
|
const kind = parts[0];
|
|
if (kind === 'weekly') {
|
|
// Snap to the requested weekday in the next 1-7 days
|
|
const targetWd = parseInt(parts[1], 10);
|
|
let delta = (targetWd - d.getDay() + 7) % 7;
|
|
if (delta === 0) delta = 7;
|
|
d.setDate(d.getDate() + delta);
|
|
d.setHours(hh, mm, 0, 0);
|
|
return;
|
|
}
|
|
if (kind === 'monthly') {
|
|
const sub = parts[1];
|
|
const ny = d.getFullYear() + (d.getMonth() === 11 ? 1 : 0);
|
|
const nm = (d.getMonth() + 1) % 12;
|
|
let target;
|
|
if (sub === 'day') {
|
|
const wantDay = parseInt(parts[2], 10);
|
|
const lastDay = new Date(ny, nm + 1, 0).getDate();
|
|
target = new Date(ny, nm, Math.min(wantDay, lastDay));
|
|
} else if (sub === 'nth') {
|
|
const n = parseInt(parts[2], 10);
|
|
const wd = parseInt(parts[3], 10);
|
|
target = _nthWeekdayOfMonth(ny, nm, wd, n);
|
|
} else if (sub === 'last') {
|
|
const wd = parseInt(parts[2], 10);
|
|
target = _lastWeekdayOfMonth(ny, nm, wd);
|
|
} else {
|
|
d = null; return;
|
|
}
|
|
target.setHours(hh, mm, 0, 0);
|
|
d = target;
|
|
return;
|
|
}
|
|
d = null;
|
|
}
|
|
|
|
step();
|
|
if (d === null) return null;
|
|
const now = Date.now();
|
|
// Cap catch-up to avoid runaway on a malformed/very-old date.
|
|
let guard = 5000;
|
|
while (d.getTime() <= now) {
|
|
if (--guard <= 0) return null;
|
|
step();
|
|
if (d === null) return null;
|
|
}
|
|
return _toLocalDatetimeStr(d);
|
|
}
|