From 0a687821726bf1e542c0c094ee5ce09236308d20 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 2 Oct 2026 10:48:13 +0000 Subject: [PATCH] fix(tasks): enable editing goals and tasks --- CHANGELOG.md | 6 ++++ README.md | 4 +-- package.json | 2 +- public/addons.js | 2 +- public/data-actions.js | 17 ++++++++++++ public/i18n-renderers.js | 2 +- public/index.html | 6 ++-- public/locales/ar.json | 9 +++++- public/locales/en.json | 9 +++++- public/locales/es.json | 9 +++++- public/locales/fr.json | 9 +++++- public/locales/ja.json | 9 +++++- public/locales/ru.json | 9 +++++- public/locales/zh-CN.json | 9 +++++- public/locales/zh-HK.json | 9 +++++- public/locales/zh-MO.json | 9 +++++- public/locales/zh-TW.json | 9 +++++- public/service-worker.js | 2 +- public/system-navigation.js | 2 +- public/tasks.js | 31 +++++++++++++++------ public/terminal-app.js | 4 +-- scripts/build.mjs | 2 +- scripts/validate.mjs | 55 +++++++++++++++++++++++++++++++++++-- 23 files changed, 191 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c44286..1889787 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ 本项目按 `v主版本.次版本.修订版本` 标记应用版本;按需创建 Git tag。下列内容以对应提交和已发布功能为准。 +## v1.18.1 — 2026-10-02 + +- 目标与待办新增编辑入口:可修改待办标题、截止日期、优先级及关联目标,也可修改目标标题、备注、目标日期和桌面倒计时选项。 +- 已完成的目标与待办同样支持编辑;保留记录 ID、创建时间、完成状态及现有关联,沿用记录级云同步。 +- 修正验证脚本对固定“今日”日期的假设,并覆盖编辑表单和数据写入。 + ## v1.18.0 — 2026-09-28 - 日历按语言地区和浏览器区域推断国家或地区,并提供独立选择;选择仅保存在当前设备,可随时切回自动模式。 diff --git a/README.md b/README.md index 3c31525..9a77b14 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 以桌面操作系统为交互方式的个人记录网站。用 App 记录日常、管理目标与待办,并沿时间线回看已经留下的内容。 -在线站点:[Life OS](https://life-os-personal-desktop.biss0.chatgpt.site) · 当前应用版本:**v1.18.0** · [更新日志](CHANGELOG.md) +在线站点:[Life OS](https://life-os-personal-desktop.biss0.chatgpt.site) · 当前应用版本:**v1.18.1** · [更新日志](CHANGELOG.md) ## 功能 @@ -11,7 +11,7 @@ | 今日 | 为选定日期写记忆,记录天气、1–5 能量、标题、标签、地点和关联任务;每日电影是写作提示,不会替代记忆标题。 | | 时间线 | 按日期浏览记忆,使用关键词或标签筛选,编辑、删除和收藏。 | | 博物馆 | 回看收藏的记忆。 | -| 目标与待办 | 创建目标和任务,设置截止日期、优先级和完成状态;目标倒计时可选择显示在桌面。 | +| 目标与待办 | 创建和编辑目标、待办,调整日期、优先级、关联目标与完成状态;目标倒计时可选择显示在桌面。 | | 日历 | 在月视图与日视图中查看记忆、任务、目标、节日和公共假期;按界面语言地区自动选择日历国家,也可手动切换,为选定日期新增记忆。 | | 数据库管理器 | 在再次登录并确认风险后,按表查看、创建、编辑和删除当前账号的云端记录。 | | 洞察 | 周报和月报展示记录频率、完成任务数、常见天气、能量记录及收藏或标记高光的记忆;仅依据已保存的数据统计。 | diff --git a/package.json b/package.json index 5e4e727..d57b354 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "life-os", - "version": "1.18.0", + "version": "1.18.1", "private": true, "type": "module", "scripts": { diff --git a/public/addons.js b/public/addons.js index f18ac36..a107ca8 100644 --- a/public/addons.js +++ b/public/addons.js @@ -3,7 +3,7 @@ const DRAFT_KEY = 'lifeos_today_draft_v1'; const localDay = () => { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; }; const escapeHtml = value => String(value || '').replace(/[&<>]/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char])); - const APP_VERSION = '1.18.0'; + const APP_VERSION = '1.18.1'; window.LifeOSVersion = APP_VERSION; const appIds = ['today', 'timeline', 'museum', 'insights', 'profile', 'tasks', 'calendar', 'system', 'browser', 'terminal', 'database']; const t = (key, values) => LifeI18n.t(key, values); diff --git a/public/data-actions.js b/public/data-actions.js index 121713c..c36e6bd 100644 --- a/public/data-actions.js +++ b/public/data-actions.js @@ -19,6 +19,14 @@ const goal = { id: `goal_${crypto.randomUUID()}`, title, description: String(description).trim().slice(0, 240), targetDate, showOnDesktop: Boolean(targetDate && showOnDesktop), done: false, createdAt: new Date().toISOString() }; const data = board(); data.goals.unshift(goal); persistBoard(data); return goal; }, + updateGoal(id, { title, description = '', targetDate = '', showOnDesktop = false }) { + title = required(title, 100); + if (!validDate(targetDate)) throw new Error('invalidDate'); + const data = board(), goal = data.goals.find(item => item.id === id); + if (!goal) throw new Error('notFound'); + Object.assign(goal, { title, description: String(description).trim().slice(0, 240), targetDate, showOnDesktop: Boolean(targetDate && showOnDesktop), updatedAt: new Date().toISOString() }); + persistBoard(data); return goal; + }, createTask({ title, dueDate = '', goalId = '', priority = 'normal' }) { title = required(title, 140); if (!validDate(dueDate)) throw new Error('invalidDate'); @@ -28,6 +36,15 @@ const task = { id: `task_${crypto.randomUUID()}`, title, dueDate, goalId, priority, done: false, createdAt: new Date().toISOString() }; data.tasks.push(task); persistBoard(data); return task; }, + updateTask(id, { title, dueDate = '', goalId = '', priority = 'normal' }) { + title = required(title, 140); + if (!validDate(dueDate)) throw new Error('invalidDate'); + if (!['normal', 'high', 'low'].includes(priority)) throw new Error('invalidPriority'); + const data = board(), task = data.tasks.find(item => item.id === id); + if (!task || (goalId && !data.goals.some(goal => goal.id === goalId))) throw new Error('notFound'); + Object.assign(task, { title, dueDate, goalId, priority, updatedAt: new Date().toISOString() }); + persistBoard(data); return task; + }, setTaskDone(id, done) { const data = board(), task = data.tasks.find(item => item.id === id); if (!task) throw new Error('notFound'); diff --git a/public/i18n-renderers.js b/public/i18n-renderers.js index 2819643..1db99c7 100644 --- a/public/i18n-renderers.js +++ b/public/i18n-renderers.js @@ -110,7 +110,7 @@ window.saveProfile = () => { save('lifeos_profile', { name: pname.value, birthday: pbday.value, startDate: pstart.value || day(), motto: pmotto.value }); renderAll(); toast(t('profile.saved')); }; window.systemApp = container => { const state = settings(), preference = LifeI18n.preference(), bytes = new Blob([JSON.stringify({ mems: mems(), museum: museum(), profile: profile(), system: sys() })]).size, oldBackup = state.lastBackup && Date.now() - new Date(state.lastBackup) > 12096e5, detected = t('system.browserDetected', { name: LifeI18n.names[LifeI18n.browserLanguage()] }); - container.innerHTML = `
${t('system.eyebrow')}

${t('system.title')}

${t('system.appearance')}
${['dark','light','auto'].map(item => ``).join('')}
${t('system.language')}

${detected}

${LifeI18n.supported.map(code => ``).join('')}
${window.LifeOSNotifications?.markup() || ''}
${t('system.data')}
${t('system.storage')}

${t('system.local')}
${t('system.storageSummary',{memories:LifeI18n.number(mems().length),museum:LifeI18n.number(museum().length),tasks:LifeI18n.number(sys().tasksBoard?.tasks?.length||0),size:LifeI18n.number(Math.ceil(bytes/1024))})}

${t('system.dataStorage')}

${t('system.stored')}
${t('system.backups')}

${t('system.lastBackup',{value:state.lastBackup ? short(state.lastBackup.slice(0,10)) : t('common.never')})}${oldBackup ? `
${t('system.recommended')}` : ''}

${t('system.version')}

${t('system.release',{version:window.LifeOSVersion||'1.18.0'})}

`; + container.innerHTML = `
${t('system.eyebrow')}

${t('system.title')}

${t('system.appearance')}
${['dark','light','auto'].map(item => ``).join('')}
${t('system.language')}

${detected}

${LifeI18n.supported.map(code => ``).join('')}
${window.LifeOSNotifications?.markup() || ''}
${t('system.data')}
${t('system.storage')}

${t('system.local')}
${t('system.storageSummary',{memories:LifeI18n.number(mems().length),museum:LifeI18n.number(museum().length),tasks:LifeI18n.number(sys().tasksBoard?.tasks?.length||0),size:LifeI18n.number(Math.ceil(bytes/1024))})}

${t('system.dataStorage')}

${t('system.stored')}
${t('system.backups')}

${t('system.lastBackup',{value:state.lastBackup ? short(state.lastBackup.slice(0,10)) : t('common.never')})}${oldBackup ? `
${t('system.recommended')}` : ''}

