Shopping lists:
- Multiple lists per tenant: picker uses checkboxes instead of
radios. Relay schema migrated to composite PK (tenant_id,
card_uid) with a position column for display ordering.
- Picker rows are drag-and-drop reorderable (grip handle +
accent drop indicator). Order persists in localStorage and is
pushed to the relay as position integers — phone renders lists
in that order (ORDER BY position, title).
- Each picker row has a goto-arrow that closes the popup,
switches to the card's bucket if needed, and opens the card.
- Relay DELETE /api/shopping removes deactivated lists. Client
proxy forwards DELETE. Deactivating a list in the picker
immediately removes it from the relay.
- Phone UI renders multiple lists as stacked boxes with headers.
Section headings (# h1–h6) get 14px top padding for breathing
room. CSS selectors fixed to match div-based headings inside
the new .shopping-list-box containers.
- Help text updated for multi-list wording.
- pullOne preserves the synced position so a pull doesn't cause
a spurious re-push of the same order.
Test fixes:
- Line-numbers gutter height tolerance bumped 50%→60% — OS text
shaping can push the divergence past 50%.
- Find/replace flaky: replaced sleep(200) with waitFor on the
confirm dialog so slow renders don't miss the OK click.
- Shopping picker test updated: checkboxes instead of radios.
Bumps to 1.9.25.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
71 lines
2.5 KiB
PHP
71 lines
2.5 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Shopping Plugin: backend.
|
|
*
|
|
* Thin forwarder from the client to the relay's /api/shopping*
|
|
* endpoints. The phone-side view is part of the relay UI; this
|
|
* plugin just pipes hyprcrd's push/pull through the configured
|
|
* relay credentials.
|
|
*
|
|
* Routes:
|
|
* GET /api/shopping — pull active list state from relay
|
|
* POST /api/shopping — push active card {uid,title,body}
|
|
* POST /api/shopping/check — (mostly phone-only; proxied for
|
|
* completeness/tests — relay will
|
|
* 401 for client-auth callers)
|
|
*/
|
|
|
|
function shoppingJson($data, int $code = 200): void {
|
|
http_response_code($code);
|
|
header('Content-Type: application/json');
|
|
echo json_encode($data);
|
|
exit;
|
|
}
|
|
|
|
function shoppingRelay(): array {
|
|
$db = Database::connect();
|
|
return [
|
|
'url' => $db->query("SELECT value FROM settings WHERE key = 'relay_url'")->fetchColumn() ?: '',
|
|
'user' => $db->query("SELECT value FROM settings WHERE key = 'relay_username'")->fetchColumn() ?: '',
|
|
'key' => $db->query("SELECT value FROM settings WHERE key = 'relay_key'")->fetchColumn() ?: '',
|
|
];
|
|
}
|
|
|
|
PluginManager::instance()->onApiRoute(function (string $route, string $method) {
|
|
if ($route !== '/api/shopping' && $route !== '/api/shopping/check') return;
|
|
if (!in_array($method, ['GET', 'POST', 'DELETE'], true)) return;
|
|
|
|
$r = shoppingRelay();
|
|
if (!$r['url'] || !$r['user'] || !$r['key']) {
|
|
shoppingJson(['ok' => false, 'error' => 'relay not configured']);
|
|
}
|
|
|
|
$ch = curl_init(rtrim($r['url'], '/') . $route);
|
|
$opts = [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_HTTPHEADER => [
|
|
'Content-Type: application/json',
|
|
'X-Relay-User: ' . $r['user'],
|
|
'X-Relay-Key: ' . $r['key'],
|
|
],
|
|
CURLOPT_TIMEOUT => 10,
|
|
];
|
|
if ($method === 'POST') {
|
|
$opts[CURLOPT_POST] = true;
|
|
$opts[CURLOPT_POSTFIELDS] = file_get_contents('php://input') ?: '{}';
|
|
} elseif ($method === 'DELETE') {
|
|
$opts[CURLOPT_CUSTOMREQUEST] = 'DELETE';
|
|
$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);
|
|
header('Content-Type: application/json');
|
|
echo is_string($response) ? $response : json_encode(['ok' => false, 'error' => 'relay unreachable']);
|
|
exit;
|
|
});
|