Commit Graph

133 Commits

Author SHA1 Message Date
football2801 d7889be25a fix(design-v2): side rail footer pushed to bottom
CI / test (push) Has been cancelled
Tabs container had v1's flex:1, eating the vertical space and pinning
the footer right under the nav items. Resetting __tabs flex to 0 so
margin-top: auto on __foot can push it to the rail's bottom edge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:41:52 -04:00
football2801 29cd5a4775 fix(design-v2): !important on side rail subtitle + footer display
CI / test (push) Has been cancelled
Vue scoped CSS hides the subtitle and footer by default; my design-v2
override of display:block tied on specificity and lost to source order.
!important on the v2-only display rules forces the show.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:40:13 -04:00
football2801 0bc6f389cc fix(design-v2): side rail subtitle + footer signature + theme swatch harbor
CI / test (push) Has been cancelled
Mockup vs live diff revealed:
- Side rail brand area: mockup has italic '— home/library/settings' subtitle
  below WeVisto, live had none. Added via reactive activeSub computed.
- Side rail footer: mockup has 'a frame, gifted · v 0.4' italic at the
  bottom of the rail. Added top-nav__foot element; hidden in horizontal
  layouts, shown in v2 side rail via design-v2.scss
- Side rail mark size: bumped from 36px to 44px to match mockup
- Theme swatch harbor preview wasn't winning the cascade fight against
  Vue scoped styles (equal-specificity tie, Vue cascades later). Adding
  !important on the few preview properties — v2 is opt-in so this is
  appropriate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:38:42 -04:00
football2801 7081d2bee7 chore(devices): TEMP unconditional power_telemetry log + all X- headers
CI / test (push) Has been cancelled
Cycle 4+ polls landed cleanly but power_telemetry stayed silent even
after a php opcache bounce, so the question is whether the firmware
is sending X-Prev-* at all or the controller is filtering them out.
Log unconditionally on every poll and include every X-* request
header so the next poll shows definitively what's arriving over
the wire. Tighten back once diagnosed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 15:32:04 -04:00
football2801 0489028486 refactor(design): single source of truth — wevisto-design.css
CI / test (push) Has been cancelled
v2 tokens were duplicated: in design-v2.scss for the SPA, inlined in
login.html.twig for Twig. Two places to keep in sync.

Now: one shared /public/css/wevisto-design.css loaded by every Twig
standalone template AND linked from the SPA index.html. It contains:
- Brand constants (yellow / navy / fonts)
- v2 tokens with per-theme dusk overrides
- v2 base body bg + editorial typography defaults
- v2 overrides for the .card / .btn / .field-error / .logo-badge
  patterns used across all Twig templates

The SPA's design-v2.scss now holds only SPA-specific composition:
side rail at desktop, frame card, theme swatch harbor preview,
settings polish. No token duplication.

Result: changing a v2 color in one file flows to every surface in both
worlds. Adding v2 to another Twig template only requires the existing
shared CSS link (already wired up to all 11 standalone templates).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:24:45 -04:00
football2801 0e4d78471b fix(design-v2): apply v2 styles to login template
CI / test (push) Has been cancelled
Twig login page was hardcoded with v1 cream/terracotta colors and had
no [data-design=v2] overrides. Cookie + html attribute injection were
working but had nothing to style off. Adding v2 overrides inline so
the login page matches the v2 register the rest of the app uses.

Default v2 palette on login uses ocean-dusk values (navy + cream text +
yellow V brand) since user isn't logged in yet so we can't read their
theme preference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:19:41 -04:00
football2801 645291c724 fix(design-v2): match approved mockups — solid navy, side rail, editorial type
CI / test (push) Has been cancelled
v2 was 'harbor backdrop everywhere + glass cards', but the approved
mockups at _design/atmospheric-redesign/ use solid navy with subtle
gradient, harbor selectively (theme swatches, frame heroes), left
side rail at desktop, and editorial Marcellus/Cormorant typography.

This rewrite:
- Drops the full-page harbor backdrop; body is now solid navy with
  a single radial gradient highlight
- Loads Marcellus, Cormorant Garamond, DM Mono via Google Fonts
- Editorial type recipes: h1/h2/h3 + frame card name + settings title
  use Marcellus. settings__hint becomes italic Cormorant. Section
  labels become DM Mono caps with 0.28em letterspacing
- TopNav restyled at desktop (≥960px) into a left-fixed side rail:
  240px wide, vertical stack of nav items, active item shows inset
  yellow rule + surface bg. Body gets 240px padding-left to shift
  content right.
