Files
pictureFrame-webApp/public/build/assets/BaseBottomSheet-CV7IMg-N.js
T
football2801 ba9625d45d
CI / test (push) Has been cancelled
feat(home): live updates via Mercure — server pushes device state to the PWA
Subscribe per-device with a Symfony Mercure hub: server publishes a fresh
device payload after every poll (200/304/204), every PATCH, and every
lock/unlock. The frontend opens one EventSource per device topic and
splats inbound JSON straight into the devices store — same shape as
GET /api/devices, so no envelope handling.

Topic: https://pictureframe.edholm.me/devices/{id}

Stack mirrors aqua-iq:
- symfony/mercure-bundle + config/packages/mercure.yaml
- App\Service\MercurePublisher (errors swallowed + logged; a flaky hub
  must not break a poll response)
- App\Service\DeviceSerializer extracted as the single source of truth
  for the wire shape (REST + Mercure share it)
- Frontend useDeviceMercure() composable: opens/closes EventSources to
  match the device list reactively, reconnects on hub-side closes
- SpaController exposes MERCURE_PUBLIC_URL via window.__PF_MERCURE_URL__

Production compose adds a dunglas/mercure container with Traefik labels
for pictureframe.edholm.me/.well-known/mercure (handled separately on
the host since the file isn't in this repo).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 16:20:21 -04:00

1 line
4.9 KiB
JavaScript

import{C as e,H as t,M as n,P as r,R as i,_ as a,c as o,d as s,dt as c,f as l,ft as u,k as d,m as f,p,r as m,t as h,v as g,w as _,z as v}from"./_plugin-vue_export-helper-DRLwVS0w.js";import{c as y,d as b,f as x}from"./index-CJkFKvc-.js";var S=m(`devices`,()=>{let e=t([]),n=t(!1),r=t(null);async function i(t={}){t.silent||(n.value=!0),r.value=null;try{let t=await fetch(`/api/devices`);if(!t.ok)throw Error(`Failed to load devices`);e.value=await t.json()}catch(e){r.value=e instanceof Error?e.message:`Unknown error`}finally{n.value=!1}}async function a(t,n){let r=await fetch(`/api/devices/${t}`,{method:`PATCH`,headers:{"Content-Type":`application/json`},body:JSON.stringify(n)});if(!r.ok)throw Error(`Failed to update device`);let i=await r.json(),a=e.value.findIndex(e=>e.id===t);return a!==-1&&(e.value[a]=i),i}async function o(t,n){let r=await fetch(`/api/devices/${t}/lock`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({imageId:n})});if(!r.ok)throw Error(`Failed to lock image`);let i=await r.json(),a=e.value.findIndex(e=>e.id===t);return a!==-1&&(e.value[a]=i),i}async function s(t){let n=await fetch(`/api/devices/${t}/lock`,{method:`DELETE`});if(!n.ok)throw Error(`Failed to unlock`);let r=await n.json(),i=e.value.findIndex(e=>e.id===t);return i!==-1&&(e.value[i]=r),r}return{devices:e,loading:n,error:r,fetchDevices:i,updateDevice:a,lockImage:o,unlockImage:s}}),C=m(`upload`,()=>{let e=t(null),n=t(null),r=t(null),i=t(null),a=t(null),o=t(null),s=t([]),c=t(null),l=t([]),u=t(null);function d(t,r){_(),e.value=t,n.value=URL.createObjectURL(t),c.value=r??null,l.value=r?[r]:[]}async function f(t,r){_();let i=await(await fetch(t.originalUrl)).blob();e.value=new File([i],t.originalFilename,{type:i.type}),n.value=URL.createObjectURL(i),u.value=t.id,a.value=t.cropParams??null,o.value=t.cropOrientation??null,s.value=t.stickerState?[...t.stickerState]:[],l.value=t.approvedDeviceIds,c.value=r??null}function p(e,t,n){i.value&&URL.revokeObjectURL(i.value),r.value=e,i.value=URL.createObjectURL(e),a.value=t,o.value=n}function m(e){s.value=[...s.value,e]}function h(e,t){s.value=s.value.map(n=>n.id===e?{...n,...t}:n)}function g(e){s.value=s.value.filter(t=>t.id!==e)}function _(){n.value&&URL.revokeObjectURL(n.value),i.value&&URL.revokeObjectURL(i.value),e.value=null,n.value=null,r.value=null,i.value=null,a.value=null,o.value=null,s.value=[],c.value=null,l.value=[],u.value=null}return{originalFile:e,originalUrl:n,croppedBlob:r,croppedUrl:i,cropParams:a,cropOrientation:o,stickers:s,contextDeviceId:c,selectedDeviceIds:l,editingImageId:u,init:d,initEdit:f,setCrop:p,addSticker:m,updateSticker:h,removeSticker:g,cleanup:_}}),w={key:0,class:`btn__spinner`,"aria-hidden":`true`},T=h(g({__name:`BaseButton`,props:{variant:{default:`primary`},tag:{default:`button`},type:{default:`button`},disabled:{type:Boolean,default:!1},loading:{type:Boolean,default:!1}},setup(t){return(i,a)=>(d(),l(r(t.tag),e({type:t.tag===`button`?t.type:void 0,disabled:t.disabled||t.loading,class:[`btn`,`btn--${t.variant}`,{"btn--loading":t.loading}]},i.$attrs),{default:v(()=>[t.loading?(d(),f(`span`,w)):p(``,!0),n(i.$slots,`default`,{},void 0,!0)]),_:3},16,[`type`,`disabled`,`class`]))}}),[[`__scopeId`,`data-v-7d3f1e61`]]),E=[`aria-label`],D=80,O=h(g({__name:`BaseBottomSheet`,props:{modelValue:{type:Boolean},label:{}},emits:[`update:modelValue`],setup(e,{emit:r}){let m=e,h=r,g=t(null),S=t(0),C=t(!1),w=0,T=null,O=null;function k(){h(`update:modelValue`,!1)}function A(e){w=e.touches[0].clientY,C.value=!0,S.value=0}function j(e){if(!C.value)return;let t=e.touches[0].clientY-w;S.value=t>0?t:0}function M(){C.value&&(C.value=!1,S.value>D&&k(),S.value=0)}function N(e){e.pointerType!==`touch`&&(w=e.clientY,C.value=!0,S.value=0,T=e.pointerId,e.currentTarget.setPointerCapture(e.pointerId),window.addEventListener(`pointermove`,P),window.addEventListener(`pointerup`,F),window.addEventListener(`pointercancel`,F))}function P(e){if(!C.value||e.pointerId!==T)return;let t=e.clientY-w;S.value=t>0?t:0}function F(e){e.pointerId===T&&(T=null,window.removeEventListener(`pointermove`,P),window.removeEventListener(`pointerup`,F),window.removeEventListener(`pointercancel`,F),M())}return i(()=>m.modelValue,async e=>{e?(O=document.activeElement,await _(),g.value?.focus()):(O?.focus(),O=null,S.value=0,C.value=!1)}),(t,r)=>(d(),l(o,{to:`body`},[a(y,{name:`sheet`},{default:v(()=>[e.modelValue?(d(),f(`div`,{key:0,class:`sheet-overlay`,role:`dialog`,"aria-label":e.label,"aria-modal":`true`,onClick:x(k,[`self`]),onKeydown:b(k,[`esc`])},[s(`div`,{ref_key:`sheetRef`,ref:g,class:c([`sheet`,{"sheet--dragging":C.value}]),style:u(S.value>0?{transform:`translateY(${S.value}px)`}:void 0),tabindex:`-1`},[s(`div`,{class:`sheet__handle-target`,onTouchstartPassive:A,onTouchmovePassive:j,onTouchend:M,onTouchcancel:M,onPointerdown:N,"aria-hidden":`true`},[...r[0]||=[s(`div`,{class:`sheet__handle`},null,-1)]],32),n(t.$slots,`default`,{},void 0,!0)],6)],40,E)):p(``,!0)]),_:3})]))}}),[[`__scopeId`,`data-v-967683c3`]]);export{S as i,T as n,C as r,O as t};