${t('system.version')}

${t('system.release',{version:window.LifeOSVersion||'1.18.1'})}

`; }; window.importPreview = () => { const data = importData, board = data.system?.tasksBoard || {}; rootModal().innerHTML = ``; }; window.replaceConfirm = () => { rootModal().innerHTML = ``; }; diff --git a/public/index.html b/public/index.html index 9d6a732..e30e63b 100644 --- a/public/index.html +++ b/public/index.html @@ -14,7 +14,7 @@ function browserLanguage(){return window.LifeI18n?.browserLanguage()||'en'}funct function appName(id){return i18n(APPS[id][1])}function moodName(m){return i18n(m.toUpperCase(),cap(m))}function day(){const d=new Date();return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`}function fmtDate(d){return new Date(d+'T12:00:00').toLocaleDateString(LifeI18n.locale(),{weekday:'long',month:'long',day:'numeric',year:'numeric'})}function shortDate(d){return new Date(d+'T12:00:00').toLocaleDateString(LifeI18n.locale(),lang().startsWith('zh')?{month:'numeric',day:'numeric'}:{month:'short',day:'numeric'}).toUpperCase()}function cap(x){return x[0].toUpperCase()+x.slice(1)}function esc(s){return (s||'').replace(/[&<>]/g,x=>({'&':'&','<':'<','>':'>'}[x]))} function applyWallpaper(w){if(!w?.url)return;desktop.style.backgroundImage=`linear-gradient(120deg,color-mix(in srgb,var(--bg) 68%,transparent),color-mix(in srgb,var(--bg) 36%,transparent)),url('${w.url}')`;desktop.style.backgroundSize='cover';desktop.style.backgroundPosition='center';desktop.title=LifeI18n.t('shell.wallpaperTitle',{title:w.title||LifeI18n.t('wallpaper.dailyImage')})}async function loadBingWallpaper(){let s=settings();if(s.wallpaper?.date===day()&&s.wallpaper?.source==='lifeos-proxy'){applyWallpaper(s.wallpaper);return}try{let r=await fetch('/api/bing-wallpaper'),d=await r.json();if(!r.ok||!d.url)throw Error(d.error||'No Bing image');s=settings();if((s.wallpaperSource||'bing')!=='bing')return;s.wallpaper={date:day(),url:'/api/bing-wallpaper-image?date='+day(),title:d.title||'Bing daily image',source:'lifeos-proxy'};save('lifeos_settings',s);applyWallpaper(s.wallpaper)}catch(e){console.info('Bing wallpaper unavailable; using the calm fallback.',e)}}function init(){let s=settings();document.documentElement.dataset.theme=s.theme==='auto'?(matchMedia('(prefers-color-scheme:light)').matches?'light':'dark'):s.theme;renderIcons();renderTaskbar();loadBingWallpaper();setInterval(clock,1000);clock();setTimeout(()=>window.applyLanguage?.(),0);setTimeout(()=>boot.remove(),650)} function desktopIconClick(id,e){if(innerWidth<650){openApp(id);return}selectedIcon=id;document.querySelectorAll('.app-icon').forEach(x=>x.classList.remove('selected'));e.currentTarget.classList.add('selected')}function desktopIconKey(id,e){if(e.key==='Enter'||e.key===' '){e.preventDefault();openApp(id)}}function renderIcons(){icons.innerHTML=Object.entries(APPS).map(([id,[g,n]])=>``).join('')}function taskClick(id){let w=windows[id];if(!w){openApp(id);return}if(w.classList.contains('min')){openApp(id);return}if(activeWindow===id){minWin(id);return}focus(w)}function renderTaskbar(){let ids=innerWidth<650?Object.keys(windows):['today','timeline','museum','insights','tasks','calendar','browser','terminal','database'];tasks.innerHTML=ids.map(id=>{let w=windows[id],min=w?.classList.contains('min'),fg=activeWindow===id&&!min,name=window.appName(id);return``}).join('')} -function clock(){let d=new Date();document.querySelector('#clock').textContent=d.toLocaleTimeString(LifeI18n.locale(), {hour:'2-digit',minute:'2-digit'});document.querySelector('#date').textContent=d.toLocaleDateString(LifeI18n.locale(),lang().startsWith('zh')?{month:'numeric',day:'numeric'}:{month:'short',day:'numeric'}).toUpperCase()}function toggleLauncher(){launcher.classList.toggle('hidden');calendar.classList.add('hidden');launcher.innerHTML=`
LIFE OS${LifeI18n.t(`shell.${new Date().getHours()<12?'morning':new Date().getHours()<18?'afternoon':'evening'}`)}
${Object.entries(APPS).map(([id,a])=>``).join('')}
${LifeI18n.t('shell.systemOnline',{version:window.LifeOSVersion||'1.18.0'})}
`}function toggleCalendar(){calendar.classList.toggle('hidden');launcher.classList.add('hidden');calendar.innerHTML=`
${LifeI18n.t('shell.systemDate')}

${fmtDate(day())}

${LifeI18n.t('shell.todaySummary',{memories:mems().length,preserved:museum().length})}

`}function hideContext(){document.querySelector('#life-context')?.remove()}function showContext(e,id){e.preventDefault();e.stopPropagation();hideContext();let labels={open:LifeI18n.t('shell.open'),min:LifeI18n.t('shell.minimize'),close:LifeI18n.t('shell.menuCloseWindow'),refresh:LifeI18n.t('shell.refresh'),new:LifeI18n.t('shell.newToday'),system:LifeI18n.t('shell.systemSettings')},actions=id?`${windows[id]?``:''}`:`
`;let m=document.createElement('div');m.id='life-context';m.style.cssText=`position:fixed;z-index:7000;left:${Math.min(e.clientX,innerWidth-190)}px;top:${Math.min(e.clientY,innerHeight-150)}px;width:180px;padding:6px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:var(--shadow)`;m.innerHTML=actions;m.querySelectorAll('button').forEach(b=>b.style.cssText='display:block;width:100%;border:0;background:transparent;color:var(--ink);text-align:left;border-radius:6px;padding:9px 10px;font:12px Manrope');m.querySelectorAll('button').forEach(b=>b.onmouseenter=()=>b.style.background='var(--panel2)');document.body.append(m)}document.addEventListener('contextmenu',e=>{if(e.target.closest('.desktop')&&!e.target.closest('.app-icon')&&!e.target.closest('.window'))showContext(e,null)});document.addEventListener('click',hideContext) +function clock(){let d=new Date();document.querySelector('#clock').textContent=d.toLocaleTimeString(LifeI18n.locale(), {hour:'2-digit',minute:'2-digit'});document.querySelector('#date').textContent=d.toLocaleDateString(LifeI18n.locale(),lang().startsWith('zh')?{month:'numeric',day:'numeric'}:{month:'short',day:'numeric'}).toUpperCase()}function toggleLauncher(){launcher.classList.toggle('hidden');calendar.classList.add('hidden');launcher.innerHTML=`
LIFE OS${LifeI18n.t(`shell.${new Date().getHours()<12?'morning':new Date().getHours()<18?'afternoon':'evening'}`)}
${Object.entries(APPS).map(([id,a])=>``).join('')}
${LifeI18n.t('shell.systemOnline',{version:window.LifeOSVersion||'1.18.1'})}
`}function toggleCalendar(){calendar.classList.toggle('hidden');launcher.classList.add('hidden');calendar.innerHTML=`
${LifeI18n.t('shell.systemDate')}

${fmtDate(day())}

${LifeI18n.t('shell.todaySummary',{memories:mems().length,preserved:museum().length})}

`}function hideContext(){document.querySelector('#life-context')?.remove()}function showContext(e,id){e.preventDefault();e.stopPropagation();hideContext();let labels={open:LifeI18n.t('shell.open'),min:LifeI18n.t('shell.minimize'),close:LifeI18n.t('shell.menuCloseWindow'),refresh:LifeI18n.t('shell.refresh'),new:LifeI18n.t('shell.newToday'),system:LifeI18n.t('shell.systemSettings')},actions=id?`${windows[id]?``:''}`:`
`;let m=document.createElement('div');m.id='life-context';m.style.cssText=`position:fixed;z-index:7000;left:${Math.min(e.clientX,innerWidth-190)}px;top:${Math.min(e.clientY,innerHeight-150)}px;width:180px;padding:6px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:var(--shadow)`;m.innerHTML=actions;m.querySelectorAll('button').forEach(b=>b.style.cssText='display:block;width:100%;border:0;background:transparent;color:var(--ink);text-align:left;border-radius:6px;padding:9px 10px;font:12px Manrope');m.querySelectorAll('button').forEach(b=>b.onmouseenter=()=>b.style.background='var(--panel2)');document.body.append(m)}document.addEventListener('contextmenu',e=>{if(e.target.closest('.desktop')&&!e.target.closest('.app-icon')&&!e.target.closest('.window'))showContext(e,null)});document.addEventListener('click',hideContext) function openApp(id){launcher.classList.add('hidden');let w=windows[id];if(w){w.classList.remove('min');focus(w);return}w=document.createElement('section');w.className='window';w.dataset.id=id;w.style.left=(innerWidth<650?0:90+Object.keys(windows).length*36)+'px';w.style.top=(innerWidth<650?0:72+Object.keys(windows).length*28)+'px';w.innerHTML=`
${APPS[id][0]}${appName(id)}
`;windows[id]=w;document.querySelector('#windows').append(w);let controls=w.querySelector('.controls'),bar=w.querySelector('.titlebar');controls.addEventListener('pointerdown',e=>{e.preventDefault();e.stopPropagation()});controls.addEventListener('click',e=>{let action=e.target.closest('button')?.dataset.windowAction;if(!action)return;e.preventDefault();e.stopPropagation();if(action==='min')minWin(id);if(action==='max')maxWin(id);if(action==='close')closeWin(id)});bar.addEventListener('dblclick',e=>{if(!e.target.closest('.controls'))maxWin(id)});w.addEventListener('pointerdown',()=>focus(w));drag(w);renderApp(id);focus(w)}function focus(w){if(!w)return;activeWindow=w.dataset.id;w.style.zIndex=++z;document.querySelectorAll('.window').forEach(x=>x.classList.toggle('active-window',x===w));renderTaskbar()}function closeWin(id){windows[id]?.remove();delete windows[id];if(activeWindow===id){activeWindow=Object.keys(windows).find(k=>!windows[k].classList.contains('min'))||null;if(activeWindow)focus(windows[activeWindow])}renderTaskbar()}function minWin(id){let w=windows[id];if(!w)return;w.classList.add('min');if(activeWindow===id){activeWindow=Object.keys(windows).find(k=>k!==id&&!windows[k].classList.contains('min'))||null;if(activeWindow)focus(windows[activeWindow])}renderTaskbar();toast(i18n('MINIMIZED — TAP THE APP ICON TO RESTORE'))}function maxWin(id){let w=windows[id];if(!w)return;w.classList.toggle('max');focus(w)}function snapWindow(w){if(innerWidth<650||w.classList.contains('max'))return;let r=w.getBoundingClientRect(),edge=22;if(r.topinnerWidth-edge){w.style.left='calc(50vw + 9px)';w.style.top='18px';w.style.width='calc(50vw - 27px)';w.style.height='calc(100vh - 98px)'}}function drag(w){let bar=w.querySelector('.titlebar'),x,y,l,t;bar.onpointerdown=e=>{if(e.target.closest('.controls')||innerWidth<650)return;if(w.classList.contains('max'))w.classList.remove('max');x=e.clientX;y=e.clientY;l=w.offsetLeft;t=w.offsetTop;bar.setPointerCapture(e.pointerId);bar.onpointermove=e=>{w.style.left=Math.max(0,l+e.clientX-x)+'px';w.style.top=Math.max(0,t+e.clientY-y)+'px'};bar.onpointerup=()=>{bar.onpointermove=null;snapWindow(w)}}}document.addEventListener('keydown',e=>{if(e.altKey&&e.key==='F4'&&activeWindow){e.preventDefault();closeWin(activeWindow)}if(e.key==='Escape'&&activeWindow&&!document.querySelector('.modal')&&!document.querySelector('#lifeos-search')){let w=windows[activeWindow];if(w?.classList.contains('max')){e.preventDefault();maxWin(activeWindow)}}}) function renderApp(id){let c=document.querySelector('#content-'+id);if(!c)return;if(id==='today')window.today(c);if(id==='timeline')window.timeline(c);if(id==='museum')window.museumApp(c);if(id==='insights')window.insights(c);if(id==='profile')window.profileApp(c);if(id==='tasks')window.tasksApp(c);if(id==='calendar')window.calendarApp(c);if(id==='system')window.systemApp(c);if(id==='browser')window.browserApp(c);if(id==='terminal')window.terminalApp(c);if(id==='database')window.databaseApp(c);setTimeout(()=>window.applyLanguage?.(),0)}function renderAll(){Object.keys(windows).forEach(renderApp);renderTaskbar();window.renderDesktopGoalCountdowns?.();setTimeout(()=>window.applyLanguage?.(),0)}document.addEventListener('click',e=>{if(e.target.closest('#content-today .chip,#content-today .energy button'))setTimeout(()=>window.applyLanguage?.(),0)}) let todayDraft=null,todayTitleDraft=null,todayTargetDate=null;function today(c){let m=mems().find(x=>x.date===day());if(todayDraft===null){selectedMood=m?.mood||'cloudy';selectedEnergy=m?.energy||3}let f=m||filmFor(selectedMood,selectedEnergy),text=todayDraft===null?(m?.text||''):todayDraft;c.innerHTML=`
${new Date().toLocaleDateString('en-US',{weekday:'long'}).toUpperCase()}

${fmtDate(day()).replace(/^[^,]+, /,'')}

How was your day?

`}function filmFor(m,e){const dict=LifeI18n.dictionaries[lang()],seed=[...`${day()}|${m}|${e}`].reduce((sum,char,index)=>sum+char.charCodeAt(0)*(index+1),0),n=seed%dict.prompt.films.length;return{filmTitle:dict.prompt.films[n].title,filmSubtitle:dict.prompt.films[n].subtitle,filmLine:dict.prompt.lines[(n*3+e)%dict.prompt.lines.length]}}function rerenderToday(){let old=document.querySelector('#entry');todayDraft=old?old.value:todayDraft||'';window.today(document.querySelector('#content-today'));let entry=document.querySelector('#entry');entry?.focus();entry?.setSelectionRange(entry.value.length,entry.value.length)}function setMood(x){selectedMood=x;rerenderToday()}function setEnergy(x){selectedEnergy=x;rerenderToday()}function archiveToday(){let text=document.querySelector('#entry').value.trim(),a=mems(),old=a.find(x=>x.date===day()),f=old||filmFor(selectedMood,selectedEnergy),now=new Date().toISOString(),m={id:old?.id||'memory_'+crypto.randomUUID(),date:day(),text,mood:selectedMood,energy:selectedEnergy,filmTitle:f.filmTitle,filmSubtitle:f.filmSubtitle,filmLine:f.filmLine,createdAt:old?.createdAt||now,updatedAt:now};let status=document.querySelector('#saveStatus');status.textContent='ARCHIVING MEMORY...';setTimeout(()=>{save('lifeos_memories',[...a.filter(x=>x.date!==day()),m].sort((x,y)=>y.date.localeCompare(x.date)));todayDraft=null;status.textContent='MEMORY ARCHIVED';renderAll()},360)} @@ -23,6 +23,6 @@ function museumApp(c){let a=museum(),ms=mems();c.innerHTML=`
m.date===d.toISOString().slice(0,10))){s++;d.setDate(d.getDate()-1)}return s}function longest(a){let ds=a.map(x=>x.date).sort(),best=0,run=0,last='';ds.forEach(d=>{run=last&&((new Date(d)-new Date(last))/864e5===1)?run+1:1;best=Math.max(best,run);last=d});return best}function insights(c){let a=mems(),last=a.filter(m=>(Date.now()-new Date(m.date+'T12:00:00'))<2592e6),cnt={clear:0,light:0,cloudy:0,rain:0,storm:0};last.forEach(x=>cnt[x.mood]++);let avg=a.length?(a.reduce((s,x)=>s+x.energy,0)/a.length).toFixed(1):'—',days={};a.forEach(x=>{let d=new Date(x.date+'T12:00:00').toLocaleDateString('en-US',{weekday:'long'});days[d]=(days[d]||0)+1});let active=Object.entries(days).sort((x,y)=>y[1]-x[1])[0]?.[0]||'—';c.innerHTML=`
LAST 30 DAYS

Insights, quietly.

WEATHER
${Object.entries(cnt).map(([k,v])=>`
${cap(k)} ${v} DAYS
`).join('')}
AVERAGE ENERGY
${avg} / 5
${a.slice(0,10).reverse().map(x=>``).join('')||'No entries yet'}
MOST ACTIVE DAY
${active}
CURRENT STREAK
${streak(a)} DAYS
Longest streak: ${longest(a)} days
`} function profileApp(c){let p=profile(),a=mems(),common=Object.entries(a.reduce((o,m)=>(o[m.mood]=(o[m.mood]||0)+1,o),{})).sort((x,y)=>y[1]-x[1])[0]?.[0]||'—',avg=a.length?(a.reduce((x,m)=>x+m.energy,0)/a.length).toFixed(1):'—',started=Math.max(1,Math.floor((Date.now()-new Date(p.startDate))/864e5));c.innerHTML=`
SYSTEM PROFILE

${p.name?esc(p.name)+' · ':''}LIFE OS ONLINE

${started}DAYS ONLINE
${a.length}MEMORIES
${museum().length}MUSEUM ITEMS
${streak(a)}CURRENT STREAK
${longest(a)}LONGEST STREAK
${cap(common)}COMMON WEATHER
PERSONAL SYSTEM
AVERAGE ENERGY ${avg} / 5
`}function saveProfile(){save('lifeos_profile',{name:pname.value,birthday:pbday.value,startDate:pstart.value||day(),motto:pmotto.value});renderAll();toast('PROFILE SAVED')} function today(c){let m=mems().find(x=>x.date===day());if(todayDraft===null){selectedMood=m?.mood||'cloudy';selectedEnergy=m?.energy||3}let f=m||filmFor(selectedMood,selectedEnergy),text=todayDraft===null?(m?.text||''):todayDraft,cWeek=new Date().toLocaleDateString(LifeI18n.locale(),{weekday:'long'}).toUpperCase();c.innerHTML=`
${cWeek}

${fmtDate(day()).replace(/^[^,]+, /,'')}

${i18n('How was your day?')}

`}function rerenderToday(){let old=document.querySelector('#entry');todayDraft=old?old.value:todayDraft||'';window.today(document.querySelector('#content-today'));let entry=document.querySelector('#entry');entry?.focus();entry?.setSelectionRange(entry.value.length,entry.value.length)} -function systemApp(c){let s=settings(),bytes=new Blob([JSON.stringify({mems:mems(),museum:museum(),profile:profile()})]).size;let rec=s.lastBackup&&(Date.now()-new Date(s.lastBackup)>12096e5);c.innerHTML=`
SYSTEM SETTINGS

System, local and yours.

APPEARANCE
${['dark','light','auto'].map(x=>``).join('')}
LANGUAGE
DATA
STORAGE

Local Storage
${mems().length} memories · ${museum().length} museum items · Approx. ${Math.ceil(bytes/1024)} KB

DATA STORAGE

Your data is stored locally in this browser.
Create regular backups to prevent data loss.

${s.lastBackup?'LAST BACKUP · '+fmtDate(s.lastBackup.slice(0,10)):'LAST BACKUP · NEVER'} ${rec?'
BACKUP RECOMMENDED':''}

VERSION

Life OS
Version ${window.LifeOSVersion||'1.18.0'} · Schema v2

`}function theme(x){let s=settings();s.theme=x;save('lifeos_settings',s);init();renderAll()}function exportData(){let data={app:'Life OS',version:window.LifeOSVersion||'1.18.0',schemaVersion:1,exportedAt:new Date().toISOString(),profile:profile(),memories:mems(),museum:museum(),settings:settings()};let a=document.createElement('a');a.href=URL.createObjectURL(new Blob([JSON.stringify(data,null,2)],{type:'application/json'}));a.download='life-os-backup-'+day()+'.json';a.click();let s=settings();s.lastBackup=new Date().toISOString();save('lifeos_settings',s);renderAll();toast('BACKUP CREATED')}function readImport(el){let f=el.files[0];if(!f)return;let r=new FileReader;r.onload=()=>{try{let d=JSON.parse(r.result);if(d.app!=='Life OS'||![1,2].includes(d.schemaVersion)||!Array.isArray(d.memories)||!Array.isArray(d.museum))throw 0;importData=d;importPreview()}catch{toast('INVALID BACKUP FILE')}};r.readAsText(f)}function importPreview(){let d=importData;modal.innerHTML=``}function replaceConfirm(){modal.innerHTML=``}function applyImport(mode){let d=importData;if(mode==='replace'){save('lifeos_memories',d.memories);save('lifeos_museum',d.museum);save('lifeos_profile',d.profile||{});save('lifeos_settings',d.settings||{})}else{let a=mems(), ids=new Set(a.map(x=>x.id)),dates=new Set(a.map(x=>x.date));save('lifeos_memories',[...a,...d.memories.filter(x=>!ids.has(x.id)&&!dates.has(x.date))]);let mu=museum(),mi=new Set(mu.map(x=>x.id));save('lifeos_museum',[...mu,...d.museum.filter(x=>!mi.has(x.id))])}modal.innerHTML='';toast('RESTORING SYSTEM...');setTimeout(()=>{renderAll();toast('SYSTEM RESTORED')},500)}function resetConfirm(){modal.innerHTML=``}function toast(t){let x=document.createElement('div');x.className='toast';x.textContent=t;document.body.append(x);setTimeout(()=>x.remove(),2000)} +function systemApp(c){let s=settings(),bytes=new Blob([JSON.stringify({mems:mems(),museum:museum(),profile:profile()})]).size;let rec=s.lastBackup&&(Date.now()-new Date(s.lastBackup)>12096e5);c.innerHTML=`
SYSTEM SETTINGS

System, local and yours.

APPEARANCE
${['dark','light','auto'].map(x=>``).join('')}
LANGUAGE
DATA
STORAGE

Local Storage
${mems().length} memories · ${museum().length} museum items · Approx. ${Math.ceil(bytes/1024)} KB

DATA STORAGE

Your data is stored locally in this browser.
Create regular backups to prevent data loss.

${s.lastBackup?'LAST BACKUP · '+fmtDate(s.lastBackup.slice(0,10)):'LAST BACKUP · NEVER'} ${rec?'
BACKUP RECOMMENDED':''}

VERSION

Life OS
Version ${window.LifeOSVersion||'1.18.1'} · Schema v2

`}function theme(x){let s=settings();s.theme=x;save('lifeos_settings',s);init();renderAll()}function exportData(){let data={app:'Life OS',version:window.LifeOSVersion||'1.18.1',schemaVersion:1,exportedAt:new Date().toISOString(),profile:profile(),memories:mems(),museum:museum(),settings:settings()};let a=document.createElement('a');a.href=URL.createObjectURL(new Blob([JSON.stringify(data,null,2)],{type:'application/json'}));a.download='life-os-backup-'+day()+'.json';a.click();let s=settings();s.lastBackup=new Date().toISOString();save('lifeos_settings',s);renderAll();toast('BACKUP CREATED')}function readImport(el){let f=el.files[0];if(!f)return;let r=new FileReader;r.onload=()=>{try{let d=JSON.parse(r.result);if(d.app!=='Life OS'||![1,2].includes(d.schemaVersion)||!Array.isArray(d.memories)||!Array.isArray(d.museum))throw 0;importData=d;importPreview()}catch{toast('INVALID BACKUP FILE')}};r.readAsText(f)}function importPreview(){let d=importData;modal.innerHTML=``}function replaceConfirm(){modal.innerHTML=``}function applyImport(mode){let d=importData;if(mode==='replace'){save('lifeos_memories',d.memories);save('lifeos_museum',d.museum);save('lifeos_profile',d.profile||{});save('lifeos_settings',d.settings||{})}else{let a=mems(), ids=new Set(a.map(x=>x.id)),dates=new Set(a.map(x=>x.date));save('lifeos_memories',[...a,...d.memories.filter(x=>!ids.has(x.id)&&!dates.has(x.date))]);let mu=museum(),mi=new Set(mu.map(x=>x.id));save('lifeos_museum',[...mu,...d.museum.filter(x=>!mi.has(x.id))])}modal.innerHTML='';toast('RESTORING SYSTEM...');setTimeout(()=>{renderAll();toast('SYSTEM RESTORED')},500)}function resetConfirm(){modal.innerHTML=``}function toast(t){let x=document.createElement('div');x.className='toast';x.textContent=t;document.body.append(x);setTimeout(()=>x.remove(),2000)} window.renderIcons=renderIcons;window.renderAll=renderAll;window.toast=toast; - + diff --git a/public/locales/ar.json b/public/locales/ar.json index c3aa078..990de16 100644 --- a/public/locales/ar.json +++ b/public/locales/ar.json @@ -462,7 +462,14 @@ "pinOnCreate": "إظهار العد التنازلي على سطح المكتب", "todayHeading": "تركيز اليوم", "openCount": "{count} مهام مفتوحة", - "overdue": "متأخرة" + "overdue": "متأخرة", + "edit": "تعديل", + "editTask": "تعديل المهمة", + "editGoal": "تعديل الهدف", + "dueDate": "تاريخ الاستحقاق", + "priorityLabel": "الأولوية", + "taskUpdated": "تم تحديث المهمة", + "goalUpdated": "تم تحديث الهدف" }, "shell": { "pageTitle": "Life OS", diff --git a/public/locales/en.json b/public/locales/en.json index 83c955c..8b4b44f 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -462,7 +462,14 @@ "pinOnCreate": "Show countdown on desktop", "todayHeading": "Focus for today", "openCount": "{count} OPEN TASKS", - "overdue": "OVERDUE" + "overdue": "OVERDUE", + "edit": "EDIT", + "editTask": "Edit task", + "editGoal": "Edit goal", + "dueDate": "Due date", + "priorityLabel": "Priority", + "taskUpdated": "TASK UPDATED", + "goalUpdated": "GOAL UPDATED" }, "shell": { "pageTitle": "Life OS", diff --git a/public/locales/es.json b/public/locales/es.json index 176c55f..95ce5d5 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -462,7 +462,14 @@ "pinOnCreate": "Mostrar cuenta atrás en el escritorio", "todayHeading": "Prioridades de hoy", "openCount": "{count} TAREAS PENDIENTES", - "overdue": "ATRASADA" + "overdue": "ATRASADA", + "edit": "EDITAR", + "editTask": "Editar tarea", + "editGoal": "Editar objetivo", + "dueDate": "Fecha límite", + "priorityLabel": "Prioridad", + "taskUpdated": "TAREA ACTUALIZADA", + "goalUpdated": "OBJETIVO ACTUALIZADO" }, "shell": { "pageTitle": "Life OS", diff --git a/public/locales/fr.json b/public/locales/fr.json index 44ffdf7..1834d4e 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -462,7 +462,14 @@ "pinOnCreate": "Afficher le compte à rebours sur le bureau", "todayHeading": "Priorités du jour", "openCount": "{count} TÂCHES OUVERTES", - "overdue": "EN RETARD" + "overdue": "EN RETARD", + "edit": "MODIFIER", + "editTask": "Modifier la tâche", + "editGoal": "Modifier l’objectif", + "dueDate": "Date d’échéance", + "priorityLabel": "Priorité", + "taskUpdated": "TÂCHE MODIFIÉE", + "goalUpdated": "OBJECTIF MODIFIÉ" }, "shell": { "pageTitle": "Life OS", diff --git a/public/locales/ja.json b/public/locales/ja.json index 1b2bf6e..36d8a72 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -462,7 +462,14 @@ "pinOnCreate": "デスクトップに表示", "todayHeading": "今日の重点", "openCount": "未完了タスク {count} 件", - "overdue": "期限超過" + "overdue": "期限超過", + "edit": "編集", + "editTask": "タスクを編集", + "editGoal": "目標を編集", + "dueDate": "期限", + "priorityLabel": "優先度", + "taskUpdated": "タスクを更新しました", + "goalUpdated": "目標を更新しました" }, "shell": { "pageTitle": "Life OS", diff --git a/public/locales/ru.json b/public/locales/ru.json index b867cee..064df71 100644 --- a/public/locales/ru.json +++ b/public/locales/ru.json @@ -462,7 +462,14 @@ "pinOnCreate": "Показывать отсчёт на рабочем столе", "todayHeading": "Главное на сегодня", "openCount": "ОТКРЫТЫХ ЗАДАЧ: {count}", - "overdue": "ПРОСРОЧЕНО" + "overdue": "ПРОСРОЧЕНО", + "edit": "ИЗМЕНИТЬ", + "editTask": "Изменить задачу", + "editGoal": "Изменить цель", + "dueDate": "Срок выполнения", + "priorityLabel": "Приоритет", + "taskUpdated": "ЗАДАЧА ОБНОВЛЕНА", + "goalUpdated": "ЦЕЛЬ ОБНОВЛЕНА" }, "shell": { "pageTitle": "Life OS", diff --git a/public/locales/zh-CN.json b/public/locales/zh-CN.json index dbc46b2..24a4ecd 100644 --- a/public/locales/zh-CN.json +++ b/public/locales/zh-CN.json @@ -462,7 +462,14 @@ "pinOnCreate": "在桌面显示倒计时", "todayHeading": "今天要推进的事", "openCount": "还有 {count} 项待办", - "overdue": "已逾期" + "overdue": "已逾期", + "edit": "编辑", + "editTask": "编辑待办", + "editGoal": "编辑目标", + "dueDate": "截止日期", + "priorityLabel": "优先级", + "taskUpdated": "待办已更新", + "goalUpdated": "目标已更新" }, "shell": { "pageTitle": "Life OS / 人生操作系统", diff --git a/public/locales/zh-HK.json b/public/locales/zh-HK.json index d2b36fc..b7d59eb 100644 --- a/public/locales/zh-HK.json +++ b/public/locales/zh-HK.json @@ -462,7 +462,14 @@ "pinOnCreate": "在桌面顯示倒計時", "todayHeading": "今天要推進的事", "openCount": "還有 {count} 項待辦", - "overdue": "已逾期" + "overdue": "已逾期", + "edit": "編輯", + "editTask": "編輯待辦", + "editGoal": "編輯目標", + "dueDate": "截止日期", + "priorityLabel": "優先次序", + "taskUpdated": "待辦已更新", + "goalUpdated": "目標已更新" }, "shell": { "pageTitle": "Life OS / 人生操作系統", diff --git a/public/locales/zh-MO.json b/public/locales/zh-MO.json index 6d44b2e..72dcea5 100644 --- a/public/locales/zh-MO.json +++ b/public/locales/zh-MO.json @@ -462,7 +462,14 @@ "pinOnCreate": "在桌面顯示倒計時", "todayHeading": "今天要推進的事", "openCount": "還有 {count} 項待辦", - "overdue": "已逾期" + "overdue": "已逾期", + "edit": "編輯", + "editTask": "編輯待辦", + "editGoal": "編輯目標", + "dueDate": "截止日期", + "priorityLabel": "優先次序", + "taskUpdated": "待辦已更新", + "goalUpdated": "目標已更新" }, "shell": { "pageTitle": "Life OS / 人生操作系統", diff --git a/public/locales/zh-TW.json b/public/locales/zh-TW.json index dc2a432..925e6d4 100644 --- a/public/locales/zh-TW.json +++ b/public/locales/zh-TW.json @@ -462,7 +462,14 @@ "pinOnCreate": "在桌面顯示倒計時", "todayHeading": "今天要推進的事", "openCount": "還有 {count} 項待辦", - "overdue": "已逾期" + "overdue": "已逾期", + "edit": "編輯", + "editTask": "編輯待辦", + "editGoal": "編輯目標", + "dueDate": "截止日期", + "priorityLabel": "優先順序", + "taskUpdated": "待辦已更新", + "goalUpdated": "目標已更新" }, "shell": { "pageTitle": "Life OS / 人生作業系統", diff --git a/public/service-worker.js b/public/service-worker.js index 2c40786..92bdd0d 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const CACHE = 'life-os-shell-v1.18.0'; +const CACHE = 'life-os-shell-v1.18.1'; const SHELL = [ '/', '/index.html', '/manifest.webmanifest', '/life-os-icon.svg' ]; diff --git a/public/system-navigation.js b/public/system-navigation.js index 4aa102b..a71362c 100644 --- a/public/system-navigation.js +++ b/public/system-navigation.js @@ -26,7 +26,7 @@ shell.innerHTML = `
${t('settings')}

${t(active)}

${active === 'home' ? `

${t('homeHint')}

` : ''}
`; const view = shell.querySelector('.system-settings-view'); if (active === 'home') { - view.innerHTML = `
${t('device')}

${t('deviceHint')}

${t('version', { version: window.LifeOSVersion || '1.18.0' })}
${t('quickSettings')}
${['appearance', 'notifications', 'language', 'data'].map(id => `
${navigationButton(id, true)}

${t(`${id}Hint`)}

`).join('')}
`; + view.innerHTML = `
${t('device')}

${t('deviceHint')}

${t('version', { version: window.LifeOSVersion || '1.18.1' })}
${t('quickSettings')}
${['appearance', 'notifications', 'language', 'data'].map(id => `
${navigationButton(id, true)}

${t(`${id}Hint`)}

`).join('')}
`; } else { const group = document.createElement('div'); group.className = 'settings system-settings-group'; diff --git a/public/tasks.js b/public/tasks.js index f9c0920..2ce78b6 100644 --- a/public/tasks.js +++ b/public/tasks.js @@ -29,12 +29,12 @@
${visible.length ? visible.map(task => { const goal = data.goals.find(item => item.id === task.goalId); - return `
${esc(task.title)}
${t(`priority.${task.priority}`)}${goal ? `${esc(goal.title)}` : ''}${isOverdue(task) ? `${t('overdue')} · ` : ''}${task.dueDate ? t('due', { date: dateLabel(task.dueDate) }) : t('noDate')}
`; + return `
${esc(task.title)}
${t(`priority.${task.priority}`)}${goal ? `${esc(goal.title)}` : ''}${isOverdue(task) ? `${t('overdue')} · ` : ''}${task.dueDate ? t('due', { date: dateLabel(task.dueDate) }) : t('noDate')}
`; }).join('') : `
○

${t('noTasks')}

`}
${t('activeGoals')}
${activeGoals.length}
${activeGoals.length ? `
${activeGoals.map(goal => { const linked = data.tasks.filter(task => task.goalId === goal.id), completed = linked.filter(task => task.done).length, progress = linked.length ? Math.round(completed / linked.length * 100) : 0; - return `
◎
${goal.targetDate ? `` : ''}

${esc(goal.title)}

${goal.description ? `

${esc(goal.description)}

` : ''}
${t('progress', { done: completed, total: linked.length })}${progress}%
${goal.targetDate ? `
${t('target')} · ${dateLabel(goal.targetDate)}
` : ''}
`; - }).join('')}
` : `
${t('noGoals')}
`}${finishedGoals.length ? `
${t('finishedGoals')} · ${finishedGoals.length}
${finishedGoals.map(goal => `
✓

${esc(goal.title)}

`).join('')}
` : ''}
`; + return `
◎
${goal.targetDate ? `` : ''}

${esc(goal.title)}

${goal.description ? `

${esc(goal.description)}

` : ''}
${t('progress', { done: completed, total: linked.length })}${progress}%
${goal.targetDate ? `
${t('target')} · ${dateLabel(goal.targetDate)}
` : ''}
`; + }).join('')}
` : `
${t('noGoals')}
`}${finishedGoals.length ? `
${t('finishedGoals')} · ${finishedGoals.length}
${finishedGoals.map(goal => `
✓

${esc(goal.title)}

`).join('')}
` : ''}`; } window.tasksApp = renderTasks; @@ -47,14 +47,27 @@ }; window.toggleTask = id => { const task = board().tasks.find(item => item.id === id); if (task) window.LifeOSActions.setTaskDone(id, !task.done); }; window.deleteTask = id => { window.LifeOSActions.deleteTask(id); toast('tasks.taskRemoved'); }; - window.openGoalForm = () => { - document.querySelector('#modal').innerHTML = ``; + window.openTaskEdit = id => { + const task = board().tasks.find(item => item.id === id); if (!task) return; + const goals = board().goals.map(goal => ``).join(''); + document.querySelector('#modal').innerHTML = ``; + setTimeout(() => document.querySelector('#task-edit-title-input')?.focus(), 0); + }; + window.saveTaskEdit = event => { + event.preventDefault(); const form = event.currentTarget, id = form.dataset.id; + window.LifeOSActions.updateTask(id, { title: form.querySelector('#task-edit-title-input').value, dueDate: form.querySelector('#task-edit-date').value, goalId: form.querySelector('#task-edit-goal').value, priority: form.querySelector('#task-edit-priority').value }); + closeModal(); toast('tasks.taskUpdated'); + }; + window.openGoalForm = (id = '') => { + const goal = id ? board().goals.find(item => item.id === id) : null; if (id && !goal) return; + document.querySelector('#modal').innerHTML = ``; setTimeout(() => document.querySelector('#goal-new-title')?.focus(), 0); }; window.saveGoal = event => { - event.preventDefault(); const title = document.querySelector('#goal-new-title').value.trim(); if (!title) return; - window.LifeOSActions.createGoal({ title, description: document.querySelector('#goal-new-description').value.trim(), targetDate: document.querySelector('#goal-new-date').value || '', showOnDesktop: document.querySelector('#goal-new-desktop').checked }); - document.querySelector('#modal').innerHTML = ''; toast('tasks.goalCreated'); + event.preventDefault(); const form = event.currentTarget, title = form.querySelector('#goal-new-title').value.trim(); if (!title) return; + const values = { title, description: form.querySelector('#goal-new-description').value, targetDate: form.querySelector('#goal-new-date').value, showOnDesktop: form.querySelector('#goal-new-desktop').checked }; + if (form.dataset.id) window.LifeOSActions.updateGoal(form.dataset.id, values); else window.LifeOSActions.createGoal(values); + closeModal(); toast(form.dataset.id ? 'tasks.goalUpdated' : 'tasks.goalCreated'); }; window.toggleGoalDesktop = id => { const data = board(), goal = data.goals.find(item => item.id === id); if (!goal || !goal.targetDate) return; goal.showOnDesktop = !goal.showOnDesktop; persist(data); }; window.renderDesktopGoalCountdowns = () => { const container = document.querySelector('#desktop-goal-countdowns'); if (!container) return; const goals = board().goals.filter(goal => !goal.done && goal.targetDate && goal.showOnDesktop); if (!goals.length) { container.innerHTML = ''; container.classList.add('hidden'); return; } container.classList.remove('hidden'); const now = new Date(), todayUtc = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()); container.innerHTML = `
${t('countdown')}
${goals.map(goal => { const [year, month, date] = goal.targetDate.split('-').map(Number), delta = Math.round((Date.UTC(year, month - 1, date) - todayUtc) / 864e5), count = Math.abs(delta), label = delta === 0 ? t('todayTarget') : delta > 0 ? t('daysLeft', { count }) : t('daysOverdue', { count }); return ``; }).join('')}`; }; @@ -66,6 +79,6 @@ window.confirmDeleteGoal = id => { window.LifeOSActions.deleteGoal(id); document.querySelector('#modal').innerHTML = ''; toast('tasks.goalRemoved'); }; const style = document.createElement('style'); - style.textContent = `.task-head{display:flex;align-items:flex-start;justify-content:space-between;gap:14px}.task-head h1{max-width:430px}.task-head .secondary{white-space:nowrap}.task-focus{display:flex;align-items:center;justify-content:space-between;margin:18px 0;padding:18px 20px;background:linear-gradient(115deg,color-mix(in srgb,var(--accent) 15%,var(--panel2)),var(--panel2));border:1px solid var(--line);border-radius:13px}.task-focus .eyebrow{margin-bottom:5px}.task-focus strong{font-size:30px;letter-spacing:-.06em}.task-focus span{margin-left:10px;color:var(--muted);font-size:12px}.task-focus-mark{font-size:26px;color:var(--accent)}.task-quick{display:grid;grid-template-columns:minmax(130px,1fr) 145px 120px 95px auto;gap:7px}.task-quick input,.task-quick select,.goal-form input,.goal-form textarea{min-width:0;border:1px solid var(--line);background:var(--panel2);border-radius:8px;padding:10px 11px;outline:none}.task-quick input:focus,.task-quick select:focus,.goal-form input:focus,.goal-form textarea:focus{border-color:var(--accent)}.task-quick select{font-size:12px}.task-quick .primary{padding:10px 11px;white-space:nowrap}.task-filters{display:flex;gap:7px;flex-wrap:wrap;margin:19px 0 8px}.task-filters .chip{padding:7px 12px}.task-list{display:grid;gap:7px}.task-row{display:flex;align-items:center;gap:12px;padding:12px;background:color-mix(in srgb,var(--panel2) 55%,transparent);border:1px solid var(--line);border-radius:10px}.task-check{flex:none;width:21px;height:21px;border:1px solid var(--muted);border-radius:7px;background:transparent;color:var(--accent);font-weight:700}.task-check[aria-pressed=true]{background:var(--accent);border-color:var(--accent);color:var(--bg)}.task-copy{min-width:0;flex:1}.task-copy>strong{display:block;font-size:13px;font-weight:550;overflow-wrap:anywhere}.task-meta{display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin-top:5px;color:var(--muted);font-size:10px}.task-meta span{display:inline-flex;align-items:center;gap:4px}.task-meta b,.is-overdue .task-meta{color:#df8a70}.task-priority{font:9px 'DM Mono';letter-spacing:.05em}.task-priority.high{color:#df8a70}.task-priority.low{color:var(--muted)}.is-done .task-copy>strong{text-decoration:line-through;color:var(--muted)}.task-delete{width:28px;height:28px;border:0;border-radius:7px;background:transparent;color:var(--muted);font-size:19px;line-height:1}.task-delete:hover{background:var(--panel2);color:var(--ink)}.task-empty,.goal-empty{padding:21px;text-align:center;color:var(--muted);font-size:13px}.task-empty span{font-size:23px;color:var(--accent)}.goal-section{margin-top:24px;padding-top:16px;border-top:1px solid var(--line)}.goal-section-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}.goal-section-head .eyebrow{margin:0}.goal-count{font:11px 'DM Mono';color:var(--muted)}.goal-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px}.goal-card{min-width:0;padding:14px;border:1px solid var(--line);border-radius:11px;background:color-mix(in srgb,var(--panel2) 55%,transparent)}.goal-card-top{display:flex;justify-content:space-between;align-items:center}.goal-actions{display:flex;align-items:center;gap:4px;flex-wrap:wrap;justify-content:flex-end}.goal-desktop-toggle{border:1px solid var(--line);border-radius:7px;background:transparent;color:var(--muted);font:9px 'DM Mono';padding:6px 8px;white-space:nowrap}.goal-desktop-toggle.selected{color:var(--accent);border-color:var(--accent)}.goal-form label.goal-pin-option{display:flex!important;grid-template-columns:18px 1fr;align-items:center;gap:8px!important}.goal-form label.goal-pin-option input{width:16px;height:16px;min-width:16px;padding:0;accent-color:var(--accent)}.goal-finish{border:0;background:transparent;color:var(--muted);font:9px 'DM Mono';letter-spacing:.04em;padding:5px}.goal-finish:hover{color:var(--accent)}.finished-goals{margin-top:14px}.finished-goals summary{cursor:pointer;color:var(--muted);font:10px 'DM Mono';letter-spacing:.08em;margin-bottom:9px}.is-goal-done{opacity:.72}.goal-mark{font-size:20px;color:var(--accent)}.goal-card h3{margin:8px 0 4px;font-size:15px;letter-spacing:-.025em;overflow-wrap:anywhere}.goal-card p{margin:0 0 12px;color:var(--muted);font-size:11px;line-height:1.5}.goal-progress-line{display:flex;justify-content:space-between;gap:8px;margin-top:11px;font:9px 'DM Mono';color:var(--muted)}.goal-progress{height:4px;background:var(--line);border-radius:5px;margin-top:5px;overflow:hidden}.goal-progress i{display:block;height:100%;background:var(--accent);border-radius:5px}.goal-date{margin-top:10px;color:var(--muted);font-size:10px}.goal-form{display:grid;gap:13px}.goal-form h2{font-size:21px;margin-bottom:0}.goal-form label{display:grid;gap:6px;color:var(--muted);font-size:12px}.goal-form textarea{resize:vertical}.goal-form .actions{margin:0}.desktop-goal-countdowns{position:absolute;z-index:1;top:24px;right:28px;width:min(310px,30vw);max-height:72vh;overflow:auto;display:grid;gap:8px}.desktop-goal-countdowns.hidden{display:none}.desktop-goal-heading{color:var(--muted);font:10px 'DM Mono';letter-spacing:.15em;text-align:right}.desktop-goal-card{display:grid;gap:5px;width:100%;padding:13px 15px;text-align:left;background:color-mix(in srgb,var(--panel) 88%,transparent);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow);backdrop-filter:blur(12px)}.desktop-goal-card span{font-size:13px;font-weight:600;overflow-wrap:anywhere}.desktop-goal-card strong{color:var(--accent);font:500 13px 'DM Mono'}.desktop-goal-card small{color:var(--muted);font-size:11px}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media(max-width:650px){.desktop-goal-countdowns{position:relative;top:auto;right:auto;width:auto;max-width:none;max-height:30vh;margin:12px 16px 92px;overflow:auto;grid-template-columns:repeat(2,minmax(0,1fr));align-items:start}.desktop-goal-heading{grid-column:1/-1;text-align:left}.desktop-icons{padding:76px 14px 18px;display:flex;flex-wrap:wrap;justify-content:flex-start;gap:16px 4px}.app-icon{flex:0 0 calc(25% - 4px);min-width:0;justify-items:center;font-size:10px;letter-spacing:.04em}.desktop-goal-card{padding:11px 12px}.desktop-goal-card span{font-size:12px}}@media(min-width:651px){.desktop-icons{padding:72px 32px;display:grid;grid-template-columns:96px;justify-items:start;align-content:start;gap:14px}.app-icon{width:96px;justify-items:center;font-size:11px}.desktop-goal-countdowns{top:26px;right:30px;width:min(330px,28vw);max-height:calc(100vh - 110px)}}@media(max-width:700px){.task-quick{grid-template-columns:1fr 1fr}.task-quick input:first-of-type{grid-column:1/-1}.task-quick .primary{grid-column:1/-1}.task-head{align-items:flex-start}.task-head .secondary{font-size:9px;padding:9px 10px}.goal-grid{grid-template-columns:1fr}.task-meta{gap:6px}.task-focus{padding:14px}.task-focus span{display:block;margin:3px 0 0}}`; + style.textContent = `.task-head{display:flex;align-items:flex-start;justify-content:space-between;gap:14px}.task-head h1{max-width:430px}.task-head .secondary{white-space:nowrap}.task-focus{display:flex;align-items:center;justify-content:space-between;margin:18px 0;padding:18px 20px;background:linear-gradient(115deg,color-mix(in srgb,var(--accent) 15%,var(--panel2)),var(--panel2));border:1px solid var(--line);border-radius:13px}.task-focus .eyebrow{margin-bottom:5px}.task-focus strong{font-size:30px;letter-spacing:-.06em}.task-focus span{margin-left:10px;color:var(--muted);font-size:12px}.task-focus-mark{font-size:26px;color:var(--accent)}.task-quick{display:grid;grid-template-columns:minmax(130px,1fr) 145px 120px 95px auto;gap:7px}.task-quick input,.task-quick select,.goal-form input,.goal-form textarea,.goal-form select{min-width:0;border:1px solid var(--line);background:var(--panel2);border-radius:8px;padding:10px 11px;outline:none}.task-quick input:focus,.task-quick select:focus,.goal-form input:focus,.goal-form textarea:focus,.goal-form select:focus{border-color:var(--accent)}.task-quick select{font-size:12px}.task-quick .primary{padding:10px 11px;white-space:nowrap}.task-filters{display:flex;gap:7px;flex-wrap:wrap;margin:19px 0 8px}.task-filters .chip{padding:7px 12px}.task-list{display:grid;gap:7px}.task-row{display:flex;align-items:center;gap:12px;padding:12px;background:color-mix(in srgb,var(--panel2) 55%,transparent);border:1px solid var(--line);border-radius:10px}.task-check{flex:none;width:21px;height:21px;border:1px solid var(--muted);border-radius:7px;background:transparent;color:var(--accent);font-weight:700}.task-check[aria-pressed=true]{background:var(--accent);border-color:var(--accent);color:var(--bg)}.task-copy{min-width:0;flex:1}.task-copy>strong{display:block;font-size:13px;font-weight:550;overflow-wrap:anywhere}.task-meta{display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin-top:5px;color:var(--muted);font-size:10px}.task-meta span{display:inline-flex;align-items:center;gap:4px}.task-meta b,.is-overdue .task-meta{color:#df8a70}.task-priority{font:9px 'DM Mono';letter-spacing:.05em}.task-priority.high{color:#df8a70}.task-priority.low{color:var(--muted)}.is-done .task-copy>strong{text-decoration:line-through;color:var(--muted)}.task-edit{border:1px solid var(--line);border-radius:7px;background:transparent;color:var(--muted);font:10px 'DM Mono';padding:6px 8px;white-space:nowrap}.task-edit:hover,.task-edit:focus-visible{color:var(--accent);border-color:var(--accent)}.task-delete{width:28px;height:28px;border:0;border-radius:7px;background:transparent;color:var(--muted);font-size:19px;line-height:1}.task-delete:hover{background:var(--panel2);color:var(--ink)}.task-empty,.goal-empty{padding:21px;text-align:center;color:var(--muted);font-size:13px}.task-empty span{font-size:23px;color:var(--accent)}.goal-section{margin-top:24px;padding-top:16px;border-top:1px solid var(--line)}.goal-section-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}.goal-section-head .eyebrow{margin:0}.goal-count{font:11px 'DM Mono';color:var(--muted)}.goal-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px}.goal-card{min-width:0;padding:14px;border:1px solid var(--line);border-radius:11px;background:color-mix(in srgb,var(--panel2) 55%,transparent)}.goal-card-top{display:flex;justify-content:space-between;align-items:center}.goal-actions{display:flex;align-items:center;gap:4px;flex-wrap:wrap;justify-content:flex-end}.goal-desktop-toggle{border:1px solid var(--line);border-radius:7px;background:transparent;color:var(--muted);font:9px 'DM Mono';padding:6px 8px;white-space:nowrap}.goal-desktop-toggle.selected{color:var(--accent);border-color:var(--accent)}.goal-form label.goal-pin-option{display:flex!important;grid-template-columns:18px 1fr;align-items:center;gap:8px!important}.goal-form label.goal-pin-option input{width:16px;height:16px;min-width:16px;padding:0;accent-color:var(--accent)}.goal-finish{border:0;background:transparent;color:var(--muted);font:9px 'DM Mono';letter-spacing:.04em;padding:5px}.goal-finish:hover{color:var(--accent)}.finished-goals{margin-top:14px}.finished-goals summary{cursor:pointer;color:var(--muted);font:10px 'DM Mono';letter-spacing:.08em;margin-bottom:9px}.is-goal-done{opacity:.72}.goal-mark{font-size:20px;color:var(--accent)}.goal-card h3{margin:8px 0 4px;font-size:15px;letter-spacing:-.025em;overflow-wrap:anywhere}.goal-card p{margin:0 0 12px;color:var(--muted);font-size:11px;line-height:1.5}.goal-progress-line{display:flex;justify-content:space-between;gap:8px;margin-top:11px;font:9px 'DM Mono';color:var(--muted)}.goal-progress{height:4px;background:var(--line);border-radius:5px;margin-top:5px;overflow:hidden}.goal-progress i{display:block;height:100%;background:var(--accent);border-radius:5px}.goal-date{margin-top:10px;color:var(--muted);font-size:10px}.goal-form{display:grid;gap:13px}.goal-form h2{font-size:21px;margin-bottom:0}.goal-form label{display:grid;gap:6px;color:var(--muted);font-size:12px}.goal-form textarea{resize:vertical}.goal-form .actions{margin:0}.desktop-goal-countdowns{position:absolute;z-index:1;top:24px;right:28px;width:min(310px,30vw);max-height:72vh;overflow:auto;display:grid;gap:8px}.desktop-goal-countdowns.hidden{display:none}.desktop-goal-heading{color:var(--muted);font:10px 'DM Mono';letter-spacing:.15em;text-align:right}.desktop-goal-card{display:grid;gap:5px;width:100%;padding:13px 15px;text-align:left;background:color-mix(in srgb,var(--panel) 88%,transparent);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow);backdrop-filter:blur(12px)}.desktop-goal-card span{font-size:13px;font-weight:600;overflow-wrap:anywhere}.desktop-goal-card strong{color:var(--accent);font:500 13px 'DM Mono'}.desktop-goal-card small{color:var(--muted);font-size:11px}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media(max-width:650px){.desktop-goal-countdowns{position:relative;top:auto;right:auto;width:auto;max-width:none;max-height:30vh;margin:12px 16px 92px;overflow:auto;grid-template-columns:repeat(2,minmax(0,1fr));align-items:start}.desktop-goal-heading{grid-column:1/-1;text-align:left}.desktop-icons{padding:76px 14px 18px;display:flex;flex-wrap:wrap;justify-content:flex-start;gap:16px 4px}.app-icon{flex:0 0 calc(25% - 4px);min-width:0;justify-items:center;font-size:10px;letter-spacing:.04em}.desktop-goal-card{padding:11px 12px}.desktop-goal-card span{font-size:12px}}@media(min-width:651px){.desktop-icons{padding:72px 32px;display:grid;grid-template-columns:96px;justify-items:start;align-content:start;gap:14px}.app-icon{width:96px;justify-items:center;font-size:11px}.desktop-goal-countdowns{top:26px;right:30px;width:min(330px,28vw);max-height:calc(100vh - 110px)}}@media(max-width:700px){.task-quick{grid-template-columns:1fr 1fr}.task-quick input:first-of-type{grid-column:1/-1}.task-quick .primary{grid-column:1/-1}.task-head{align-items:flex-start}.task-head .secondary{font-size:9px;padding:9px 10px}.goal-grid{grid-template-columns:1fr}.task-meta{gap:6px}.task-focus{padding:14px}.task-focus span{display:block;margin:3px 0 0}}`; document.head.append(style); })(); diff --git a/public/terminal-app.js b/public/terminal-app.js index a411fc9..b6833ff 100644 --- a/public/terminal-app.js +++ b/public/terminal-app.js @@ -150,7 +150,7 @@ return { message: new Intl.DateTimeFormat(LifeI18n.locale(), options).format(new Date()) }; } if (kind === 'uptime') { if (verb) throw new Error('usage'); const seconds = Math.floor((Date.now() - startedAt) / 1000); return { message: t('uptime', { minutes: Math.floor(seconds / 60), seconds: seconds % 60 }) }; } - if (kind === 'version') { if (verb) throw new Error('usage'); return { message: `Life OS v${window.LifeOSVersion || '1.18.0'}` }; } + if (kind === 'version') { if (verb) throw new Error('usage'); return { message: `Life OS v${window.LifeOSVersion || '1.18.1'}` }; } if (kind === 'status') { if (verb) throw new Error('usage'); const data = board(); return { message: t('status', { memories: mems().length, goals: data.goals.length, tasks: data.tasks.length }) }; } if (kind === 'apps') { if (verb) throw new Error('usage'); return { message: Object.keys(APPS).map(id => `${id} — ${LifeI18n.t(`app.${id}`)}`).join('\n') }; } if (kind === 'history') { if (verb) throw new Error('usage'); return { message: history.slice(-20).map((entry,index) => `${index + 1}. ${entry}`).join('\n') || t('empty') }; } @@ -235,7 +235,7 @@ }; window.terminalApp = node => { if (node.querySelector('.terminal-shell') && node.dataset.terminalLocale === lang()) { output(node); return; } - if (!welcomed) { lines.unshift({ value: t('welcome', { version: window.LifeOSVersion || '1.18.0' }), kind: 'system' }); welcomed = true; } + if (!welcomed) { lines.unshift({ value: t('welcome', { version: window.LifeOSVersion || '1.18.1' }), kind: 'system' }); welcomed = true; } node.dataset.terminalLocale = lang(); node.innerHTML = `
Life OS / ${t('title')}${t('hint')}
`; output(node); composer(node, Boolean(compose)); diff --git a/scripts/build.mjs b/scripts/build.mjs index 3db68b5..fa17d29 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -8,7 +8,7 @@ const meta = new URL("../dist/.openai/", import.meta.url); await rm(dist, { recursive: true, force: true }); await Promise.all([mkdir(server, { recursive: true }), mkdir(meta, { recursive: true })]); const [rawIndexHtml, addonsJs, bootGuardJs, i18nJs, tasksJs, calendarJs, notificationsJs, i18nRenderersJs, desktopPersonalizationJs, desktopWidgetsJs, systemNavigationJs, browserAppJs, dataActionsJs, terminalAppJs, dbAdminJs, manifest, serviceWorker, icon, localeEn, localeZh] = await Promise.all([text("index.html"), text("addons.js"), text("boot-guard.js"), text("i18n.js"), text("tasks.js"), text("calendar.js"), text("notifications.js"), text("i18n-renderers.js"), text("desktop-personalization.js"), text("desktop-widgets.js"), text("system-navigation.js"), text("browser-app.js"), text("data-actions.js"), text("terminal-app.js"), text("db-admin.js"), text("manifest.webmanifest"), text("service-worker.js"), text("life-os-icon.svg"), readFile(new URL("../public/locales/en.json", import.meta.url), "utf8"), readFile(new URL("../public/locales/zh-CN.json", import.meta.url), "utf8")]); -const release = "1.18.0"; +const release = "1.18.1"; const localeEnJs = `window.LifeOSLocales=window.LifeOSLocales||{};window.LifeOSLocales.en=${localeEn};`; const localeZhJs = `window.LifeOSLocales=window.LifeOSLocales||{};window.LifeOSLocales['zh-CN']=${localeZh};`; const extraLocaleCodes = ['zh-HK', 'zh-MO', 'zh-TW', 'ar', 'fr', 'ru', 'es', 'ja']; diff --git a/scripts/validate.mjs b/scripts/validate.mjs index d23c841..e3e255f 100644 --- a/scripts/validate.mjs +++ b/scripts/validate.mjs @@ -70,6 +70,57 @@ assert.doesNotMatch(assets.indexHtml, / ({}), head: { append() {} }, querySelector: selector => selector === '#modal' ? editModal : null }, + LifeI18n: { t: key => key, locale: () => 'en-US' }, + sys: () => editState, save: (key, value) => { if (key === 'lifeos_system') Object.assign(editState, structuredClone(value)); }, + renderAll: () => {}, closeModal: () => { editModal.innerHTML = ''; }, toast: value => editToasts.push(value), + day: () => '2026-10-02', setInterval: () => 0, setTimeout: () => 0, crypto: globalThis.crypto, Date +}); +editContext.window = editContext; +vm.runInContext(assets.dataActionsJs, editContext); +vm.runInContext(assets.tasksJs, editContext); +editContext.tasksApp(editContent); +assert.match(editContent.innerHTML, /openTaskEdit\(this\.dataset\.id\)/); +assert.match(editContent.innerHTML, /openGoalForm\(this\.dataset\.id\)/); +editContext.setTaskFilter('done'); +editContext.tasksApp(editContent); +assert.match(editContent.innerHTML, /Finished task/); +assert.match(editContent.innerHTML, /openTaskEdit\(this\.dataset\.id\)/, 'completed tasks have an edit button'); +editContext.openTaskEdit('t1'); +assert.match(editModal.innerHTML, /value="Original task"/); +assert.match(editModal.innerHTML, /value="2026-10-10"/); +const taskFields = { '#task-edit-title-input': 'Updated task', '#task-edit-date': '2026-10-12', '#task-edit-goal': 'g2', '#task-edit-priority': 'high' }; +editContext.saveTaskEdit({ preventDefault() {}, currentTarget: { dataset: { id: 't1' }, querySelector: selector => ({ value: taskFields[selector] }) } }); +assert.equal(editState.tasksBoard.tasks[0].title, 'Updated task'); +assert.equal(editState.tasksBoard.tasks[0].goalId, 'g2'); +assert.equal(editState.tasksBoard.tasks[0].priority, 'high'); +assert.equal(editState.tasksBoard.tasks[0].dueDate, '2026-10-12'); +assert.equal(editState.tasksBoard.tasks[0].createdAt, '2026-09-02T00:00:00Z'); +assert.equal(editState.tasksBoard.tasks[0].done, false); +assert.throws(() => editContext.LifeOSActions.updateTask('t1', { title: 'No link', goalId: 'missing' }), /notFound/); +assert.equal(editState.tasksBoard.tasks[0].goalId, 'g2', 'invalid edits do not write changes'); +editContext.openGoalForm('g1'); +assert.match(editModal.innerHTML, /value="Original goal"/); +assert.match(editModal.innerHTML, /checked/); +const goalFields = { '#goal-new-title': { value: 'Updated goal' }, '#goal-new-description': { value: 'New note' }, '#goal-new-date': { value: '' }, '#goal-new-desktop': { checked: true } }; +editContext.saveGoal({ preventDefault() {}, currentTarget: { dataset: { id: 'g1' }, querySelector: selector => goalFields[selector] } }); +assert.equal(editState.tasksBoard.goals[0].title, 'Updated goal'); +assert.equal(editState.tasksBoard.goals[0].description, 'New note'); +assert.equal(editState.tasksBoard.goals[0].showOnDesktop, false, 'removing the target date also removes the countdown'); +assert.equal(editState.tasksBoard.goals[0].createdAt, '2026-09-01T00:00:00Z'); +assert.equal(editState.tasksBoard.tasks[0].goalId, 'g2', 'editing a goal keeps task links'); +editContext.openGoalForm('g2'); +assert.match(editModal.innerHTML, /value="Finished goal"/, 'finished goals remain editable'); +assert.deepEqual(editToasts, ['tasks.taskUpdated', 'tasks.goalUpdated']); const reminderStorage = new Map(), delivered = []; reminderStorage.set('lifeos_notifications_v1', JSON.stringify(Object.fromEntries(['memory', 'tasks', 'goals'].map(kind => [kind, { enabled: true, time: '12:00' }])))); const reminderContext = vm.createContext({ @@ -143,7 +194,7 @@ assert.equal(terminalStorage.get('lifeos_system').tasksBoard.tasks[0].done, true assert.equal(command('memory add "Experiment" --date 2026-09-28 --tag school --energy 4 --text "Finished lab"').message, 'terminal.memorySaved'); assert.equal(terminalStorage.get('lifeos_memories')[0].text, 'Finished lab'); assert.equal(terminalStorage.get('lifeos_memories')[0].tags[0], 'school'); -assert.throws(() => command('memory add "Again" --text "Duplicate"'), /memoryExists/); +assert.throws(() => command('memory add "Again" --date 2026-09-28 --text "Duplicate"'), /memoryExists/); assert.throws(() => command('goal add "Invalid date" --due 2026-02-30'), /invalidDate/); assert.match(command('memory add "Multiline" --date 2026-09-29').message, /terminal\.wizard\.tags/); assert.match(command('back').message, /terminal\.wizard\.date/); @@ -218,7 +269,7 @@ assert.ok(command('date').message.length > 8); assert.ok(command('time').message.includes(':')); assert.ok(command('datetime').message.length > 12); assert.equal(command('uptime').message, 'terminal.uptime'); -assert.equal(command('version').message, 'Life OS v1.18.0'); +assert.equal(command('version').message, 'Life OS v1.18.1'); assert.equal(command('status').message, 'terminal.status'); assert.match(command('apps').message, /calendar/); assert.equal(command('echo "hello Life OS"').message, 'hello Life OS');