cauldron/cauldron/templates/picks.html
Cobb Hayes 592b4f1161 Public-flip audit: env-driven paths, scrub audit-ticket prefixes, terser README
Lucy bind paths + LAN host pins replaced with env defaults. Repository URLs
→ git.sulkta.com. Audit-changelog scaffolding stripped from inline comments
(technical reasoning preserved). README sheds marketing scaffolding. AI-speak
in load-bearing prompts/SOULs left alone — that IS the product.
2026-05-27 11:42:56 -07:00

80 lines
3.5 KiB
HTML

{% extends "_base.html" %}
{% block title %}Picks · Cauldron{% endblock %}
{% block content %}
<div class="page-head">
<div class="crumb">// picks · household pool</div>
<h1>the <span class="accent">pool</span></h1>
<div class="lede">
{% if picks %}
{{ picks|length }} {% if picks|length == 1 %}recipe{% else %}recipes{% endif %} pinned by your household ({{ household_size }} {% if household_size == 1 %}member{% else %}members{% endif %}). next ai meal-plan run uses all of these.
{% else %}
empty pool. tap the mushroom 🍄 on any recipe in <a href="/recipes">the grimoire</a> to pin it for the next meal-plan run.
{% endif %}
</div>
</div>
{% if picks %}
<section class="panel purple">
<div class="panel-head">
<h2>pinned</h2>
<span class="ctx">{{ picks|length }} {% if picks|length == 1 %}recipe{% else %}recipes{% endif %}</span>
</div>
<ul id="picks-list" style="list-style: none; padding: 0; margin: 0;">
{% for p in picks %}
<li data-slug="{{ p.slug }}" style="padding: 12px 0; border-bottom: 1px solid var(--line-soft);">
<div style="display: flex; justify-content: space-between; align-items: baseline; gap: 14px;">
<a href="/recipes/{{ p.slug }}" style="flex: 1; color: var(--bone); font-family: var(--serif); font-size: 1.05em; border: none;">{{ p.name }}</a>
{% if p.mine %}
<button class="btn js-unpin" type="button" style="font-size: 11px; padding: .35em .9em;">unpin</button>
{% endif %}
</div>
<div style="margin-top: 4px; color: var(--muted); font-size: 11px; letter-spacing: .1em; text-transform: uppercase; font-family: var(--mono);">
🍄 pinned by
{% for picker in p.pickers %}<span style="color: var(--green-bright);">{{ picker }}</span>{% if not loop.last %} · {% endif %}{% endfor %}
</div>
</li>
{% endfor %}
</ul>
</section>
<section class="panel green">
<div class="panel-head">
<h2>next</h2>
</div>
<p>once the meal plan generator lands in <code>v0.3</code>, this whole pool seeds the week — guaranteed slots for pinned dishes, ai fills around them.</p>
<p class="muted">picks are shared across your household. you can only unpin your own; ask whoever pinned the others to drop them.</p>
</section>
{% else %}
<section class="panel">
<p>head to <a href="/recipes">the grimoire</a>, tap the mushroom 🍄 on any recipe to pin it.</p>
<p class="muted">picks are shared. abby's pins, your pins, bay's pins — all combine.</p>
</section>
{% endif %}
<script>
// Delegated unpin listener — slug is read from the parent <li>'s
// data-slug attribute (HTML-attribute context, autoescaped by Jinja),
// never interpolated into a JS string literal inside HTML. The prior
// `onclick="removePick('{{ slug }}',...)"` pattern was a stored-XSS
// surface because HTML attribute decoding returns the bare `'` to the
// JS engine.
document.getElementById('picks-list')?.addEventListener('click', async (ev) => {
const btn = ev.target.closest('.js-unpin');
if (!btn) return;
const li = btn.closest('li');
const slug = li?.dataset?.slug;
if (!slug) return;
btn.disabled = true; btn.textContent = '…';
try {
const r = await fetch(`/api/picks/${encodeURIComponent(slug)}`, { method: 'DELETE' });
if (!r.ok) throw new Error(r.status);
li.remove();
if (!document.querySelectorAll('#picks-list li').length) location.reload();
} catch (e) {
btn.disabled = false; btn.textContent = 'unpin';
}
});
</script>
{% endblock %}