/* Life OS reliability and productivity add-ons. */ (() => { 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.6.0'; window.LifeOSVersion = APP_VERSION; const appIds = ['today', 'timeline', 'museum', 'insights', 'profile', 'tasks', 'calendar', 'system']; const t = (key, values) => LifeI18n.t(key, values); function readDraft() { try { return JSON.parse(localStorage.getItem(DRAFT_KEY)); } catch { return null; } } function saveDraft(text) { localStorage.setItem(DRAFT_KEY, JSON.stringify({ date: (typeof todayTargetDate !== 'undefined' && todayTargetDate) || localDay(), text, updatedAt: new Date().toISOString() })); } function restoreDraft(entry) { const draft = readDraft(); const today = (typeof todayTargetDate !== 'undefined' && todayTargetDate) || localDay(); if (!draft || draft.date !== today || !draft.text || entry.value) return; entry.value = draft.text; entry.dispatchEvent(new Event('input', { bubbles: true })); if (typeof toast === 'function') toast('addon.draftRestored'); } function wireToday() { const entry = document.querySelector('#entry'); if (!entry || entry.dataset.lifeosDraft === 'ready') return; entry.dataset.lifeosDraft = 'ready'; restoreDraft(entry); entry.addEventListener('input', () => saveDraft(entry.value)); } function wireLanguageControl() { /* The System renderer owns this control. */ } function clearDraftAfterSave() { setTimeout(() => { const today = (typeof todayTargetDate !== 'undefined' && todayTargetDate) || localDay(); try { const saved = JSON.parse(localStorage.getItem('lifeos_memories') || '[]').some(m => m.date === today); if (saved) localStorage.removeItem(DRAFT_KEY); } catch {} }, 700); } function closeSearch() { document.querySelector('#lifeos-search')?.remove(); } function openSearch() { closeSearch(); const box = document.createElement('div'); box.id = 'lifeos-search'; box.innerHTML = `
${t('addon.searchTitle')}

${t('addon.searchHint')}

`; document.body.append(box); const input = box.querySelector('input'); const results = box.querySelector('.lifeos-search-results'); const render = () => { let memories = []; try { memories = JSON.parse(localStorage.getItem('lifeos_memories') || '[]'); } catch {} const query = input.value.trim().toLowerCase(); const found = memories.filter(m => !query || [m.title, m.filmTitle, m.text, m.mood, m.date].join(' ').toLowerCase().includes(query)).slice(0, 8); results.innerHTML = found.length ? found.map(m => ``).join('') : `
${t('addon.noResults')}
`; results.querySelectorAll('button').forEach(button => button.addEventListener('click', () => { closeSearch(); openApp('timeline'); setTimeout(() => detail(button.dataset.id), 20); })); }; input.addEventListener('input', render); box.querySelector('.lifeos-search-head button').addEventListener('click', closeSearch); box.querySelector('.lifeos-search-backdrop').addEventListener('click', closeSearch); render(); input.focus(); } const style = document.createElement('style'); style.textContent = `.lifeos-search-backdrop{position:fixed;inset:0;background:#0008;z-index:8000}.lifeos-search-panel{position:fixed;z-index:8001;top:18vh;left:50%;transform:translateX(-50%);width:min(560px,calc(100vw - 28px));background:var(--panel);border:1px solid var(--line);border-radius:15px;box-shadow:var(--shadow);padding:15px}.lifeos-search-head{display:flex;justify-content:space-between;align-items:center;font:500 11px 'DM Mono';letter-spacing:.12em;color:var(--muted);margin-bottom:13px}.lifeos-search-head button{border:0;background:transparent;color:var(--ink);font-size:20px}.lifeos-search-panel input{width:100%;background:var(--panel2);border:1px solid var(--line);border-radius:9px;padding:12px;outline:none}.lifeos-search-panel input:focus{border-color:var(--accent)}.lifeos-search-results{margin-top:9px;max-height:360px;overflow:auto}.lifeos-search-results button{display:grid;gap:3px;width:100%;text-align:left;border:0;border-radius:9px;padding:11px;background:transparent;color:var(--ink)}.lifeos-search-results button:hover{background:var(--panel2)}.lifeos-search-results small,.lifeos-search-results span,.lifeos-search-hint{color:var(--muted);font-size:12px}.lifeos-search-results strong{font-size:14px}.lifeos-search-results span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.lifeos-search-empty{padding:24px;color:var(--muted);text-align:center;font-size:13px}.lifeos-search-hint{margin:11px 0 0;font:10px 'DM Mono';letter-spacing:.04em}.launchitem svg{width:28px;height:28px;display:block;margin:auto;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}.launchitem:hover svg{color:var(--accent)}`; document.head.append(style); function exportData() { const data = { app: 'Life OS', version: APP_VERSION, schemaVersion: 2, exportedAt: new Date().toISOString(), profile: profile(), memories: mems(), museum: museum(), settings: settings(), system: sys() }; const link = document.createElement('a'); link.href = URL.createObjectURL(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })); link.download = `life-os-backup-${day()}.json`; link.click(); const state = settings(); state.lastBackup = new Date().toISOString(); save('lifeos_settings', state); renderAll(); toast('feedback.backup'); } // The System panel uses inline onclick handlers, so expose this complete exporter. window.exportData = exportData; const CLOUD_KEYS = { profile: 'lifeos_profile', memories: 'lifeos_memories', museum: 'lifeos_museum', settings: 'lifeos_settings', system: 'lifeos_system' }; let cloudReady = false, applyingCloud = false, syncTimer = null, syncErrorShown = false; const cloudFallback = { profile: {}, memories: [], museum: [], settings: {}, system: { schemaVersion: 2 } }; function cloudSnapshot() { const snapshot = Object.fromEntries(Object.entries(CLOUD_KEYS).map(([field, key]) => { try { return [field, JSON.parse(localStorage.getItem(key) || JSON.stringify(cloudFallback[field]))]; } catch { return [field, cloudFallback[field]]; } })); snapshot.system = { ...snapshot.system, schemaVersion: 2 }; return snapshot; } function hasLocalLife(state) { return state.memories.length || state.museum.length || Object.keys(state.profile).length || Object.keys(state.settings).length > 2 || state.system?.tasksBoard?.goals?.length || state.system?.tasksBoard?.tasks?.length; } function applyCloudState(state) { let localSystem = {}; try { localSystem = JSON.parse(localStorage.getItem('lifeos_system') || '{}'); } catch {} const localBoard = localSystem.tasksBoard, cloudBoard = state.system?.tasksBoard; const systemNeedsMigration = (Number(state.system?.schemaVersion) || 1) < 2; state = { ...state, system: { ...(state.system || {}), schemaVersion: 2 } }; let mergedLocalBoard = false; if ((localBoard?.goals?.length || localBoard?.tasks?.length) && state.system) { const mergeById = (cloudItems = [], localItems = []) => { mergedLocalBoard ||= localItems.some(item => !cloudItems.some(existing => existing.id === item.id)); return [...cloudItems, ...localItems.filter(item => !cloudItems.some(existing => existing.id === item.id))]; }; state = { ...state, system: { ...state.system, tasksBoard: { goals: mergeById(cloudBoard?.goals, localBoard.goals), tasks: mergeById(cloudBoard?.tasks, localBoard.tasks) } } }; } applyingCloud = true; Object.entries(CLOUD_KEYS).forEach(([field, key]) => localStorage.setItem(key, JSON.stringify(state[field] ?? cloudFallback[field]))); applyingCloud = false; todayDraft = null; window.renderIcons?.(); window.renderAll?.(); window.applyLanguage?.(); return mergedLocalBoard || systemNeedsMigration; } async function uploadCloudState() { if (!cloudReady) return; const response = await fetch('/api/lifeos-state', { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ state: cloudSnapshot() }) }); if (!response.ok) throw new Error('sync failed'); } function scheduleCloudSync() { if (!cloudReady || applyingCloud) return; clearTimeout(syncTimer); syncTimer = setTimeout(() => uploadCloudState().catch(() => { if (!syncErrorShown) { syncErrorShown = true; toast('addon.syncPaused'); } }), 450); } function migrationPrompt(state) { const dialog = document.createElement('div'); dialog.id = 'lifeos-cloud-migration'; dialog.innerHTML = `
${t('addon.syncReady')}

${t('addon.migrateQuestion')}

${t('addon.migrateBody')}

`; document.body.append(dialog); dialog.querySelector('[data-cloud="move"]').addEventListener('click', async () => { dialog.remove(); cloudReady = true; await uploadCloudState(); toast('addon.migrated'); }); dialog.querySelector('[data-cloud="empty"]').addEventListener('click', async () => { dialog.remove(); applyCloudState(cloudFallback); cloudReady = true; await uploadCloudState(); }); } async function startCloudSync() { try { const response = await fetch('/api/lifeos-state', { cache: 'no-store' }); if (!response.ok) throw new Error('cloud unavailable'); const { state } = await response.json(); const local = cloudSnapshot(); if (state) { if (hasLocalLife(local)) localStorage.setItem('lifeos_precloud_backup_v1', JSON.stringify({ exportedAt: new Date().toISOString(), state: local })); const mergedLocalBoard = applyCloudState(state); cloudReady = true; if (mergedLocalBoard) await uploadCloudState(); toast('addon.cloudRestored'); } else if (hasLocalLife(local)) { migrationPrompt(local); } else { cloudReady = true; await uploadCloudState(); } } catch { if (!syncErrorShown) { syncErrorShown = true; toast('addon.cloudUnavailable'); } } } const originalSetItem = Storage.prototype.setItem; Storage.prototype.setItem = function(key, value) { originalSetItem.call(this, key, value); if (this === localStorage && Object.values(CLOUD_KEYS).includes(key)) scheduleCloudSync(); }; const cloudStyle = document.createElement('style'); cloudStyle.textContent = `#lifeos-cloud-migration{position:fixed;inset:0;z-index:9000;background:#071008b8;display:grid;place-items:center;padding:20px}.lifeos-cloud-card{width:min(470px,100%);background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:25px;box-shadow:var(--shadow)}.lifeos-cloud-card h2{margin:10px 0;font-size:25px;letter-spacing:-.04em}.lifeos-cloud-card p{color:var(--muted);line-height:1.65;margin:0 0 20px}`; document.head.append(cloudStyle); startCloudSync(); new MutationObserver(() => { wireToday(); wireLanguageControl(); }).observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['title'] }); wireToday(); wireLanguageControl(); document.addEventListener('click', e => { if (e.target.closest('#content-today .primary')) clearDraftAfterSave(); }); document.addEventListener('click', e => { if (e.target.closest('.launchitem')) document.querySelector('#launcher')?.classList.add('hidden'); }); document.addEventListener('keydown', e => { const mod = e.ctrlKey || e.metaKey; if (mod && e.key.toLowerCase() === 'k') { e.preventDefault(); toggleLauncher(); } if (mod && e.key.toLowerCase() === 'f') { e.preventDefault(); openSearch(); } if (mod && e.key.toLowerCase() === 's' && document.querySelector('#entry')) { e.preventDefault(); archiveToday(); } if (e.altKey && /^[1-8]$/.test(e.key)) { e.preventDefault(); openApp(appIds[Number(e.key) - 1]); } if (e.key === 'Escape') { closeSearch(); document.querySelector('#launcher')?.classList.add('hidden'); document.querySelector('#calendar')?.classList.add('hidden'); document.querySelector('#life-context')?.remove(); } }); })();