'use strict'; // Personal reminder state and scheduling. Domain policy and display data are external. globalThis.PersonalReminderEngine = class PersonalReminderEngine { constructor(config,storage,clock=()=>Date.now()){this.config=config;this.storage=storage;this.clock=clock;} read(){const raw=this.storage.getItem(this.config.storage_key);if(!raw)return {version:this.config.schema_version,items:[],settings:{...this.config.defaults}};const s=JSON.parse(raw);if(s.version!==this.config.schema_version||!Array.isArray(s.items)||!s.settings)throw Error('storage');return s;} write(s){this.storage.setItem(this.config.storage_key,JSON.stringify(s));return s;} clockParts(t){if(!/^\d{2}:\d{2}$/.test(t))throw Error('invalid');const a=t.split(':').map(Number);if(a[0]>23||a[1]>59)throw Error('invalid');return a;} localDay(t){const d=new Date(t);return [d.getFullYear(),d.getMonth(),d.getDate()].join('-');} recurringSpec(x){ if(Number.isFinite(x.startAt)&&Number.isInteger(x.repeatEvery)&&x.repeatEvery>0&&this.config.repeat_units.includes(x.repeatUnit))return {startAt:x.startAt,every:x.repeatEvery,unit:x.repeatUnit}; const [h,m]=this.clockParts(x.time),d=new Date(x.createdAt);d.setHours(h,m,0,0); if(x.frequency==='weekly')d.setDate(d.getDate()+(Number(x.weekday)-d.getDay()+7)%7); if(d.getTime()0&&this.occurrence(spec,index)>now)index--; while(this.occurrence(spec,index+1)<=now)index++; return {spec,index,due:this.occurrence(spec,index),next:this.occurrence(spec,index+1)}; } due(x,now){const s=this.schedule(x,now);return s.due??s.next;} next(x,now){return this.schedule(x,now).next;} timelineOccurrences(x,start,end){ if(!(Number.isFinite(start)&&Number.isFinite(end)&&end>start))return []; if(x.mode==='recurring'){ const schedule=this.schedule(x,start-1),result=[];let index=schedule.index+1,previous=-Infinity; while(true){const at=this.occurrence(schedule.spec,index++);if(!Number.isFinite(at)||at>=end||at<=previous)break;if(Number.isFinite(x.endAt)&&at>x.endAt)break;if(at>=start)result.push(at);previous=at;} return result; } let at=x.target; if(x.mode==='pomodoro'){ // Keep the original appointment marker even as phases advance or finish. at=x.startAt; if(!Number.isFinite(at))at=x.phase==='waiting'?x.end:x.end-((x.session-1)*(x.work+x.rest)+x.work+(x.phase==='break'?x.rest:0))*60000; } return Number.isFinite(at)&&at>=start&&atx.id===id),mode=input.mode??old?.mode??c.type_behaviors[input.type];if(!c.enabled_types.includes(input.type)||!c.scheduling_modes.includes(mode))throw Error('invalid'); const text=String(input.text||'').trim();if(!text||text.length>c.limits.text_length)throw Error('invalid'); const x={id:old?.id||(globalThis.crypto?.randomUUID?.()||now.toString(36)+'-'+Math.random().toString(36).slice(2)),type:input.type,text,mode,createdAt:old?.createdAt||now}; if(mode==='one_time'){x.target=Number(input.target);x.lead=Number(input.lead);if(!Number.isFinite(x.target)||x.target<=now||!Number.isFinite(x.lead)||x.lead<0||x.lead>c.limits.lead_minutes)throw Error('invalid');x.preDone=x.target-x.lead*60000<=now;x.dueDone=false;x.sound=input.sound===true;} if(mode==='recurring'){ x.startAt=Number(input.startAt);x.repeatEvery=Number(input.repeatEvery);x.repeatUnit=String(input.repeatUnit||''); if(input.startAt==null||!Number.isFinite(x.startAt)||!Number.isInteger(x.repeatEvery)||x.repeatEvery<1||x.repeatEvery>c.limits.repeat_value||!c.repeat_units.includes(x.repeatUnit))throw Error('invalid'); const previous=old?.mode==='recurring'?this.recurringSpec(old):null; const unchanged=previous&&previous.startAt===x.startAt&&previous.every===x.repeatEvery&&previous.unit===x.repeatUnit; x.sound=input.sound===true;x.lastNotified=unchanged?old.lastNotified??null:null; if(this.tracksStreak(x)){x.streak=unchanged&&this.tracksStreak(old)?old.streak||0:0;x.lastCompleted=unchanged&&this.tracksStreak(old)?old.lastCompleted??null:null;} if(unchanged&&old.notice)x.notice=old.notice; } if(mode==='pomodoro'){ x.work=Number(input.work);x.rest=Number(input.rest);x.total=Number(input.total); if(![x.work,x.rest].every(n=>Number.isFinite(n)&&n>0&&n<=c.limits.duration_minutes)||!Number.isInteger(x.total)||x.total<1||x.total>c.limits.sessions)throw Error('invalid'); const requested=input.startAt==null||input.startAt===''?null:Number(input.startAt); const unchanged=old?.mode==='pomodoro'&&old.work===x.work&&old.rest===x.rest&&old.total===x.total&&(requested===null||requested===old.startAt); if(unchanged){for(const key of ['startAt','phase','session','end','active','notice'])if(old[key]!==undefined)x[key]=old[key];} else{if(requested!==null&&(!Number.isFinite(requested)||requestednow?'waiting':'work';x.session=1;x.end=x.phase==='waiting'?x.startAt:x.startAt+x.work*60000;x.active=true;} x.sound=input.sound===true; } const action=input.openQuran===undefined?old?.openQuran:input.openQuran; if(action!==undefined){if(!['','resume','wird'].includes(action))throw Error('invalid');x.openQuran=action;} const planId=input.quranPlanId??old?.quranPlanId; if(planId!==undefined){if(typeof planId!=='string'||!/^khatma-[a-z0-9-]+$/.test(planId))throw Error('invalid');x.quranPlanId=planId;} const endAt=input.endAt??old?.endAt; if(endAt!==undefined){if(mode!=='recurring'||!Number.isFinite(endAt)||endAtx.id!==id);this.write(s);} complete(id){const s=this.read(),x=s.items.find(x=>x.id===id);if(!x)return; if(this.tracksCompletion(x)){s.items=s.items.filter(x=>x.id!==id);this.write(s);return;} if(!this.tracksStreak(x))return; const now=this.clock(),schedule=this.schedule(x,now),due=schedule.due;if(due==null||duex.id===id),o=this.config.snooze_options.find(x=>x.id===option);if(!x||!o||x.mode!=='one_time')return;const now=this.clock();let target; if(o.mode==='duration')target=now+o.minutes*60000;else{const [h,m]=this.clockParts(s.settings[o.setting]);const d=new Date(now);d.setHours(h,m,0,0);if(o.mode==='next_day'||d.getTime()<=now)d.setDate(d.getDate()+1);target=d.getTime();} x.target=target;x.dueDone=false;x.preDone=target-x.lead*60000<=now;delete x.notice;this.write(s); } acknowledge(id){const s=this.read(),x=s.items.find(x=>x.id===id);if(x){delete x.notice;this.write(s);}} settings(values){const s=this.read();this.clockParts(values.evening_time);this.clockParts(values.tomorrow_time);const lead=Number(values.lead_minutes);if(!Number.isFinite(lead)||lead<0||lead>this.config.limits.lead_minutes)throw Error('invalid');s.settings={lead_minutes:lead,evening_time:values.evening_time,tomorrow_time:values.tomorrow_time,sound_enabled:values.sound_enabled===true};this.write(s);} tick(){const s=this.read(),now=this.clock();let changed=false; for(const x of s.items){ if(Number.isFinite(x.endAt)&&now>x.endAt){if(x.notice){delete x.notice;changed=true;}continue;} if(x.mode==='one_time'){ if(!x.dueDone&&now>=x.target){x.dueDone=true;x.preDone=true;x.notice='due';changed=true;} else if(x.lead>0&&!x.preDone&&now>=x.target-x.lead*60000){x.preDone=true;x.notice='pre';changed=true;} }else if(x.mode==='recurring'){ const schedule=this.schedule(x,now),due=schedule.due;if(due!=null&&due>=x.createdAt&&(!this.tracksStreak(x)||x.lastCompleted!==due)&&x.lastNotified!==due){const prev=this.occurrence(schedule.spec,schedule.index-1);if(this.tracksStreak(x)&&x.lastCompleted!=null&&prev!=null&&x.lastCompleted=x.end){moved=true;if(x.phase==='waiting'){x.phase='work';x.end+=x.work*60000;}else if(x.phase==='work'){ if(x.session>=x.total){x.active=false;x.notice='finished';break;} x.phase='break';x.end+=x.rest*60000; }else{x.phase='work';x.session++;x.end+=x.work*60000;}} if(moved){if(x.active)x.notice=x.phase;changed=true;} } } if(changed)this.write(s);return s; } }