qr_apply_owner_scope() used where('id_owner', X) + orWhere('id_owner', NULL,
'IS'). Any caller that had already added its own where('id', $id) before
calling it (qrcode_image.php, bulk_action.php's download path) ended up
with "WHERE id = ? AND id_owner = ? OR id_owner IS NULL" - AND binds
tighter than OR in SQL, so this was actually "(id = ? AND id_owner = ?) OR
(id_owner IS NULL)", which silently detaches the id filter and returns an
arbitrary null-owner row instead (or nothing, if that row's file is
missing) whenever the intended row didn't have a null owner. This is what
broke qr code thumbnails/downloads on qr.ensembia.com for scoped (non-super)
accounts, old and newly-created codes alike - reproduced and confirmed
fixed with a local before/after query dump, then with a live HTTP request
scenario (two accounts, two codes, one null-owner).
Fixed by building the scope as a single parenthesized raw condition
instead of two separate where() calls, so it can't be split apart by
whatever the caller already added to the query.
Also this session, per user feedback on the OSS rebrand review:
- Format moved back next to Filename in both qr-creation forms (was
separated from it when Filename got grouped with Owner last session).
- README/About now mention the temporary admin/admin demo account instead
of the not-yet-built self-registration flow.
Filename now sits in the same row as Owner instead of the crowded
top row shared with format/frame text/frame font/frame font size/icon.
Freeing that column lets those fields shift left and use the space
better. Applied to both the shared static-form partial
(qrcode_options.php) and the dynamic-form's separate copy of the same
fields (form_dynamic_add.php) - the two have diverged since Fase 3 and
don't share markup. Edit forms (form_static_edit.php/form_dynamic_edit.php)
already had filename/owner side by side, no change needed there.
- Default the Owner select to the creator's own account instead of "All", so
superadmin-created codes no longer silently become visible to every admin
(the underlying NULL-fallback sharing behavior for an explicit "All" choice
is unchanged).
- Add a live color/precision/size preview swatch next to the preset picker.
- Let the frame text use a chosen DejaVu font + font size instead of a fixed
GD bitmap font.
- Add an optional self-uploaded icon rendered above the qr code (not embedded
in it, so scanability is unaffected).
- Make the Bitcoin qr amount optional; a standing wallet address is useful
without forcing a one-off amount per code.
- Add a WPA3 option to the WiFi qr encryption select.
- Fix a real bug surfaced while testing the preview/style JS: qrcode_options.php
was included once per static qr type (16 times on one page) and each
inclusion re-executed <script src="qrcode-style-tools.js">, so every button
click fired once per type - e.g. saving one preset wrote 16 duplicate rows,
and every tab except the first ("Text") had dead random-style/preset
buttons since only the first DOM match ever got a listener. Moved the
script include to load once per page and rewrote the JS to scope every
lookup to the triggering element's own tab-pane/form instead of relying on
getElementById's first-match behavior.
Preset system: qr_presets table (migration 005) plus a presets.php AJAX
endpoint (list/save/delete, CSRF-protected, scoped to the logged-in user's
own id - presets are personal, never shared across accounts). UI/JS lives in
dist/js/qrcode-style-tools.js.
Random style button: client-side only, fills foreground/background with a
random hex color pair (playful randomize, no contrast/scannability
guarantee).
Qr scanner (scan_qrcode.php): camera or image upload, decoded entirely
client-side via html5-qrcode (CDN, pinned to 2.3.8).
PWA: manifest.json + service-worker.js, icons generated from the existing
DynamicQRCode_Original.png glyph. The service worker only caches static
assets (css/js/images) and deliberately never touches PHP pages, since those
carry CSRF tokens and session-specific content that must never be cached.
Fixes a gap found while testing: qrcode_options.php is only a shared partial
for the static qr forms - the dynamic qr form (form_dynamic_add.php) has its
own separate copy of the foreground/background/level/size/filename/format
fields (pre-existing structure, not something introduced here). That meant
frame_text and the new preset/random-style UI never showed up on the
dynamic qr page. Added the same fields there too, verified with a dynamic qr
plus frame text (150x180px, the expected +30px padding).