Uploads were written to the qr disk byte-for-byte. A bookshop cover arrived as a
9000x6600 print-resolution JPEG — 18MB — and was then served in full to every
visitor of a public bio page. During a launch that one file accounted for roughly
1.6GB of 1.9GB of image traffic, and because these assets are proxied through the
app rather than served statically, PHP streamed every byte of it and held a worker
for the duration.
Nothing the QR/bio pages display needs more than a couple of thousand pixels, so
UploadedImageOptimizer caps the long edge at 2000px and re-encodes on the way in.
The incident file becomes ~350KB. Configurable via qr.image.*.
Deliberately conservative, because a broken upload is worse than a large one:
- Format is preserved. A PNG stays a PNG so transparency survives; converting to
JPEG would put black boxes behind logos.
- GIF is excluded — GD would silently drop animation frames.
- SVG and any file GD cannot read is stored untouched.
- Small files are passed through rather than lossily re-encoded for no gain.
- Output is discarded if it came out larger than the original.
- Any failure logs and falls back to storing the original.
Applied to the seven image paths only. Books (PDF/EPUB) and documents keep the
plain store — the optimiser would have fallen through for them anyway, but routing
non-images through something called an image optimiser invites the wrong change
later.
This is the source-level counterpart to the nginx cache: caching stops the bytes
being regenerated, this stops them existing.
Tests: 6 new, including the real 9000x6600 shape, PNG alpha survival, format
preservation and non-image passthrough. Pre-existing suite failures unchanged at 8.
Co-Authored-By: Claude <noreply@anthropic.com>
Create was making three sequential public HTTPS billing calls (~400ms each)
while holding a DB transaction open for image generation. Use loopback DNS
forcing, cache balances briefly, return balance from debit, regenerate images
only when style changes, and keep remote work outside the DB transaction.
Show wallet balance prominently in the mobile profile bottom sheet with refresh
on open; align session lifetime with the platform default.
Co-authored-by: Cursor <cursoragent@cursor.com>
Public URLs, encoded payloads, slug preview, payment callbacks, and asset paths now use ladl.link instead of ladill.com/q/*.
Co-authored-by: Cursor <cursoragent@cursor.com>
Ladill Accounting is now live at accounting.ladill.com; add its row + icon to
the shared app launcher (byte-identical across all Ladill apps).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wallet balance peek rendered as a menu row directly under Billing, backed by
a /wallet/balance endpoint (BillingClient) and guarded by Route::has. Also
opens Afia via a direct window event for reliability.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
crm.ladill.com is now live in the suite; add its launcher row + icon
(byte-identical shared launcher config).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Correct localhost marketing_url default and stop redirecting guests through
/sso/connect on visit; SSO starts only via explicit sign-in.
Co-authored-by: Cursor <cursoragent@cursor.com>
Remove interactive OAuth fallback on silent SSO failure and add ladill.marketing_url config so app subdomains open the dashboard when signed in locally.
Co-authored-by: Cursor <cursoragent@cursor.com>
Sync ladill_launcher.php and give.svg across all extracted apps so Give appears in every product switcher.
Co-authored-by: Cursor <cursoragent@cursor.com>
Include transfer.svg and the SSO launcher row so Transfer appears in the
topbar grid across all Ladill apps.
Co-authored-by: Cursor <cursoragent@cursor.com>
Remove route-based page titles and the uppercase subtitle line so mobile
headers display a single title like Ladill Bird.
Co-authored-by: Cursor <cursoragent@cursor.com>
Route-based subtitle and page titles replace plain app labels in mobile topbars, and the Afia AI button is visible on mobile across topbars and mobile-page-header screens.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the shared auth session alive with periodic pings, store OIDC refresh tokens for silent re-login, and route launcher links through SSO connect.
Co-authored-by: Cursor <cursoragent@cursor.com>
Wire in-app chat scoped to QR codes, billing, and scans so users get product-specific help without leaving qrplus.ladill.com.
Co-authored-by: Cursor <cursoragent@cursor.com>
Utility QR types only (URL, WiFi, link list, business, app download) with
SSO, Billing API integration, public /q resolver, and qr-plus:import for
platform migration. vCard and commerce types stay on the platform.
Co-authored-by: Cursor <cursoragent@cursor.com>