Add journal export and backup
AI Just committed 2026-08-18 09:01:30 +0000

Analyzing diff...

index.html+25 −1
index 8d12f09..240081a 100644
--- a/index.html
+++ b/index.html
@@ -20,8 +20,9 @@
 .personal{margin-top:5px;font-size:11px;padding:6px 8px;border-radius:9px;background:#f4f7f4;line-height:1.35}
 .personal.warn{background:#fff4df}.personal.alert{background:#fdebea}
 .evidence{font-size:10px;opacity:.72;margin-top:2px}
+.export{margin:14px 0;padding:14px;background:#f7f5f1;border-radius:14px}.export-controls{display:grid;grid-template-columns:minmax(180px,1fr) auto auto;gap:8px;align-items:end}.export-status{min-height:17px;margin-top:7px}
 
-.warn{background:#fff6e5;border:1px solid #edd8ac;padding:10px 12px;border-radius:12px;font-size:12px;margin-bottom:14px}.source{font-size:11px;color:var(--muted);margin-top:14px}.pill{font-size:11px;padding:3px 7px;border-radius:99px;background:#f0ede7;color:#68635b}.flex{display:flex;gap:8px;align-items:center;flex-wrap:wrap}@media(max-width:800px){.grid{grid-template-columns:1fr}.add{grid-template-columns:1fr 85px 48px}.add .primary{grid-column:auto}.meal-row{grid-template-columns:1fr 85px 70px 32px}.wrap{padding:15px}.top{display:block}}
+.warn{background:#fff6e5;border:1px solid #edd8ac;padding:10px 12px;border-radius:12px;font-size:12px;margin-bottom:14px}.source{font-size:11px;color:var(--muted);margin-top:14px}.pill{font-size:11px;padding:3px 7px;border-radius:99px;background:#f0ede7;color:#68635b}.flex{display:flex;gap:8px;align-items:center;flex-wrap:wrap}@media(max-width:800px){.grid{grid-template-columns:1fr}.add{grid-template-columns:1fr 85px 48px}.add .primary{grid-column:auto}.meal-row{grid-template-columns:1fr 85px 70px 32px}.wrap{padding:15px}.top{display:block}.export-controls{grid-template-columns:1fr}.export-controls button{width:100%}}
 </style></head>
 <body><div class="wrap">
 <div class="top"><div><h1>FODMAP-конструктор</h1><div class="sub">Соберите приём пищи по граммам. Цвет продукта меняется с порцией, а индикатор оценивает совокупную FODMAP-нагрузку и предлагает, что сократить.</div></div><span class="pill">локально • данные сохраняются в браузере</span></div>
@@ -38,6 +39,8 @@
 </div>
 <div class="card journal"><h2>📚 Сохранённые приёмы пищи</h2>
 <div class="small">Сначала сохраните состав еды. Реакцию можно добавить позже — через 30 минут, несколько часов или на следующий день.</div>
+<div class="export" data-feature="journal-export-v1"><b>Экспорт дневника</b><div class="small" style="margin:4px 0 9px">Скопируйте читаемый отчёт за выбранный период или сохраните полную резервную копию для будущего импорта.</div>
+<div class="export-controls"><label><span class="small">Период отчёта</span><select id="exportPeriod" style="width:100%"><option value="today">Сегодня</option><option value="week">Последние 7 дней</option><option value="all">Весь дневник</option></select></label><button onclick="copyJournalReport()">Скопировать отчёт</button><button class="secondary" onclick="downloadJournalBackup()">Скачать резервную копию</button></div><div id="exportStatus" class="small export-status" role="status" aria-live="polite"></div></div>
 <div id="history" class="history"></div>
 <div id="analytics" class="analytics"></div>
 </div>
@@ -142,6 +145,27 @@ function mealDateTime(e){
  return new Date(e.createdAt)
 }
 function mealDateLabel(e){return mealDateTime(e).toLocaleString('ru-RU',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'})}
+function exportEntries(period){
+ let entries=getEntries().slice().sort((a,b)=>mealDateTime(a)-mealDateTime(b));if(period==='all')return entries;
+ let today=new Date();today.setHours(0,0,0,0);let from=new Date(today),until=new Date(today);until.setDate(until.getDate()+1);if(period==='week')from.setDate(from.getDate()-6);
+ return entries.filter(e=>{let date=mealDateTime(e);return date>=from&&date<until})
+}
+function journalReport(period){
+ let labels={today:'сегодня',week:'последние 7 дней',all:'весь дневник'},entries=exportEntries(period),lines=[`Дневник питания — ${labels[period]||labels.all}`,`Сформирован: ${new Date().toLocaleString('ru-RU')}`,`Приёмов пищи: ${entries.length}`,''];
+ if(!entries.length)return lines.concat('За выбранный период записей нет.').join('\n');
+ entries.forEach((e,i)=>{lines.push(`${i+1}. ${mealDateLabel(e)}`);lines.push('Состав: '+(e.meal||[]).map(m=>`${m.name} — ${m.g} г`).join(', '));let rs=e.reactions||[];
+ if(!rs.length)lines.push('Реакции: не отмечены.');else{lines.push(`Реакции (${rs.length}):`);rs.forEach((r,j)=>{let details=[`${r.severity}/10 — ${r.severityLabel||severityText[r.severity]||'без описания'}`,r.delay];if(r.types&&r.types.length)details.push(r.types.join(', '));lines.push(`  ${j+1}) ${new Date(r.createdAt).toLocaleString('ru-RU')}: ${details.filter(Boolean).join(' • ')}`);if(r.comment)lines.push(`     Комментарий: ${r.comment}`)})}lines.push('')});return lines.join('\n').trim()
+}
+function exportMessage(text,isError){let el=document.getElementById('exportStatus');el.textContent=text;el.style.color=isError?'var(--red)':'var(--green)'}
+async function copyJournalReport(){
+ let report=journalReport(document.getElementById('exportPeriod').value),copied=false;
+ try{if(navigator.clipboard&&window.isSecureContext){await navigator.clipboard.writeText(report);copied=true}}catch(e){}
+ if(!copied){let area=document.createElement('textarea');area.value=report;area.setAttribute('readonly','');area.style.cssText='position:fixed;left:-9999px;top:0';document.body.appendChild(area);area.select();area.setSelectionRange(0,area.value.length);try{copied=document.execCommand('copy')}catch(e){}area.remove()}
+ exportMessage(copied?'Отчёт скопирован в буфер обмена.':'Не удалось скопировать автоматически. Разрешите доступ к буферу обмена и попробуйте снова.',!copied)
+}
+function downloadJournalBackup(){
+ let backup={metadata:{format:'olga-fodmap-journal-backup',version:1,exportedAt:new Date().toISOString()},entries:getEntries(),customFoods:JSON.parse(localStorage.getItem('fodmapCustomFoods')||'[]')},blob=new Blob([JSON.stringify(backup,null,2)],{type:'application/json;charset=utf-8'}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=`olga-fodmap-backup-${localDateString()}.json`;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1000);exportMessage('Резервная копия скачана.')
+}
 
 const foodSel=document.getElementById('food');
 function populateFoods(){