- Theme swatches reuse the harbor.jpg inside their preview area,
  tinted to each dusk's color — matches the mockup exactly
- Per-dusk surface colors made opaque (was rgba 0.55) so cards are
  fully readable

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:55:19 -04:00
football2801 4f78ed8897 fix(pwa): manifest background_color → navy to kill the white frame
CI / test (push) Has been cancelled
iOS / Chrome use background_color to fill any padding when displaying
the install icon. Cream (#fdf6ee) was leaving a visible 'thick white
border' around the V-viewfinder when the browser auto-padded the icon
on the install-preview screen. Setting background to navy (#0e2740) —
matching the icon's outer fill — makes any auto-padding invisible.

Theme_color also updated for consistency on the install splash.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:46:08 -04:00
football2801 77c51586e8 feat(brand): V-viewfinder 3a — the approved rebrand (was: split-W)
CI / test (push) Has been cancelled
The V-viewfinder was approved during the favicon-v2 picker at
/v2/, then deployed prematurely (e7b9756) and reverted (81effca) after
the 'design pick is not a deploy command' lesson. Deploying it now with
explicit go-ahead.

Files: yellow V outline with the Camogli harbor visible inside, navy
field outside. Replaces the split-W (two Vs forming a W) across:
- favicon-16/32/64
- apple-touch-icon (180)
- icon-192 + icon-512 manifest icons
- icon-512-maskable (V at 65% safe zone)
- favicon.svg vector
- favicon.ico multi-res
- root-level apple-touch-icon{,-precomposed}.png for iOS fallback paths

Cache-bust query bumped to ?v=20260515-vviewfinder so browsers refetch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:42:00 -04:00
football2801 5e8d9efb7b fix(pwa): cache-bust icon link tags (?v=20260515-3a)
CI / test (push) Has been cancelled
iOS Safari caches the apple-touch-icon per origin and ignores byte-level
changes on the same URL. Adding a version query forces a refetch on
fresh visits without renaming the source files. Buttressed across all
standalone Twig templates and the SPA index plus the manifest icons so
Chrome desktop also refetches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:37:56 -04:00
football2801 8ae4a2fd5a chore(devices): TEMP log power-monitor telemetry headers from firmware
CI / test (push) Has been cancelled
Firmware emits X-Prev-Awake-Ms + X-Prev-Panel-Init-Ms on each poll
so we can verify the 13.3" PIN_PWR rail-cut change isn't slowing
the panel re-init or extending the awake window. Log them with
boot-reason context via `device.poll.power_telemetry` — greppable
in `docker logs pictureframe-php-1`. Remove this log + the
firmware NVS_KEY_PREV_* writes once the change is validated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 14:15:50 -04:00
football2801 76c72f69d8 fix(design-v2): theme swatches preview their own dusk
CI / test (push) Has been cancelled
Active-theme-only override made all six swatches show the same amber tint
in v2. Per-swatch rules via aria-label now give each its own dusk preview.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:05:33 -04:00
football2801 b53c0593c0 fix(design-v2): visibility polish — stronger glass + themed nav chrome
CI / test (push) Has been cancelled
First-cut v2 had transparent cards (alpha 0.55) that disappeared into the
harbor backdrop, plus the v1 cream chrome leaked through.

This pass:
- Bumps --color-surface / surface-2 to 0.85 alpha across every dusk so
  cards stay readable on top of the photo
- Darkens backdrop with a vignette + bumps --color-bg overlay to 0.70
- Adds [data-design=v2] chrome rules: top-nav/bottom-nav glass, theme
  swatches use dusks tokens, design-toggle cards become glass, install
  button keeps accent fg color, frame-card gets inset highlight
- Brightens --color-text-muted in each dusk for legibility

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:02:37 -04:00
football2801 a302ac09b4 feat(design): v2 opt-in (atmospheric dusks) — Settings toggle, cookie-mirrored
CI / test (push) Has been cancelled
Lets users opt into the new atmospheric design without affecting users on v1.
Adds a beta-flag toggle in Settings → Design. Server-side preference persists
across devices; a cookie mirrors it so unauthenticated Twig pages do correct
first-paint without an extra DB roundtrip.

Backend:
- User.designVersion column (nullable VARCHAR(10); null defaults to 'v1')
- Migration Version20260515120000
- PATCH /api/user/design endpoint accepting 'v1'|'v2', sets wevisto_design cookie
- SpaController injects data-design on <html> + refreshes the cookie on every
  SPA load (keeps cross-device pref in sync)
- Twig templates (base, login, register, help, setup, token-*) read the
  cookie via {{ app.request.cookies.get('wevisto_design')|default('v1') }}
  so login/setup pages also respect the user's design choice

Frontend:
- design-v2.scss — opt-in overlay scoped under [data-design="v2"]. Overrides
  --color-* tokens to dusk variants per theme (warm-craft → amber, ocean-dusk
  stays, etc.), adds harbor photo backdrop via body::before with theme tint
  via body::after. Glass-card blur on existing surfaces. v1 untouched.
- harbor.jpg shipped as a public asset (270KB, single-fetch, cached)
- User type gains designVersion ('v1' | 'v2')
- SettingsView toggle (Original / Atmospheric) calls the API, updates the
  data-design attribute optimistically, reverts on failure

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 12:28:44 -04:00
football2801 5bb8289a54 feat(ui): v1 desktop responsive — top app bar + content max-widths
CI / test (push) Has been cancelled
The existing PWA layout was mobile-first only: BottomNav hides at ≥960px
with no replacement, leaving desktop users with zero navigation and views
that stretch to viewport width. Fixes both:

- New TopNav.vue mirrors BottomNav (Home / Library / Settings) but renders
  as a top horizontal app bar at ≥960px only. Includes the wordmark + mark.
- App.vue includes <TopNav v-if="!route.meta.hideNav" /> alongside BottomNav
  so upload-flow hideNav: true still hides both.
- HomeView, LibraryView, SettingsView get desktop max-widths (820 / 1100 /
  720 respectively) so content centers instead of stretching to 1440+.

Same cream/terracotta theme tokens, no aesthetic change — just gives v1
proper desktop chrome. Prep for v2 opt-in landing next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 12:17:49 -04:00
football2801 81effca22b Revert "feat(brand): V-viewfinder favicon set (replaces split-W on photo)"
CI / test (push) Has been cancelled
This reverts commit e7b97561c1.
2026-05-15 10:35:21 -04:00
football2801 e7b97561c1 feat(brand): V-viewfinder favicon set (replaces split-W on photo)
CI / test (push) Has been cancelled
Picked 3a from the favicons-and-logo-v2 iteration: a yellow V cut out of
navy with the harbor photo visible inside — "you are looking at a
photograph framed by the V". The brand's own glyph rather than initials.

What changed:
- favicon-16 / -32 / -64 / icon-192 / icon-512 / apple-touch-icon (180):
  V at 86% of canvas, navy outside, full center-cropped harbor inside,
  yellow stroke-outlined border proportional to size.
- icon-512-maskable: V at 65% of canvas (inside the Android safe zone),
  navy in the outer 35% so circle/squircle launcher masks crop navy
  pixels, not the V.
- favicon.svg: lightweight vector — yellow V outline on navy, no embedded
  photo (kept under 300 bytes so it's fast even before the build cache).
- favicon.ico: multi-resolution 16/32/64 for legacy clients.

Root-level fallbacks (public/apple-touch-icon.png + -precomposed +
favicon.{svg,ico}) updated in lock-step so iOS's Add-to-Home-Screen
probes pick up the new icon without falling back to a cached old one.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:28:09 -04:00
football2801 3bd0b9a5a1 design: favicon v2 (V-centric) + logo-placement mockups
CI / test (push) Has been cancelled
Iteration after the atmospheric-redesign checkpoint. The earlier A/B/C/D
favicon set was scrapped — generic invented icons (roof, frame+horizon,
skyline, wax seal) that didn't tie to the brand's own glyph. New direction:
the yellow V from the wordmark, presented four ways. V-viewfinder (a V cut
out of navy showing the harbor photo inside) is the chosen path; refinement
3b crops to Camogli's coloured row-houses for a more brand-specific small
size reading.

Also adds three logo-placement mockups (PWA cold-launch splash, library
hero with the full wordmark logo, settings → about page) to give the
wordmark room to live beyond emails and login badges.

Self-contained: assets/ and spa/ copied into the design folder so the
mockups render without depending on neighbouring directories.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:25:09 -04:00
football2801 01b6007b1c design: atmospheric redesign mockups (login + SPA, six dusks)
CI / test (push) Has been cancelled
Self-contained HTML/CSS mockups proposing the brand-as-surface direction:
- Camogli harbor photo as the permanent backdrop on every authenticated view
- Six user themes rebuilt as atmospheric "dusks" (tinted overlays + accents)
  instead of cream variants
- Frosted-glass cards on top of the photo
- Same type system (Marcellus / Cormorant Garamond italic / Nunito body)
  shared between login and in-app

Includes:
- login-cinematic.html: canonical pre-auth example with Ken-Burns + grain
- spa/home.html, library.html, settings.html: in-app views with the new chrome
- spa/_tokens.css + _chrome.css: dusk system and frosted-glass primitives
- favicons/A-D: in-progress icon directions
- README.md with serving instructions and the porting checklist

Saved as a durable design artifact so the iteration survives /tmp wipes
and can be referred back to when porting to Vue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:44:26 -04:00
football2801 6c9959c00d fix(pwa): serve apple-touch-icon + favicons from root, add sizes hint
CI / test (push) Has been cancelled
iOS Safari's Add-to-Home-Screen flow probes /apple-touch-icon.png at the
site root in addition to the <link rel> on the page. Those root paths
currently 302 through Symfony's auth firewall to /login, so iOS gets HTML
where it expects a PNG and falls back to whatever it cached from earlier
installs (the 1 KB placeholder icon). Dropping the real PNG (and the
-precomposed alias) directly in public/ makes nginx serve them as static
files, ahead of the firewall.

Also adds favicon.svg and a multi-size favicon.ico at the root for
browsers/bots that probe / paths instead of reading <link>, and adds
sizes="180x180" to every apple-touch-icon link so iOS doesn't have to
guess.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:48:01 -04:00
football2801 40581cb98b feat(brand): wire new favicon set into every standalone Twig template
CI / test (push) Has been cancelled
Login, register, help, token-approve/decline, and setup pages each have
their own <head> (don't extend base.html.twig), so updating base alone
left them without favicon refs. Adds the same four <link> tags after the
viewport meta in each standalone template.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:43:29 -04:00
football2801 a7e7b96465 feat(brand): split-W PWA icons + favicons (replaces purple-star placeholder)
CI / test (push) Has been cancelled
- New mark: solid "W" glyph color-split left=white / right=yellow over the
  Camogli harbor photo from logo.svg; right half reads as a "V" so the W
  alone communicates "WeVisto" at icon scale where the wordmark is illegible.
- PWA icons (192, 512, apple-touch 180) rendered full-bleed; maskable
  variant shrinks the W to the inner 65% so circle/squircle launcher masks
  crop sky and harbor pixels, not the glyph.
- Adds favicon-16/32/64 PNGs and replaces the old purple-star favicon.svg
  with a lightweight vector split-W on solid navy.
- Wires the new favicons into both the SPA (frontend/index.html) and the
  Symfony Twig base (templates/base.html.twig), replacing the Symfony
  default "sf" emoji data-URL placeholder on the login page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:42:15 -04:00
football2801 e7e9202a11 feat(brand): official WeVisto logo + linked badge on user-facing pages
CI / test (push) Has been cancelled
- frontend/public/logo.svg: Camogli photo with We[V]isto knockout wordmark
  (yellow V accent), embedded base64 so the SVG is self-contained
- brand/: raw source (15.7MB Camogli original) + 900x900 crop used in the
  SVG, plus a short README documenting both
- Login, register, setup index/configure, help: linked logo badge above
  the page heading
- Email template: logo bumped to 64x64 (was 30 tall — wordmark unreadable)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:33:28 -04:00
football2801 2637eb19cd feat(brand): help page references WeVisto-XXXX AP SSID
CI / test (push) Has been cancelled
Coordinated with firmware rename of the provisioning AP from
PictureFrame-XXXX to WeVisto-XXXX. Recipients seeing the help page
mid-setup now see the same SSID name their phone WiFi list shows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:03:15 -04:00
football2801 9c29788210 feat(brand): logo placeholder + smoke.sh defaults to wevisto.com
CI / test (push) Has been cancelled
Adds frontend/public/logo.svg as a placeholder (rendered at /build/logo.svg
after Vite build). Email template share_notification.html.twig swaps the
text "WeVisto" header for an <img> referencing /build/logo.svg via
absolute_url, so dropping in the final design swaps one file with no
template change.

bin/smoke.sh HOST now defaults to wevisto.com — legacy host still smoke-
testable via HOST=pictureframe.edholm.me bin/smoke.sh under the dual-
domain coexistence (Option C).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:51:43 -04:00
football2801 db80ea5262 feat(brand): swap recipient-facing pictureFrame strings to WeVisto
CI / test (push) Has been cancelled
Updated: SPA <title>, PWA manifest name/short_name, iOS web-app title,
"Install"/"Pin to home screen" copy, HomeView empty state, all Twig page
titles (login/register/setup/token/help), and the share-notification
email header. Left alone: the firmware-broadcast SSID PictureFrame-XXXX
(coordinated firmware change needed) and internal code/comment refs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:42:50 -04:00
football2801 62cd3f57df chore(env): set MAILER_SENDER to noreply@wevisto.com
CI / test (push) Has been cancelled
Aligns the default sender with the public brand. Production mailer is
currently null:// so no live email is sent, but the From: header is now
consistent with the rest of the rebrand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:27:57 -04:00
football2801 dd89b3d934 feat(brand): switch user-facing copy + Mercure topic prefix to wevisto.com
CI / test (push) Has been cancelled
Mercure topic identifiers updated in lockstep across PHP publisher + TS
subscriber (and their tests). Help-page setup instructions now point to
wevisto.com. Traefik already serves both hosts; this aligns the in-app
references with the public brand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:27:07 -04:00
football2801 38ea9b3d06 fix(device-image): honor X-Draw-Pending to skip rotation during recovery
CI / test (push) Has been cancelled
When the firmware sends X-Draw-Pending: 1, its drawNeeded NVS flag
survived a power-loss-during-draw — it has the bytes for the previous
image in its cached /img.bin and just needs another chance to finish
painting them. Return the device's current image (no rotation advance),
which lands as a 304 since the device claims the same image-id.

Crucially this overrides the X-Boot-Reason: cold force-resync. The
typical mid-draw-interruption cause IS a reset that turns the next
wake into a cold boot, so without this override force-resync chases
a fresh image every interruption and the device cycles through the
rotation leaving torn frames on the 13.3 panel.

Locked image still wins (user intent overrides recovery). Old firmware
that doesn't send the header is unaffected — branch is gated on the
header being present.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:43:13 -04:00
football2801 f6321412aa test(frontend): cover ManageImageSheet debounce, StickerTray, PWA-install paths
CI / test (push) Has been cancelled
Add a test for ManageImageSheet's 200ms pendingApproval lock-release
(prevents the toggle becoming permanently disabled on a single tap),
expand SettingsView coverage to exercise the beforeinstallprompt event
path through usePwaInstall (accepted + dismissed outcomes), and add a
first pass of StickerTray tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:25:05 -04:00
football2801 409f51cc3e copy(crop): mention the recrop tool in the orientation-mismatch chip
CI / test (push) Has been cancelled
Old copy: "Switch the frame in Settings to display this crop."
That only surfaces one of the two ways out, and the less common one.

New copy: "Use the tool above to recrop for the current frame
orientation, or switch the frame in Settings to display this crop."
Recrop-here is the cheaper and usually-correct fix; settings-flip is
the fallback when the user really does want the other orientation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:41:08 -04:00
football2801 45e80cf4c0 fix(manage-sheet): clearer copy + visual hierarchy in row controls
CI / test (push) Has been cancelled
Matt called out the row was confusing: lock pill said "Rotate" (sounds
like a verb), and the toggle's purpose wasn't obvious.

  - Drop the "Rotate" word entirely. Lock pill is icon-only when
    unlocked, shows "Locked" + closed padlock when locked.
  - Hide the lock pill entirely when the photo isn't approved on the
    frame (instead of rendering a disabled one) — keeps the row clean
    and reinforces that locking requires approval first.
  - Add a tiny "Show" / "Hidden" label above the toggle so the meaning
    reads before the user taps. Toggle is now the visual primary on
    the row.
  - Re-label aria-text from "Add/Remove" to "Show/Hide" to match the
    visible copy.

Test "disables the lock pill when not approved" → "hides the lock pill
when not approved". 358/358 still passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:31:07 -04:00
football2801 84642ed13f feat(library): photo + status badge + ManageImageSheet (Concept A)
CI / test (push) Has been cancelled
Library was rendering one approval chip per device per photo PLUS one
lock chip per approved device. That's O(photos × devices) buttons —
fine at one or two frames, breaks at four+ (see
_bmad-output/.../library-many-frames-design-ideas.md).

Concept A from the design memo:
  - Each photo card stays a square thumb + a single "Manage" row.
  - Manage row summarises state: "3/5 frames · 🔒 Mom's Place".
  - A corner-lock badge sits on the thumb itself when any frame has the
    image locked, so the lock status is glanceable from the grid.
  - Tapping Manage opens the new ManageImageSheet bottom sheet, which
    lists every frame with an approve toggle + per-frame lock pill.
    Lock pill is disabled until the frame is approved.

Per-photo widgets drop from O(photos × devices) to O(photos). Works
identically at 1 or 50 frames. Curation principle stays "manage photos
TO the frame" — same store calls (imagesStore.setApproval,
devicesStore.lockImage/unlockImage), just routed through the sheet
instead of inline chip rows.

10 new ManageImageSheet unit tests + LibraryView tests rewritten to
cover the sheet-open + event-forwarding flow. 358/358 frontend tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:26:41 -04:00
football2801 9854688a49 docs(mockups): static HTML wireframes for Library scaling concepts
CI / test (push) Has been cancelled
Renders the five design concepts from
_bmad-output/planning-artifacts/library-many-frames-design-ideas.md as
standalone HTML so Matt can compare them in a browser without spinning
up the SPA. Mockups live at /mockups/library/ and reuse the project's
design tokens for visual consistency:

  index.html   landing page with concept links
  current.html the chip-explosion state we're shipping today
  concept-a.html  photo + status badge → DevicePicker sheet (recommended)
  concept-b.html  device-first tab
  concept-c.html  multi-select bulk action bar
  concept-d.html  device chip filter + photo dots

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:08:18 -04:00
football2801 a511b89564 fix(library): show full photo in each grid thumb (square + contain)
CI / test (push) Has been cancelled
Library thumbs were locked to 4:3 with object-fit: cover, so portrait
photos got their top and bottom crop-fitted off. Switching to 1:1
cells with object-fit: contain — full photo always visible, grid still
uniform, portrait and landscape both get symmetric letterbox bars.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:40:49 -04:00
football2801 ad0d6c572c fix(home): preview locks aspect to panel dims + object-fit so it never overflows
CI / test (push) Has been cancelled
The large FrameCard preview let the <img> drive height (`flex: 0 0
auto`, `width: auto`, `max-*: 100%`). On wide-container layouts and on
the new V2 1200×1600 dimensions the image's intrinsic size leaked
past the card, and the max-width/max-height combo can drop aspect
ratio in some browsers.

Now: the preview container locks its `aspect-ratio` to
`panelDims(model, orientation)` — same source of truth that drives the
empty-placeholder shape — and the <img> fills the container with
`object-fit: contain`. Container shape is stable whether or not the
thumbnail has loaded; image always scales to fit, portrait or
landscape device, narrow or wide phone column.

emptyAspectStyle no longer needs to carry aspect (parent already has
it); empty-preview placeholder fills 100% of the parent now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:30:16 -04:00
football2801 bea25098a0 revert(render): drop A/B tuning experiments; back to baseline pipeline
CI / test (push) Has been cancelled
Tried six experiments tuning saturation, gamma, blur, blue-channel
multiply, BLUE palette target, and WHITE palette target — each had
side effects worse than the original sky→face blue bleed we were
trying to fix (greens lost vibrance, sky went green, shadows got
more blue, etc). The baseline pipeline is the local maximum for now;
the 6-colour Spectra-6 palette is the real ceiling.

Drops RenderCompareCommand and restores RenderImageMessageHandler
verbatim from git tag render-baseline-2026-05-14.

Future render-quality work should attack the problem differently —
Lab-space color matching, semantic preprocessing, or a measured
panel-ink palette — not single-knob RGB tweaks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:14:29 -04:00
football2801 89ba3515ac experiment(render): revert BLUE shift; warm WHITE palette to (255,248,230)
CI / test (push) Has been cancelled
#5 (BLUE → [8,32,220]) made sky map to WHITE which dithered greenish.
Reverting BLUE to baseline (24,64,192).

#6: warm WHITE from (245,245,240) → (255,248,230). Slight cream shift
pulls warm-toned skin pixels toward WHITE in the nearest-color match
before error diffusion has a chance to drift them toward BLUE. Sky
mapping unchanged (still maps to BLUE). Cost: real off-white pixels
might quantize toward a slightly warmer cream — should be subtle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:07:24 -04:00
football2801 324f1b2641 experiment(render): revert blue_mul; shift BLUE palette target to (8,32,220)
CI / test (push) Has been cancelled
Experiment #4 (blue×0.95) made shadow regions appear MORE blue, not
less — reducing source blue still leaves positive blue error after a
BLACK mapping, and the dither spends that error on neighbours,
creating blue dither dots in dark regions.

Reverting blue_mul to 1.0. Experiment #5 takes a different attack on
the same problem: shift the BLUE palette mapping target from the
muted (24, 64, 192) to a more saturated (8, 32, 220). Doesn't change
what the panel displays (the blue ink is fixed); it just makes
Euclidean distance from skin tones to "BLUE" larger in the algorithm's
view, so the dither prefers RED/WHITE/YELLOW for borderline pixels.

Render-compare's BASELINE struct now carries its own frozen palette,
so half-A keeps the original (24,64,192) BLUE target while half-B
pulls the shifted palette from the live pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:03:25 -04:00
football2801 488fc3d0f4 experiment(render): revert sat/gamma/blur to baseline; blue-channel ×0.95
CI / test (push) Has been cancelled
Experiment #3 (sat 115, gamma 1.2, blur 0.6) was a net loss — greens
desaturated, no help on sky→face blue bleed.

Reverting those three to baseline (130, 1.0, 0.0). New experiment #4:
multiply the source's blue channel by 0.95 before dither. Real sky
stays well above the BLUE-vs-WHITE boundary, but borderline-bluish
skin (sky cast on outdoor faces) drops below it and maps to YELLOW /
WHITE / RED instead of feeding the dither's BLUE attractor.

Adds public BLUE_CHANNEL_MUL constant; render-compare's baseline
struct gets blue_mul=1.0 so half-A is still frozen at the original.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:54:11 -04:00
football2801 a37edcb6c7 experiment(render): revert FS + add pre-dither blur 0.6; A/B vs frozen baseline
CI / test (push) Has been cancelled
Riemersma dither produced visible "ink-spill" Hilbert-curve streaks in
low-contrast regions like skin. Reverting DITHER_METHOD to Floyd-
Steinberg and attacking the original sky-bleeds-into-face problem with
a pre-dither Gaussian blur instead:

  DITHER_METHOD  RIEMERSMA → FLOYDSTEINBERG  (back to baseline)
  + new BLUR_SIGMA = 0.6     (pre-dither softening; baseline 0.0)

Live tunables are now public so RenderCompareCommand can mirror them.
Half-A of app:render-compare is locked to the frozen-baseline set
(sat=130 gamma=1.0 sharpen=0.8 blur=0.0 FS) — what shipped at git tag
render-baseline-2026-05-14. Half-B always tracks the live pipeline, so
each future experiment is automatically the next A/B without touching
the comparison command.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:42:54 -04:00
football2801 2f3527aaf9 tool(render): app:render-compare for FS vs Riemersma A/B on the panel
CI / test (push) Has been cancelled
Stacks two dither treatments of the same image's top half into one V2
portrait .bin — top half Floyd-Steinberg, bottom half Riemersma —
overwrites the device's current V2 portrait asset, bumps rendered_at
and clears the preview PNG cache.

Usage: bin/console app:render-compare <imageId>

Lets Matt eyeball both methods on a single panel refresh instead of
two re-renders and two waits. One-shot experimental tool; not part of
the live render pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:36:12 -04:00
football2801 f3bf49ba1d experiment(render): Riemersma dither to stop sky→face blue bleed
CI / test (push) Has been cancelled
Floyd-Steinberg's row-order error diffusion was flushing residual blue
debt from sky pixels downward into the face below — visible as a blue
hue in skin tones that should have been YELLOW/RED/WHITE in the 6-color
palette. Riemersma uses a Hilbert-curve scan, so error stays local and
isn't biased along any axis.

DITHER_METHOD  FLOYDSTEINBERG → RIEMERSMA  (gamma 1.2 + saturation 115
from experiment #1 unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:29:46 -04:00
football2801 1ebc9b615d experiment(render): extract tunables + gamma 1.2, saturation 115%
CI / test (push) Has been cancelled
Make the render-pipeline knobs Matt and I are about to iterate on
visible as class constants on RenderImageMessageHandler — single source
of truth, easy diff per change.

First experiment (vs baseline git tag render-baseline-2026-05-14):
  SATURATION_PCT  130 → 115   (less risk of ruddy faces / synthetic skies)
  GAMMA           1.0 → 1.2   (gentle midtone lift; faces + shadows
                                climb out of the BLACK cluster after dither)

Sharpening (0.8) and Floyd-Steinberg dithering unchanged this round.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:22:06 -04:00
football2801 82a42011d8 fix(upload): persistent file <input> to survive iOS PWA cold launch
CI / test (push) Has been cancelled
A dynamically-created <input type="file"> that's never attached to the
DOM drops its first `change` event on a cold-launched iOS PWA — the
native photo picker resolves out of the original user-gesture context
and the closure that captured the input is gone. Symptom Matt hit
2026-05-14: first image-pick after hard-close + reopen of the PWA
silently failed to advance to the crop tool; the second attempt worked.

HomeView and LibraryView now keep a hidden <input ref="fileInputEl"
type="file"> live in their templates. onAddPhoto clicks that input
inside the user-gesture context; @change fires reliably even on cold
launches. The picker resets input.value between selections so picking
the same file twice still fires.

Tests updated to query the template input via wrapper.find() instead
of stubbing document.createElement; 347/347 frontend tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
render-baseline-2026-05-14
2026-05-14 13:02:26 -04:00
football2801 e57e711fcc chore(build): rebuild bundle for v2 crop fix + tighten Device.model
CI / test (push) Has been cancelled
The crop-aspect fix didn't reach production on the prior deploy because
public/build/ was 5 days stale. Rebuilds the SPA bundle so the
panelDims-driven CropEditor / StickerCanvas / FrameCard ship.

Also makes Device.model required in the TS type (was optional in this
session's first cut to placate test fixtures) and adds `model: 'v1'` to
every test Device fixture. A new device row from the API always has a
model, so the type should reflect that — leaving it optional was a trap
for production code that defensively assumed undefined.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:29:12 -04:00
football2801 081ca83613 fix(v2): preview rotation + crop aspect for 13.3" hardware
CI / test (push) Has been cancelled
Two related bugs that surfaced on the first 13.3" device's first photo:

1) Web-UI portrait preview was 90° sideways. DeviceApiController::
   renderBinToPng rotated whenever the device was Portrait — correct
   for V1 (landscape-native, Portrait => renderer rotated, so preview
   un-rotates) but wrong for V2 (portrait-native — the renderer
   doesn't rotate, so the preview shouldn't either). Now mirrors the
   render-pipeline check: rotate only when `orientation !==
   model->nativeOrientation()`. Two new functional tests pin the V2
   portrait and V2 landscape PNG dimensions to guard against
   regressions.

2) Cropped photo letterboxed on the 13.3" panel. CropEditor /
   StickerCanvas / FrameCard had V1 dimensions hardcoded (1600×960
   = 5:3 aspect). V2 is 4:3 (1200×1600 portrait / 1600×1200
   landscape), so a "full crop" came out the wrong shape and the
   server's white-canvas composite added bars. New `panelDims(model,
   orientation)` helper in @/types is the single source of truth on
   the frontend; matches DeviceModel::width/height on the server.
   Threaded `model` through Device serializer → Device type →
   UploadView → CropEditor / StickerCanvas, and HomeView → FrameCard.
   FrameCard tests updated to cover all four model × orientation
   placeholders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:02:39 -04:00
football2801 b286a1f241 feat(devices): DeviceModel::V2 for Waveshare 13.3" Spectra-6
CI / test (push) Has been cancelled
Adds the second panel model alongside V1 (800x480, 7.3"). V2 is
1200x1600 panel-native (tall) — the inverse aspect ratio means
its "natural" orientation is portrait, not landscape:
- DeviceModel::nativeOrientation() — V1 returns Landscape, V2 returns
  Portrait. Render rotates the source image 90 CCW only when the user's
  orientation differs from the panel's native, so the .bin stays
  panel-native scan order without per-model branches.
- DeviceModel::panelId() / fromPanelId() — string mapping for the
  firmware's X-Panel-Id header (matches -DPANEL_ID build flag).
- DeviceImageController: on every poll, if X-Panel-Id maps to a known
  model and differs from the device's current model, auto-correct.
  New Devices are created with the V1 default, so a freshly-claimed
  13.3" unit needs this correction before the first image render
  produces a wrong-dimension .bin the firmware would reject.

8 new DeviceModel unit tests, 3 new controller tests cover the
header-correction behaviour (different, same, unknown panel-id).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:53:59 -04:00
football2801 2adb07518c feat(account): change-password endpoint + Settings modal
CI / test (push) Has been cancelled
PATCH /api/user/password — verifies the current password, enforces
8-char minimum on the new one, and rehashes via the configured
password hasher. Returns 204 on success, 422 with an `error` body
on every validation failure (wrong current, too-short new, missing
fields).

Settings adds a "Change password" link under the Account section
that opens a modal with current/new/confirm fields and posts to the
new endpoint. Confirm-mismatch and submit-disabled wiring is
client-side; backend errors surface inline.

Tests: 4 new controller tests cover success, wrong-current,
short-new, and missing-fields; success path also re-fetches the
user and checks the hash actually changed.
2026-05-09 15:25:54 -04:00
football2801 bdb717de2e chore(build): drop unused imports + rebuild bundle
CI / test (push) Has been cancelled
vue-tsc -b is stricter than --noEmit; the StickerTray emoji input
ref and the StickerCanvas customAssetUrl import were unused.
2026-05-09 15:18:29 -04:00