shopping: #shopping tag, help section, tests, client-side proxy

Three follow-ups:

  - Accept #shopping as an alias for #einkauf. Either tag now makes
    a card show up in the picker; the help blurb in the dialog says
    so explicitly.
  - Add a proper help-overlay section covering the trigger tags,
    how to pick an active list, the phone URL, sync cadence, and
    why phone is check-only.
  - Add a client-side proxy in public/index.php for /api/shopping
    and /api/shopping/check — shopping.js was calling api('api/...')
    which went to hyprcrd, but only the relay had handlers. Without
    this forwarder, push/pull just 404'd. Proxy mirrors the pattern
    used by /api/relay/push.
  - New shopping.test.js: asserts the proxy returns a JSON shape
    with `ok` (works whether or not a relay is configured), the
    hamburger menu toggles as expected, and the picker lists both
    #einkauf + #shopping cards while excluding untagged ones.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.6 committed 2026-04-16 00:35:52 +02:00
1 parent 670f125627
commit e086db0c2c
6 files changed
+143 -6

No files matched your search

+49
View File
@@ -1148,6 +1148,40 @@ if (str_starts_with($route, '/api/')) {
exit;
}
// ── Shopping list (proxy to relay) ───────────────────────────
// Thin forwarder. The client (shopping.js) talks to us, we relay
// to /api/shopping with the configured X-Relay-User/-Key headers.
// Phone UI talks to the relay directly via session auth.
if (($route === '/api/shopping' || $route === '/api/shopping/check') && in_array($method, ['GET', 'POST'])) {
$relayUrl = $db->query("SELECT value FROM settings WHERE key = 'relay_url'")->fetchColumn();
$relayKey = $db->query("SELECT value FROM settings WHERE key = 'relay_key'")->fetchColumn();
$relayUser = $db->query("SELECT value FROM settings WHERE key = 'relay_username'")->fetchColumn();
if (!$relayUrl || !$relayKey || !$relayUser) {
echo json_encode(['ok' => false, 'error' => 'relay not configured']); exit;
}
$ch = curl_init(rtrim($relayUrl, '/') . $route);
$opts = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-Relay-User: ' . $relayUser,
'X-Relay-Key: ' . $relayKey,
],
CURLOPT_TIMEOUT => 10,
];
if ($method === 'POST') {
$opts[CURLOPT_POST] = true;
$opts[CURLOPT_POSTFIELDS] = file_get_contents('php://input') ?: '{}';
}
curl_setopt_array($ch, $opts);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
http_response_code($status ?: 502);
echo is_string($response) ? $response : json_encode(['ok' => false, 'error' => 'relay unreachable']);
exit;
}
http_response_code(404); echo json_encode(['error' => 'not found']); exit;
}
@@ -1656,6 +1690,21 @@ if (str_starts_with($route, '/api/')) {
</table>
</section>
<section>
<h3 class="font-pixel text-xs text-t-accent mb-3 flex items-center gap-2"><i class="ph ph-shopping-cart text-sm"></i>shopping list (phone sync)</h3>
<div class="text-xs leading-relaxed space-y-3 text-t-muted mb-3">
<p>Tag a card with <span class="text-t-accent">#einkauf</span> or <span class="text-t-accent">#shopping</span> to mark it as a shopping-list candidate. Open the header's <i class="ph ph-list-dashes text-xs"></i> menu → <span class="text-t-bright font-pixel">Einkaufsliste</span> to pick which one is the currently <span class="text-t-bright">active</span> list — only one at a time is mirrored to the phone.</p>
<p>The active card's body is pushed to the relay on every sync tick. On your phone, open <span class="text-t-accent">&lt;your-relay&gt;/shopping</span> and log in with your relay credentials. The page shows only the <span class="text-t-bright">unchecked</span> <code class="text-t-accent">- [ ]</code> items as tappable rows. Tapping an item flips it to <code class="text-t-accent">- [x]</code> on the relay in-place; the next client tick pulls that back and updates the local card.</p>
<p>Editing stays in hyprcrd — the phone can only check/uncheck, never change text. That keeps conflict handling trivial (last-writer wins by timestamp).</p>
</div>
<table class="w-full text-xs">
<tr><td class="py-1 text-t-dim w-40 font-pixel"><i class="ph ph-hash text-xs"></i> trigger tags</td><td><span class="text-t-accent">#einkauf</span> or <span class="text-t-accent">#shopping</span> — either counts</td></tr>
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-list-dashes text-xs"></i> pick active</td><td>header menu → Einkaufsliste → radio select → Save</td></tr>
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-device-mobile text-xs"></i> phone URL</td><td><code>https://&lt;relay&gt;/shopping</code> — session-auth, mobile layout</td></tr>
<tr><td class="py-1 text-t-dim font-pixel"><i class="ph ph-arrows-clockwise text-xs"></i> sync cadence</td><td>piggybacks on the shared sync tick (see Settings → Relay)</td></tr>
</table>
</section>
<section>
<h3 class="font-pixel text-xs text-t-accent mb-3 flex items-center gap-2"><i class="ph ph-download-simple text-sm"></i>import / export</h3>
<table class="w-full text-xs">