async function fetchJson(url, options) { const res = await fetch(url, options); const payload = await res.json(); if (!res.ok) throw new Error(payload.detail || 'Request failed for ' + url); return payload; } function escapeHtml(value) { return String(value || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function renderSuggestionCards(target, items) { if (!target) return; if (!items.length) { target.textContent = 'Nothing loaded yet.'; target.classList.add('muted'); return; } target.classList.remove('muted'); target.innerHTML = items.map((item) => { const ingredients = (item.ingredients || []).slice(0, 8).map((entry) => '' + escapeHtml(entry) + '').join(''); const reasons = (item.score_reasons || []).slice(0, 4).map((entry) => '
  • ' + escapeHtml(entry) + '
  • ').join(''); const kitchenowl = item.kitchenowl_status && item.kitchenowl_status !== 'not-imported' ? '

    KitchenOwl: ' + escapeHtml(item.kitchenowl_status) + '

    ' : ''; return `

    Score ${escapeHtml(item.score)}

    ${escapeHtml(item.title)}

    ${escapeHtml(item.source_query || item.source_title || '')}

    ${escapeHtml(item.status || 'suggested')}

    ${kitchenowl}
    ${ingredients || 'No ingredient list parsed'}
    `; }).join(''); } async function loadDashboard() { const data = await fetchJson('/api/dashboard/summary'); const counts = data.counts || {}; const profile = data.profile || {}; const setText = (id, value) => { const el = document.getElementById(id); if (el) el.textContent = value; }; setText('count-suggested', counts.suggested || 0); setText('count-approved', counts.approved || 0); setText('count-saved', counts.saved || 0); setText('count-rejected', counts.rejected || 0); setText('count-plans', counts.plans || 0); setText('backend-note', 'Storage backend: ' + data.backend); const blacklist = document.getElementById('blacklist-items'); if (blacklist) { const items = profile.soft_blacklist || []; if (!items.length) { blacklist.textContent = 'No soft blacklist items yet.'; } else { blacklist.classList.remove('muted'); blacklist.innerHTML = items.map((item) => '').join(''); } } const notes = document.getElementById('profile-notes'); if (notes) notes.value = profile.notes || ''; const threshold = document.getElementById('auto-import-threshold'); if (threshold) threshold.value = profile.auto_import_threshold || 96; const enabled = document.getElementById('auto-import-enabled'); if (enabled) enabled.checked = Boolean(profile.auto_import_enabled); renderSuggestionCards(document.getElementById('suggestion-list'), data.recent_suggestions || []); } async function handleDecision(event) { const button = event.target.closest('button[data-decision]'); if (!button) return; const card = button.closest('[data-suggestion-id]'); const suggestionId = card && card.dataset ? card.dataset.suggestionId : ''; const decision = button.dataset.decision; if (!suggestionId || !decision) return; button.disabled = true; try { await fetchJson('/api/suggestions/' + encodeURIComponent(suggestionId) + '/decision', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({decision, import_to_kitchenowl: decision === 'approve'}), }); await loadDashboard(); if (document.getElementById('search-results')) { await loadSuggestionsInto(document.getElementById('search-results')); } } catch (error) { alert(error.message); } finally { button.disabled = false; } } async function loadSuggestionsInto(target) { const data = await fetchJson('/api/suggestions'); renderSuggestionCards(target, data.items || []); } function wireDashboard() { document.addEventListener('click', async (event) => { if (event.target.matches('button[data-decision]')) { await handleDecision(event); return; } if (event.target.matches('button[data-blacklist-name]')) { const name = event.target.dataset.blacklistName; const enabled = event.target.dataset.blacklistEnabled !== '1'; await fetchJson('/api/profile/blacklist', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({name, enabled}), }); await loadDashboard(); } }); const seedButton = document.getElementById('seed-button'); if (seedButton) { seedButton.addEventListener('click', async () => { seedButton.disabled = true; seedButton.textContent = 'Seeding…'; try { await fetchJson('/api/search/seed', {method: 'POST'}); await loadDashboard(); } catch (error) { alert(error.message); } finally { seedButton.disabled = false; seedButton.textContent = 'Seed suggestion queue'; } }); } const blacklistForm = document.getElementById('blacklist-form'); if (blacklistForm) { blacklistForm.addEventListener('submit', async (event) => { event.preventDefault(); const input = document.getElementById('blacklist-name'); if (!input.value.trim()) return; await fetchJson('/api/profile/blacklist', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({name: input.value.trim(), enabled: true}), }); input.value = ''; await loadDashboard(); }); } const settingsForm = document.getElementById('settings-form'); if (settingsForm) { settingsForm.addEventListener('submit', async (event) => { event.preventDefault(); await fetchJson('/api/profile/settings', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ notes: document.getElementById('profile-notes').value, auto_import_threshold: Number(document.getElementById('auto-import-threshold').value || 96), auto_import_enabled: document.getElementById('auto-import-enabled').checked, }), }); await loadDashboard(); }); } } function wireSearch() { const form = document.getElementById('search-form'); if (!form) return; form.addEventListener('submit', async (event) => { event.preventDefault(); const query = document.getElementById('search-query').value.trim(); if (!query) return; const target = document.getElementById('search-results'); target.textContent = 'Searching and normalizing recipes…'; target.classList.add('muted'); try { const data = await fetchJson('/api/search', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({query, limit: 6}), }); renderSuggestionCards(target, data.results || []); } catch (error) { target.textContent = error.message; } }); } function wirePlanner() { const form = document.getElementById('planner-form'); if (!form) return; const weekStart = document.getElementById('week-start'); if (weekStart && !weekStart.value) { weekStart.value = new Date().toISOString().slice(0, 10); } form.addEventListener('submit', async (event) => { event.preventDefault(); const anchors = document.getElementById('planner-anchors').value; const target = document.getElementById('plan-output'); target.textContent = 'Building the week…'; target.classList.add('muted'); try { const data = await fetchJson('/api/planner/generate', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ week_start: document.getElementById('week-start').value, target_meals: Number(document.getElementById('target-meals').value || 5), anchors, }), }); const plan = data.plan; const shared = (plan.shared_ingredients || []).map((item) => '' + escapeHtml(item) + '').join(''); target.classList.remove('muted'); target.innerHTML = `

    Week of ${escapeHtml(plan.week_start)}

    ${escapeHtml(plan.summary)}

    ${shared || 'No shared ingredients landed yet.'}
    ${(plan.meals || []).map((meal, index) => `

    Meal ${index + 1}

    ${escapeHtml(meal.title)}

    ${escapeHtml(meal.note)}

    ${(meal.ingredients || []).slice(0, 8).map((item) => '' + escapeHtml(item) + '').join('')}
    `).join('')} `; } catch (error) { target.textContent = error.message; } }); } loadDashboard().catch(() => {}); wireDashboard(); wireSearch(); wirePlanner(); if (document.getElementById('search-results')) { loadSuggestionsInto(document.getElementById('search-results')).catch(() => {}); }