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>
Paystack blocks iframes, so checkout uses a popup beside the sheet/modal chrome. Opening about:blank on the user tap keeps that window from being blocked after the async initiate response.
Paystack checkout.paystack.com returns X-Frame-Options: SAMEORIGIN, so the shared sheet opened empty. Keep mobile sheet/desktop modal chrome, launch checkout in a named window with a Continue CTA fallback, and escape payment-return via window.opener or top.
Standardize customer-facing Ladill Pay checkout shells across products, open them on all viewports, and escape iframe callbacks back to the product flow.
act_runner v0.2.x does not expose gitea.run_attempt, so release archive and
workspace paths rendered with a trailing dash. Use gitea.run_id only, matching
the Meet/CRM deploy workflows.
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.
Portfolio showcases freelancer work projects; Bookshop lists author titles with multi-platform purchase links. Includes create/edit forms, public assets, and tests.
Replace the once-per-session sessionStorage guard (which suppressed the loading
screen after its first appearance) with entry-aware logic: show on app entry
and reloads, suppress on internal link navigations and back/forward.
Sync mobile-header-btn partials, mobile x-btn.create, and update dashboard header actions for consistent mobile FABs.
Co-authored-by: Cursor <cursoragent@cursor.com>
Remove duplicate Billing header above Home; wallet card follows Billing
in menu order to match the desktop avatar dropdown.
Co-authored-by: Cursor <cursoragent@cursor.com>
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>
Profile moves to the bottom nav sheet on small screens; optional search tab
support is synced from the Ladill platform template.
Co-authored-by: Cursor <cursoragent@cursor.com>