Commit Graph

9 Commits

Author SHA1 Message Date
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 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>
2026-05-14 13:02:26 -04:00
football2801 8beb7331dd fix(home): preview tracks frame state even with locked images and 304 polls
CI / test (push) Has been cancelled
Three problems were stacked:

1. The 200 serving path didn't set currentImage when a locked image was
   served (RotationService.advance bypassed). The frame got the locked
   photo; the DB kept the previous one; Home showed the old one.

2. The 304 path didn't flush at all. lastSeenAt (markSeen) was lost on
   every no-change poll, and any drift in currentImage couldn't self-heal.
   For a frame that's been locked for a while, polls cycle as 304 forever
   and the DB stays wrong indefinitely.

3. Pull-to-refresh fetched via fetchDevices(), which flips loading=true
   and replaces the cards with "Loading…" mid-fetch. The PTR spinner was
   working but users couldn't see the result of their refresh.

Fixes:
- Both 200 and 304 paths now set currentImage = $image and flush. The
  304 path becomes self-healing for any device whose currentImage drifted
  from reality (e.g., from before the 200-path fix).
- fetchDevices / fetchImages take an optional { silent: true } that
  skips toggling loading.value. PTR refresh callbacks pass silent so
  the cards stay visible during background refresh.
- HomeView also listens on visibilitychange and silently re-fetches when
  the PWA returns to foreground, so reopening the app shows current
  state without a manual pull.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 19:24:50 -04:00
football2801 2cd558bac3 fix(home): preview reflects what's on the frame, not what's queued
CI / test (push) Has been cancelled
Both the backend preview endpoint and the frontend cache-buster were
preferring lockedImage over currentImage. Locking is a queued override
that doesn't take effect until the device's next poll, so showing it on
Home before the device has actually pulled it lied about the frame's
state. Always use currentImage now.

Also: add a primary "+ Add Photo" button at the top of the Library page
so users can upload without bouncing back to Home; updates the empty-
state copy to point at the new button.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 19:15:14 -04:00
football2801 328ad632d3 feat: pull-to-refresh on Home and Library
CI / test (push) Has been cancelled
iOS standalone PWAs don't get Safari's native pull-to-refresh, so add
our own. New <PullToRefresh> component handles the gesture: dampened
drag past an 80px threshold triggers an async onRefresh; below that it
springs back. Swipe direction is locked to the first 6px of movement,
so horizontal carousel swipes (landscape Home) don't accidentally fire
PTR. The arrow icon rotates from 0° to 180° as the pull approaches the
threshold and turns primary-color when ready; during refresh a CSS
spinner replaces it.

- HomeView refreshes the device list (and sync status with it)
- LibraryView refreshes images, pending-share count, devices, and the
  active shared sub-tab page when it's the one in view

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 19:09:52 -04:00
football2801 5fcfb806be feat: ship as a real iOS-installable PWA, restructure bottom nav, fix safe-area
CI / test (push) Has been cancelled
- Add manifest.webmanifest with standalone display + warm-craft theme colors,
  apple-touch-icon, and 192/512/512-maskable icons (frame-with-sunset glyph).
- Add PWA meta tags + viewport-fit=cover so add-to-home-screen produces a
  true standalone app on iOS instead of a Safari bookmark.
- Drop the Shared bottom-nav tab; the in-page sub-tabs already cover that.
  Three nav tabs total (Home / Library / Settings); pending-share badge
  moves to the Library tab. Predicate-based isActive() now correctly
  disambiguates /library vs /library?tab=shared.
- Safe-area handling: bottom nav, bottom sheet, upload overlay, and #app
  respect env(safe-area-inset-*); sticky Library tabs anchor below the
  iPhone status bar. Introduces --bottom-nav-height token consumed by
  Settings, Library, and the toast.
- LibraryView reactively follows route.query.tab so deep-linking
  /library?tab=shared lands on the right sub-tab.
- Theme-color meta syncs client-side via useTheme.applyTheme so the
  user's chosen theme follows them into Android Chrome's chrome bar.

Test suite expanded to 278 tests / 100% line coverage (99.84% statements,
99.78% branches). Remaining gaps are unreachable defensive code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 18:07:16 -04:00
football2801 cbb5bb1ff3 feat: surface orientation-mismatch warning in the library
CI / test (push) Has been cancelled
Each thumbnail now shows a yellow warning triangle in its action stack
when at least one approved device's orientation does not match the
photo's crop orientation. Tap opens the edit flow with that device set
as the crop context, so the existing in-crop-tool indicator can guide
the re-crop. Photos without a stored cropOrientation fall back to
inferring it from the saved cropParams aspect, so older uploads aren't
left blind.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:25:55 -04:00
football2801 12245759ac chore: stage all in-progress work before repo split
CI / test (push) Has been cancelled
Web app: new entities (Image, RenderedAsset, SharedImage, Token,
DeviceImageHistory), enums, repositories, controllers, message handlers,
migrations, tests, frontend upload/library/sticker UI, Vue components.

Firmware: EPD background screen binaries + gen scripts, setup_bg header.

Infra: ddev config, test bundle, gitignore coverage dir.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 12:11:31 -04:00
football2801 a55b3bd187 feat(story-1.2): Vue 3 SPA scaffold, base component library, User entity, SpaController
Frontend:
- Vue 3 + Vite + TypeScript strict in frontend/; builds to public/build/
- Vue Router (hash-history, requiresAuth guard → /login) and Pinia
- Global SCSS design tokens with 6 full themes (Warm Craft, Playful Pop, Sage & Cream, Dusty Mauve, Ocean Dusk, Honey & Slate)
- Base components: BaseButton (5 variants), BaseInput (floating label, error state),
  BaseBottomSheet (slide-up, focus trap, tap-outside dismiss), BaseCard, BaseChip,
  BaseToast (2.5s auto-dismiss, aria-live polite), BottomNav (4 tabs, hides at 960px)
- Type stubs for all API shapes: User, Device, Image, StickerLayer, RenderedAsset, Token

Backend:
- SpaController catch-all serves public/build/index.html; excludes api/setup/token/login/register
- User entity (email+password+roles+theme); UserRepository with PasswordUpgrader
- SecurityController with /login, /logout, /register stubs; Twig login form
- security.yaml: form_login firewall, remember_me, role_hierarchy, access_control
- Migration: create user table

Verified: npm run build succeeds, GET / → 302 /login (unauthenticated)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 23:21:29 -04:00