Restructure the shopping-list feature end to end:
- New public/plugins/shopping/ plugin with backend.php (proxy
routes registered via PluginManager::onApiRoute), shopping.js
(self-contained; injects the header Shopping button, owns the
picker dialog + sync loop via hyprcrd.hooks), plugin.json and a
placeholder css file. Core no longer knows about shopping —
index.php loses the static button and the proxy block, app.js
loses the initShopping wiring, the old js/modules/shopping.js
is gone.
- Phone view moves out of a standalone /shopping page and into
the relay UI as a regular `view-shopping` behind a nav-burger
entry (relay/ui/layout.php + app.js + style.css). Same session
auth as compose/chat/account — no separate login. Auto-refresh
every 4 s while the view is active; no manual refresh button.
Headings "# h1 … ###### h6" render between items for grouping.
Orphan section headings (no open items under them) are hidden.
- Sync: drop timestamp cursors (broke across SQLite-UTC / PHP
timezones and silently blocked pushes after a pull). Both sides
diff the (uid, title, body) triple instead — timezone-agnostic
and authoritative. Client ticks push-before-pull so fresh local
edits land on the relay before a stale pull can overwrite them.
Relay drops its "server newer" guard and stamps server times
with gmdate so anything left comparing to SQLite is UTC.
- Client also ticks on cards.reloaded, not just sync.tick, so
any edit path (editor save, checkbox toggle, context menu,
undo) pushes the update immediately.
- Tests: updated for the plugin (still uses the same selectors
because the plugin injects the same ids), plus a plugin-
registration check and "(no active list)" option assertion.
- Help section copy updated — references the Shopping header
button and the shopping tab in the relay UI rather than the
gone /shopping URL.
Both #einkauf and #shopping work as trigger tags; phone is
check-only, editing stays in hyprcrd. Version bumped to 1.9.21.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
hyprcrd — Electron Desktop App
Wraps the PHP backend in an Electron shell. A static PHP binary is bundled per platform so users don't need PHP installed.
How it works
- On launch, the app starts PHP's built-in server on a random port (
127.0.0.1) router.phpreplaces Apache's.htaccessrewrite rules- A
BrowserWindowloadshttp://127.0.0.1:<port>/ - On quit, the PHP process is killed cleanly
User data (database + uploads) is stored outside the app bundle so it survives updates:
- Dev:
electron/appdata/ - Linux:
~/.config/hyprcrd/data/ - macOS:
~/Library/Application Support/hyprcrd/data/ - Windows:
%APPDATA%/hyprcrd/data/
Development
cd electron
npm install
npm start # launches the app using system or bundled PHP
Requires PHP 8.1+ with pdo_sqlite and zip extensions — either on
the system PATH or as a static binary in php-bin/<platform>/.
Bundling PHP
Static PHP binaries are not checked into git. Fetch them before building:
./fetch-php.sh # downloads ~20MB per platform from static-php-cli
Or place them manually (see php-bin/README.md for sources):
php-bin/linux/php
php-bin/darwin/php
php-bin/win32/php.exe
The app prefers the bundled binary. If none is found, it falls back to system PHP. If neither exists, a dialog tells the user what to install.
Building distributables
npm run dist
Uses electron-builder. Output:
| Platform | Format | Location |
|---|---|---|
| macOS | .dmg |
dist/ |
| Windows | NSIS | dist/ |
| Linux | AppImage | dist/ |
File structure
electron/
main.js Electron main process — spawns PHP, creates window
preload.js Exposes window.hyprcrdElectron (platform, isElectron)
router.php PHP built-in server router (replaces .htaccess)
fetch-php.sh Downloads static PHP binaries for all platforms
php-bin/ Platform-specific PHP binaries (gitignored)