feat(branding): one resolved brand, and the fields that were collected but never read
S2 of the mobile-portal programme. Branding lived in four uncoordinated
stores with no sync, and each layout resolved it itself in an `@php` block
issuing one SettingsService::get() per field — about sixteen SELECTs per
admin render, repeated on every Livewire round-trip, each with its own
fallback. That is how primary_color came to be defined three times with
three different defaults.
BrandingService returns a readonly BrandProfile, cached under the academy's
new `branding_version` and bumped on save, so it is held until branding
actually changes rather than for a guessed number of minutes, and a queue
worker cannot serve last week's colours. Verified on the restored oc_sport
copy: a second resolve inside one request issues 0 queries.
Defects fixed, each verified against that copy:
- `academies.address` did not exist. AcademySettings has been reading and
writing it on every save since it was written, and Eloquent silently
dropped it — no academy has ever had an address stored.
- `branding.academy_name` was read by the parent layout and by every printed
sheet and written by nothing, so both showed the literal string "الكابتن"
on every tenant. It is seeded from the academy's own name and is now
editable. The login page now reads "او سي سبورت" on the verified tenant.
- components/print/sheet.blade.php emitted the raw storage path into an
<img src>, so the logo was broken on every printed sheet. Paths become
URLs in BrandingService and nowhere else.
- Guests had no academy bound at all, so the login screen — and the member
portal's own sign-in, when it exists — rendered under the fallback brand on
every client. An installation with exactly one academy now resolves it for
guests too; more than one is ambiguous and binds nothing.
- AcademySettings had no authorize() call while every sibling settings screen
does.
Dead fields: the plan's rule is wire it or delete it, and none of them
survived as collect-but-ignore. login_background now grounds the login
screen, invoice_header and invoice_footer_text and show_logo_in_invoice
reach the printed invoice, header_bg colours the topbar, compact_sidebar
narrows the rail, and success_color/danger_color colour the flash strip.
Colour derivation. sidebar.blade.php hardcoded `color: #fff` on the brand
accent — this is a tenant-branded product, so a client whose brand is yellow
got white on yellow at 1.53:1. ColorRamp derives a 50…900 OKLCH ramp plus a
foreground chosen by WCAG contrast: that same yellow now gets #111827 at
11.58:1. Nine brand colours are asserted at AA or better.
The ramp is anchored on the tenant's own lightness rather than fixed
targets, because fixed targets are non-monotonic for an inherently light
brand: yellow sits at L 0.86, so a table putting 400 at L 0.70 makes 400
darker than 500. Chroma falls steeply at the pale end — at L 0.97 a chroma
of 0.10 is outside sRGB and clips to mud.
E1 decided as the addendum recommends: `@custom-variant dark` is declared
against the `.dark` class. Roughly 900 `dark:` utilities have been compiling
to prefers-color-scheme and rendering an untested dark ERP for every OS-dark
user, while the toggle did nothing. The OS-driven rendering stops here and
the toggle becomes the only thing that switches themes.
App icons are generated with GD directly rather than by adding
intervention/image: GD is the only image extension in the Dockerfile, and
the whole job is decode, letterbox, resize, write PNG. Dimensions are read
from the header before decoding, since a small file can declare enormous
dimensions. Filenames are content-hashed because nginx serves assets
`expires 1y; immutable`.
SVG uploads are refused everywhere they were accepted. An SVG on the
academy's own origin executes script with the site's privileges and
clean_html() never sees it.
npm run build byte baseline before portal.css exists:
app.css 208.10 kB / 31.02 kB gzip, website.css 217.08 kB / 33.25 kB gzip.
Suite: 67 passed, 0 failed.
Co-Authored-By:
Claude Opus 5 (1M context) <noreply@anthropic.com>
Showing
tests/Unit/ColorRampTest.php
0 → 100644
Please register or sign in to comment