diff --git a/public/boot-guard.js b/public/boot-guard.js new file mode 100644 index 0000000..3d4dba0 --- /dev/null +++ b/public/boot-guard.js @@ -0,0 +1,13 @@ +/* Never leave the desktop hidden behind the loading overlay after a script error. */ +(() => { + const finish = () => document.querySelector('#boot')?.remove(); + window.addEventListener('error', event => { + console.error('Life OS startup error:', event.error || event.message); + finish(); + }); + window.addEventListener('unhandledrejection', event => { + console.error('Life OS startup rejection:', event.reason); + finish(); + }); + setTimeout(finish, 1400); +})(); diff --git a/public/index.html b/public/index.html index 50543a3..e284334 100644 --- a/public/index.html +++ b/public/index.html @@ -40,4 +40,4 @@ function streak(a){let s=0,d=new Date();while(a.some(m=>m.date===d.toISOString() 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(lang()==='zh'?'zh-CN':'en-US',{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||'';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 0.1 · Schema v1

`}function theme(x){let s=settings();s.theme=x;save('lifeos_settings',s);init();renderAll()}function exportData(){let data={app:'Life OS',version:'0.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'||d.schemaVersion!==1||!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)}init(); - + diff --git a/scripts/build.mjs b/scripts/build.mjs index 0fca749..e8c798d 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -7,7 +7,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, i18nJs, i18nRenderersJs] = await Promise.all([text("index.html"), text("addons.js"), text("i18n.js"), text("i18n-renderers.js")]); +const [rawIndexHtml, addonsJs, bootGuardJs, i18nJs, i18nRenderersJs] = await Promise.all([text("index.html"), text("addons.js"), text("boot-guard.js"), text("i18n.js"), text("i18n-renderers.js")]); const release = "1.1.0"; // The original single-file prototype carried an obsolete English-to-Chinese // DOM replacement block. Runtime localisation now lives in i18n.js and @@ -16,7 +16,7 @@ const withoutLegacyI18n = rawIndexHtml .replace(/const ZH=.*?function browserLanguage/s, 'function browserLanguage') .replace(/function localizeDynamicText.*?function i18n\(key,fallback=key\)\{return lang\(\)===\'zh\'\?\(ZH\[key\]\|\|key\):fallback\}/s, 'function i18n(key,fallback=key){return fallback}'); const indexHtml = withoutLegacyI18n.replace('src="addons.js"', `src="addons.js?v=${release}"`); -await writeFile(new URL("assets.js", server), `export const indexHtml=${JSON.stringify(indexHtml)};\nexport const addonsJs=${JSON.stringify(addonsJs)};\nexport const i18nJs=${JSON.stringify(i18nJs)};\nexport const i18nRenderersJs=${JSON.stringify(i18nRenderersJs)};\n`); +await writeFile(new URL("assets.js", server), `export const indexHtml=${JSON.stringify(indexHtml)};\nexport const addonsJs=${JSON.stringify(addonsJs)};\nexport const bootGuardJs=${JSON.stringify(bootGuardJs)};\nexport const i18nJs=${JSON.stringify(i18nJs)};\nexport const i18nRenderersJs=${JSON.stringify(i18nRenderersJs)};\n`); await copyFile(new URL("../worker/index.js", import.meta.url), new URL("index.js", server)); await copyFile(new URL("../.openai/hosting.json", import.meta.url), new URL("hosting.json", meta)); console.log("Built Life OS Worker artifact"); diff --git a/worker/index.js b/worker/index.js index 299416b..4bbbb31 100644 --- a/worker/index.js +++ b/worker/index.js @@ -1,4 +1,4 @@ -import { addonsJs, i18nJs, i18nRenderersJs, indexHtml } from "./assets.js"; +import { addonsJs, bootGuardJs, i18nJs, i18nRenderersJs, indexHtml } from "./assets.js"; const BING_ARCHIVE = "https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=en-US"; const STATE_FIELDS = ["profile", "memories", "museum", "settings", "system"]; @@ -62,6 +62,7 @@ export default { return Response.json({ error: "Bing wallpaper is temporarily unavailable", detail: String(error) }, { status: 502 }); } if (url.pathname === "/addons.js") return new Response(addonsJs, { headers: headers("application/javascript; charset=utf-8", "no-cache") }); + if (url.pathname === "/boot-guard.js") return new Response(bootGuardJs, { headers: headers("application/javascript; charset=utf-8", "no-cache") }); if (url.pathname === "/i18n.js") return new Response(i18nJs, { headers: headers("application/javascript; charset=utf-8", "no-cache") }); if (url.pathname === "/i18n-renderers.js") return new Response(i18nRenderersJs, { headers: headers("application/javascript; charset=utf-8", "no-cache") }); if (url.pathname === "/" || url.pathname === "/index.html") {