feat: review snoozed deadline reminders (Closes #1104)
CI / lint (pull_request) Successful in 2m46s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 2m47s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-19 01:02:06 +00:00
1 parent 625d937f64
commit d5dc3f27db
9 files changed
+198 -5

No files matched your search

+1
View File
@@ -341,6 +341,7 @@ def test_deadline_preferences_persist_on_the_existing_device_subscription(tmp_pa
"timezone": "Europe/London",
"reminder_hour": 8,
"reminder_days": 2,
"snoozed_until": None,
}
+70 -1
View File
@@ -22,6 +22,9 @@ const deadlineHour = {value:'9', disabled:false, addEventListener:(_name, callba
const deadlineDays = {value:'2', disabled:false, addEventListener:(_name, callback) => state.deadlineDaysChange = callback};
const deadlineStatus = {set textContent(value) { state.deadlineText = value; }, get textContent() { return state.deadlineText; }};
const status = {set textContent(value) { state.text = value; }, get textContent() { return state.text; }};
const deadlineSnooze = {hidden:true};
const deadlineSnoozeStatus = {set textContent(value) { state.snoozeText = value; }, get textContent() { return state.snoozeText; }};
const deadlineSnoozeReview = {disabled:false, addEventListener:(_name, callback) => state.reviewSnooze = callback};
const existing = {endpoint:'https://push.example/device', toJSON() { return {endpoint:this.endpoint, keys:{p256dh:'key',auth:'auth'}}; }};
const registration = {pushManager:{
getSubscription: async () => state.current || null,
@@ -29,9 +32,11 @@ const registration = {pushManager:{
}};
const feature = createPushNotifications({
control, status, deadlineControl, deadlineStatus, deadlineHour, deadlineDays,
deadlineSnooze, deadlineSnoozeStatus, deadlineSnoozeReview,
onReviewDeadlines:() => { state.reviewed = true; },
notification: {permission:'default', requestPermission:async () => { state.prompts += 1; return state.permission || 'granted'; }},
serviceWorker: {ready:Promise.resolve(registration)},
fetchJson: async (url, options={}) => { state.requests.push([url,options.method || 'GET',options.body || '']); return state.server || {available:true,subscribed:false,public_key:'AQID'}; },
fetchJson: async (url, options={}) => { state.requests.push([url,options.method || 'GET',options.body || '']); if (state.failResume && options.method === 'DELETE' && url.endsWith('/deadlines/snooze')) throw new Error('offline'); return state.server || {available:true,subscribed:false,public_key:'AQID'}; },
});
(async () => { __SCENARIO__ })().catch(error => { console.error(error); process.exit(1); });
""".replace("__MODULE__", json.dumps(str(MODULE))).replace("__SCENARIO__", script)
@@ -158,6 +163,52 @@ process.stdout.write(JSON.stringify({checked:deadlineControl.checked, readiness:
assert [request[0] for request in denied["requests"]] == ["api/v1/push-subscription"]
def test_active_deadline_snooze_is_visible_in_agenda_with_a_local_resume_time():
result = run_scenario("""
state.server = {available:true,subscribed:true,deadline_enabled:true,reminder_hour:9,reminder_days:2,snoozed_until:1765003600,public_key:'AQID'};
state.current = existing;
await feature.init();
process.stdout.write(JSON.stringify({hidden:deadlineSnooze.hidden,text:state.snoozeText}));
""")
assert result["hidden"] is False
assert result["text"].startswith("Deadline reminders snoozed until ")
def test_review_now_resumes_this_device_and_opens_the_deadline_review_flow():
result = run_scenario("""
state.server = {available:true,subscribed:true,deadline_enabled:true,snoozed_until:1765003600,public_key:'AQID'};
state.current = existing;
await feature.init();
await state.reviewSnooze();
process.stdout.write(JSON.stringify({hidden:deadlineSnooze.hidden,disabled:deadlineSnoozeReview.disabled,reviewed:state.reviewed,requests:state.requests}));
""")
assert result["requests"][-1][0:2] == [
"api/v1/push-subscription/deadlines/snooze", "DELETE",
]
assert result["hidden"] is True
assert result["disabled"] is False
assert result["reviewed"] is True
def test_failed_resume_keeps_the_snooze_visible_and_retryable():
result = run_scenario("""
state.server = {available:true,subscribed:true,deadline_enabled:true,snoozed_until:1765003600,public_key:'AQID'};
state.current = existing;
await feature.init();
state.failResume = true;
await state.reviewSnooze();
process.stdout.write(JSON.stringify({hidden:deadlineSnooze.hidden,disabled:deadlineSnoozeReview.disabled,text:state.snoozeText,reviewed:state.reviewed}));
""")
assert result == {
"hidden": False,
"disabled": False,
"text": "Could not resume deadline reminders. Check your connection and try again.",
}
def test_mobile_dashboard_mounts_opt_in_and_precaches_its_controller():
root = MODULE.parents[1]
html = (root / "frontend" / "index.html").read_text()
@@ -181,3 +232,21 @@ def test_mobile_dashboard_mounts_opt_in_and_precaches_its_controller():
assert "pywebpush==" in requirements
assert "STACKCHAIN_VAPID_PUBLIC_KEY" in readme
assert "STACKCHAIN_VAPID_PRIVATE_KEY" in readme
def test_mobile_agenda_mounts_a_touch_safe_snooze_recovery_flow():
root = MODULE.parents[1]
html = (root / "frontend" / "index.html").read_text()
dashboard = (root / "frontend" / "dashboard.js").read_text()
css = (root / "frontend" / "dashboard.css").read_text()
assert 'id="deadline-snooze"' in html
assert 'id="deadline-snooze-status"' in html
assert 'id="review-snoozed-deadlines"' in html
assert "deadlineSnooze:qs('#deadline-snooze')" in dashboard
assert "onReviewDeadlines:" in dashboard
assert "window.location.hash = '#/my-work/agenda'" in dashboard
assert "qs('#protect-today')" in dashboard
assert ".deadline-snooze" in css
assert ".deadline-snooze button { min-height:44px;" in css
assert "overflow-wrap:anywhere" in css
+55
View File
@@ -1055,6 +1055,7 @@ async def test_authenticated_device_can_subscribe_report_status_and_unsubscribe(
"timezone": "UTC",
"reminder_hour": 9,
"reminder_days": 2,
"snoozed_until": None,
}
assert await main.subscribe_push(payload, request) == {"subscribed": True}
assert (await main.push_status(request))["subscribed"] is True
@@ -1119,6 +1120,60 @@ async def test_authenticated_device_can_snooze_its_enabled_deadline_reminder(tmp
assert devices["session-b"].snoozed_until is None
@pytest.mark.anyio
async def test_push_status_exposes_only_the_current_devices_active_deadline_snooze(tmp_path, monkeypatch):
store = PushSubscriptionStore(tmp_path / "push.sqlite3")
for session_id in ("session-a", "session-b"):
store.upsert(session_id, {
"endpoint": f"https://push.example/{session_id}",
"keys": {"p256dh": "public-key", "auth": "auth-secret"},
})
store.set_deadline_preferences(
session_id, enabled=True, timezone="UTC", reminder_hour=9
)
store.snooze_deadline_reminder("session-a", now=1_765_000_000)
store.snooze_deadline_reminder("session-b", now=1_765_001_000)
monkeypatch.setattr(main, "_push_subscription_store", store)
monkeypatch.setattr(main.time, "time", lambda: 1_765_000_100)
async def management_id(_session):
return "session-a"
monkeypatch.setattr(main.dashboard_auth, "session_management_id", management_id)
request = SimpleNamespace(state=SimpleNamespace(dashboard_session=object()))
status = await main.push_status(request)
assert status["snoozed_until"] == 1_765_003_600
assert store.deadline_preferences("session-a", now=1_765_004_000)["snoozed_until"] is None
@pytest.mark.anyio
async def test_authenticated_device_can_resume_only_its_own_deadline_reminders(tmp_path, monkeypatch):
store = PushSubscriptionStore(tmp_path / "push.sqlite3")
for session_id in ("session-a", "session-b"):
store.upsert(session_id, {
"endpoint": f"https://push.example/{session_id}",
"keys": {"p256dh": "public-key", "auth": "auth-secret"},
})
store.set_deadline_preferences(
session_id, enabled=True, timezone="UTC", reminder_hour=9
)
store.snooze_deadline_reminder(session_id, now=1_765_000_000)
monkeypatch.setattr(main, "_push_subscription_store", store)
async def management_id(_session):
return "session-a"
monkeypatch.setattr(main.dashboard_auth, "session_management_id", management_id)
request = SimpleNamespace(state=SimpleNamespace(dashboard_session=object()))
assert await main.resume_deadline_reminders(request) == {"snoozed": False}
devices = {device.session_id: device for device in store.deadline_reminder_devices()}
assert devices["session-a"].snoozed_until is None
assert devices["session-b"].snoozed_until == 1_765_003_600
@pytest.mark.anyio
async def test_subscription_rejects_an_unsafe_endpoint_before_persistence(tmp_path, monkeypatch):
store = PushSubscriptionStore(tmp_path / "push.sqlite3")