Small gaps: pantry flags in the meal plan, birthday and anniversary notifications, download notes and highlights
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
342c8af747
commit
b2b772941b
9 files changed
+46
-4
No files matched your search
+12
@@ -104,6 +104,11 @@ const DAY = /^\d{4}-\d{2}-\d{2}$/, MEALS = ['breakfast', 'lunch', 'dinner'], pla
|
||||
const readPlan = () => { try { const d = JSON.parse(fs.readFileSync(planFile(), 'utf8')); return { days: d && d.days && typeof d.days === 'object' ? d.days : {} }; } catch { return { days: {} }; } };
|
||||
const writePlan = d => atomicWrite(planFile(), JSON.stringify(d), { mode: 0o600 });
|
||||
const planBetween = (from, to) => { const out = {}; for (const [k, v] of Object.entries(readPlan().days)) if (k >= from && k <= to) out[k] = v; return out; };
|
||||
// The pantry: what the family keeps in stock (one shared list). The plan flags planned ingredients that match, so they can be left off the shop.
|
||||
const pantryFile = () => path.join(process.env.DATA_DIR || path.join(__dirname, 'data'), 'pantry.json');
|
||||
const readPantry = () => { try { const a = JSON.parse(fs.readFileSync(pantryFile(), 'utf8')); return Array.isArray(a) ? a : []; } catch { return []; } };
|
||||
const stem = w => w.replace(/(ies)$/, 'y').replace(/(es|s)$/, '');
|
||||
const inPantry = (text, items) => { const words = new Set(String(text).toLowerCase().match(/[a-z]+/g) || []), stems = new Set([...words].map(stem)); return items.filter(it => { const ws = (String(it).toLowerCase().match(/[a-z]+/g) || []).map(stem); return ws.length && ws.every(w => stems.has(w)); }); };
|
||||
/** A recipe's ingredients onto Mealie's shopping list, `times` over (Mealie adds up what is already there). */
|
||||
async function addRecipe(slug, times) {
|
||||
const r = await mealie('/api/recipes/' + (slugOk(slug) ? slug : '-')), list = await mainList(), mult = Math.max(0.25, Math.min(20, +times || 1));
|
||||
@@ -152,6 +157,13 @@ async function handle(req, res) {
|
||||
if (d.days[b.date].length > 12) throw fail(400, 'That day is full'); writePlan(d); return json(res, 200, { ok: true, id });
|
||||
}
|
||||
if (p === '/plan/remove' && req.method === 'POST') { const b = JSON.parse(await readBody(req, 1e4) || '{}'), d = readPlan(); for (const k of Object.keys(d.days)) { d.days[k] = d.days[k].filter(e => e.id !== b.id); if (!d.days[k].length) delete d.days[k]; } writePlan(d); return json(res, 200, { ok: true }); }
|
||||
if (p === '/pantry' && req.method === 'GET') return json(res, 200, { items: readPantry() });
|
||||
if (p === '/pantry' && req.method === 'POST') { const b = JSON.parse(await readBody(req, 2e4) || '{}'), items = [...new Set((Array.isArray(b.items) ? b.items : String(b.items || '').split(/[\n,]/)).map(x => String(x).replace(/[\u0000-\u001f]/g, '').trim().slice(0, 60)).filter(Boolean))].slice(0, 300); atomicWrite(pantryFile(), JSON.stringify(items), { mode: 0o600 }); return json(res, 200, { items }); }
|
||||
if (p === '/plan/pantry' && req.method === 'GET') { // which planned ingredients the pantry already covers
|
||||
const from = q.get('from') || '', to = q.get('to') || ''; if (!DAY.test(from) || !DAY.test(to)) throw fail(400, 'Bad dates'); const items = readPantry(), out = []; if (!items.length) return json(res, 200, { items: [] });
|
||||
for (const sl of [...new Set(Object.values(planBetween(from, to)).flat().map(e => e.slug))]) { try { const r = full(await mealie('/api/recipes/' + sl)); for (const i of r.ingredients) { const hit = i.text && inPantry(i.text, items); if (hit && hit.length) out.push({ recipe: r.name, text: i.text, have: hit[0] }); } } catch (e) { if (e.code !== 404 && e.status !== 404) throw e; } }
|
||||
return json(res, 200, { items: out });
|
||||
}
|
||||
if (p === '/plan/shop' && req.method === 'POST') { // every planned meal in the range goes on the shopping list (a recipe planned twice counts twice)
|
||||
const b = JSON.parse(await readBody(req, 1e4) || '{}'); if (!DAY.test(b.from || '') || !DAY.test(b.to || '')) throw fail(400, 'Bad dates');
|
||||
const count = {}; for (const es of Object.values(planBetween(b.from, b.to))) for (const e of es) count[e.slug] = (count[e.slug] || 0) + 1;
|
||||
|
||||
Reference in new issue
Block a user