• Mahmoud Aglan's avatar
    feat(branding): one resolved brand, and the fields that were collected but never read · 2dff900b
    Mahmoud Aglan authored
    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: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
    2dff900b
Name
Last commit
Last update
..
sidebar.blade.php Loading commit data...
topbar.blade.php Loading commit data...