feat(home): live updates via Mercure — server pushes device state to the PWA
CI / test (push) Has been cancelled
CI / test (push) Has been cancelled
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>
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import{_ as e,d as t,f as n,g as r,j as i,k as a,m as o,pt as s,s as c,t as l,u,v as d,z as f}from"./_plugin-vue_export-helper-DRLwVS0w.js";import{n as p,t as m}from"./BaseBottomSheet-CV7IMg-N.js";var h={class:`device-picker__list`},g=[`checked`,`onChange`],_={class:`device-picker__name`},v={class:`device-picker__orientation`},y=l(d({__name:`DevicePicker`,props:{modelValue:{type:Boolean},devices:{},selected:{},uploading:{type:Boolean}},emits:[`update:modelValue`,`update:selected`,`confirm`],setup(l,{emit:d}){let y=l,b=d;function x(e){y.selected.includes(e)?b(`update:selected`,y.selected.filter(t=>t!==e)):b(`update:selected`,[...y.selected,e])}let S=u(()=>{let e=y.selected.length;return e===0?`Add to frame`:`Add to ${e} frame${e>1?`s`:``}`});return(u,d)=>(a(),n(m,{"model-value":l.modelValue,label:`Choose frames`,"onUpdate:modelValue":d[1]||=e=>u.$emit(`update:modelValue`,e)},{default:f(()=>[d[2]||=t(`h2`,{class:`device-picker__title`},`Add to frames`,-1),d[3]||=t(`p`,{class:`device-picker__sub`},`Choose which frames will show this photo.`,-1),t(`div`,h,[(a(!0),o(c,null,i(l.devices,e=>(a(),o(`label`,{key:e.id,class:`device-picker__row`},[t(`input`,{type:`checkbox`,class:`device-picker__check`,checked:l.selected.includes(e.id),onChange:t=>x(e.id)},null,40,g),t(`span`,_,s(e.name),1),t(`span`,v,s(e.orientation),1)]))),128))]),e(p,{variant:`primary`,class:`device-picker__confirm`,disabled:l.selected.length===0||l.uploading,onClick:d[0]||=e=>u.$emit(`confirm`)},{default:f(()=>[r(s(l.uploading?`Uploading…`:S.value),1)]),_:1},8,[`disabled`])]),_:1},8,[`model-value`]))}}),[[`__scopeId`,`data-v-a6466fa5`]]);export{y as t};
|
||||
@@ -1 +0,0 @@
|
||||
import{A as e,O as t,R as n,_ as r,d as i,ft as a,g as o,h as s,l as c,o as l,p as u,t as d,u as f}from"./_plugin-vue_export-helper-CeYnMxKK.js";import{n as p,t as m}from"./BaseBottomSheet-Y2PW4H1i.js";var h={class:`device-picker__list`},g=[`checked`,`onChange`],_={class:`device-picker__name`},v={class:`device-picker__orientation`},y=d(r({__name:`DevicePicker`,props:{modelValue:{type:Boolean},devices:{},selected:{},uploading:{type:Boolean}},emits:[`update:modelValue`,`update:selected`,`confirm`],setup(r,{emit:d}){let y=r,b=d;function x(e){y.selected.includes(e)?b(`update:selected`,y.selected.filter(t=>t!==e)):b(`update:selected`,[...y.selected,e])}let S=c(()=>{let e=y.selected.length;return e===0?`Add to frame`:`Add to ${e} frame${e>1?`s`:``}`});return(c,d)=>(t(),i(m,{"model-value":r.modelValue,label:`Choose frames`,"onUpdate:modelValue":d[1]||=e=>c.$emit(`update:modelValue`,e)},{default:n(()=>[d[2]||=f(`h2`,{class:`device-picker__title`},`Add to frames`,-1),d[3]||=f(`p`,{class:`device-picker__sub`},`Choose which frames will show this photo.`,-1),f(`div`,h,[(t(!0),u(l,null,e(r.devices,e=>(t(),u(`label`,{key:e.id,class:`device-picker__row`},[f(`input`,{type:`checkbox`,class:`device-picker__check`,checked:r.selected.includes(e.id),onChange:t=>x(e.id)},null,40,g),f(`span`,_,a(e.name),1),f(`span`,v,a(e.orientation),1)]))),128))]),o(p,{variant:`primary`,class:`device-picker__confirm`,disabled:r.selected.length===0||r.uploading,onClick:d[0]||=e=>c.$emit(`confirm`)},{default:n(()=>[s(a(r.uploading?`Uploading…`:S.value),1)]),_:1},8,[`disabled`])]),_:1},8,[`model-value`]))}}),[[`__scopeId`,`data-v-a6466fa5`]]);export{y as t};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import{H as e,M as t,d as n,dt as r,ft as i,k as a,m as o,t as s,u as c,v as l}from"./_plugin-vue_export-helper-DRLwVS0w.js";var u={key:1,class:`ptr__spinner`,role:`status`,"aria-label":`Refreshing`},d=s(l({__name:`PullToRefresh`,props:{isAtTop:{},onRefresh:{},threshold:{default:80},maxPull:{default:140}},setup(s){let l=s,d=e(0),f=e(!1),p=0,m=0,h=!1,g=null,_=c(()=>Math.min(d.value/l.threshold,1)),v=c(()=>f.value?1:Math.min(d.value/l.threshold,1)),y=c(()=>({transform:`translateY(${d.value}px)`,transition:h?`none`:`transform 240ms cubic-bezier(0.2, 0.8, 0.2, 1)`}));function b(e){f.value||l.isAtTop&&!l.isAtTop()||e.touches.length===1&&(p=e.touches[0].clientY,m=e.touches[0].clientX,h=!0,g=null)}function x(e){if(!h)return;let t=e.touches[0].clientX-m,n=e.touches[0].clientY-p;if(g===null){if(Math.abs(t)<6&&Math.abs(n)<6)return;g=Math.abs(t)>Math.abs(n)?`x`:`y`}if(g===`x`||n<=0){h=!1,d.value=0;return}if(l.isAtTop&&!l.isAtTop()){h=!1,d.value=0;return}let r=n<l.maxPull?n*.5:l.maxPull*.5+(n-l.maxPull)*.1;d.value=Math.min(r,l.maxPull*.7),e.cancelable&&e.preventDefault()}async function S(){if(h)if(h=!1,d.value>=l.threshold){f.value=!0,d.value=l.threshold*.7;try{await l.onRefresh()}catch(e){console.error(`Pull-to-refresh failed:`,e)}finally{f.value=!1,d.value=0}}else d.value=0}return(e,s)=>(a(),o(`div`,{class:`ptr`,onTouchstartPassive:b,onTouchmove:x,onTouchend:S,onTouchcancel:S},[n(`div`,{class:r([`ptr__indicator`,{"ptr__indicator--ready":_.value>=1}]),style:i({opacity:v.value,transform:`translateY(${d.value*.6}px)`}),"aria-hidden":`true`},[f.value?(a(),o(`div`,u)):(a(),o(`svg`,{key:0,class:`ptr__arrow`,viewBox:`0 0 24 24`,style:i({transform:`rotate(${_.value*180}deg)`})},[...s[0]||=[n(`line`,{x1:`12`,y1:`5`,x2:`12`,y2:`19`,stroke:`currentColor`,"stroke-width":`2.5`,"stroke-linecap":`round`},null,-1),n(`polyline`,{points:`6 13 12 19 18 13`,stroke:`currentColor`,"stroke-width":`2.5`,fill:`none`,"stroke-linecap":`round`,"stroke-linejoin":`round`},null,-1)]],4))],6),n(`div`,{class:`ptr__content`,style:i(y.value)},[t(e.$slots,`default`,{},void 0,!0)],4)],32))}}),[[`__scopeId`,`data-v-e2242f3c`]]);export{d as t};
|
||||
@@ -1 +0,0 @@
|
||||
import{O as e,V as t,_ as n,dt as r,j as i,l as a,p as o,t as s,u as c,ut as l}from"./_plugin-vue_export-helper-CeYnMxKK.js";var u={key:1,class:`ptr__spinner`,role:`status`,"aria-label":`Refreshing`},d=s(n({__name:`PullToRefresh`,props:{isAtTop:{},onRefresh:{},threshold:{default:80},maxPull:{default:140}},setup(n){let s=n,d=t(0),f=t(!1),p=0,m=0,h=!1,g=null,_=a(()=>Math.min(d.value/s.threshold,1)),v=a(()=>f.value?1:Math.min(d.value/s.threshold,1)),y=a(()=>({transform:`translateY(${d.value}px)`,transition:h?`none`:`transform 240ms cubic-bezier(0.2, 0.8, 0.2, 1)`}));function b(e){f.value||s.isAtTop&&!s.isAtTop()||e.touches.length===1&&(p=e.touches[0].clientY,m=e.touches[0].clientX,h=!0,g=null)}function x(e){if(!h)return;let t=e.touches[0].clientX-m,n=e.touches[0].clientY-p;if(g===null){if(Math.abs(t)<6&&Math.abs(n)<6)return;g=Math.abs(t)>Math.abs(n)?`x`:`y`}if(g===`x`||n<=0){h=!1,d.value=0;return}if(s.isAtTop&&!s.isAtTop()){h=!1,d.value=0;return}let r=n<s.maxPull?n*.5:s.maxPull*.5+(n-s.maxPull)*.1;d.value=Math.min(r,s.maxPull*.7),e.cancelable&&e.preventDefault()}async function S(){if(h)if(h=!1,d.value>=s.threshold){f.value=!0,d.value=s.threshold*.7;try{await s.onRefresh()}catch(e){console.error(`Pull-to-refresh failed:`,e)}finally{f.value=!1,d.value=0}}else d.value=0}return(t,n)=>(e(),o(`div`,{class:`ptr`,onTouchstartPassive:b,onTouchmove:x,onTouchend:S,onTouchcancel:S},[c(`div`,{class:l([`ptr__indicator`,{"ptr__indicator--ready":_.value>=1}]),style:r({opacity:v.value,transform:`translateY(${d.value*.6}px)`}),"aria-hidden":`true`},[f.value?(e(),o(`div`,u)):(e(),o(`svg`,{key:0,class:`ptr__arrow`,viewBox:`0 0 24 24`,style:r({transform:`rotate(${_.value*180}deg)`})},[...n[0]||=[c(`line`,{x1:`12`,y1:`5`,x2:`12`,y2:`19`,stroke:`currentColor`,"stroke-width":`2.5`,"stroke-linecap":`round`},null,-1),c(`polyline`,{points:`6 13 12 19 18 13`,stroke:`currentColor`,"stroke-width":`2.5`,fill:`none`,"stroke-linecap":`round`,"stroke-linejoin":`round`},null,-1)]],4))],6),c(`div`,{class:`ptr__content`,style:r(y.value)},[i(t.$slots,`default`,{},void 0,!0)],4)],32))}}),[[`__scopeId`,`data-v-e2242f3c`]]);export{d as t};
|
||||
@@ -0,0 +1 @@
|
||||
import{K as e,d as t,dt as n,ft as r,j as i,k as a,m as o,p as s,pt as c,s as l,t as u,u as d,v as f}from"./_plugin-vue_export-helper-DRLwVS0w.js";import{n as p,r as m,t as h}from"./index-CJkFKvc-.js";var g={class:`settings`},_={class:`settings__section`},v={class:`theme-grid`,role:`radiogroup`,"aria-label":`Choose theme`},y=[`aria-checked`,`aria-label`,`onClick`],b={class:`theme-swatch__label`},x={key:0,class:`theme-swatch__check`,"aria-hidden":`true`},S={class:`settings__section`},C={class:`settings__row`},w={class:`settings__row-value`},T=u(f({__name:`SettingsView`,setup(u){let f=m(),{saveTheme:T}=p(),E=d(()=>f.user?.theme??`warm-craft`);function D(e){T(e)}return(u,d)=>(a(),o(`main`,g,[d[5]||=t(`h1`,{class:`settings__title`},`Settings`,-1),t(`section`,_,[d[1]||=t(`h2`,{class:`settings__section-title`},`Theme`,-1),t(`div`,v,[(a(!0),o(l,null,i(e(h),e=>(a(),o(`button`,{key:e.id,type:`button`,role:`radio`,"aria-checked":E.value===e.id,"aria-label":e.label,class:n([`theme-swatch`,{"theme-swatch--active":E.value===e.id}]),style:r({"--swatch-bg":e.bg,"--swatch-primary":e.primary,"--swatch-text":e.text}),onClick:t=>D(e.id)},[d[0]||=t(`span`,{class:`theme-swatch__preview`,"aria-hidden":`true`},[t(`span`,{class:`theme-swatch__bar`}),t(`span`,{class:`theme-swatch__dot`})],-1),t(`span`,b,c(e.label),1),E.value===e.id?(a(),o(`span`,x,`✓`)):s(``,!0)],14,y))),128))])]),t(`section`,S,[d[3]||=t(`h2`,{class:`settings__section-title`},`Account`,-1),t(`div`,C,[d[2]||=t(`span`,{class:`settings__row-label`},`Signed in as`,-1),t(`span`,w,c(e(f).user?.email),1)]),d[4]||=t(`a`,{href:`/logout`,class:`settings__logout`},`Sign out`,-1)])]))}}),[[`__scopeId`,`data-v-76ec3881`]]);export{T as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{A as e,G as t,O as n,_ as r,dt as i,f as a,ft as o,l as s,o as c,p as l,t as u,u as d,ut as f}from"./_plugin-vue_export-helper-CeYnMxKK.js";import{n as p,r as m,t as h}from"./index-DabTPrXi.js";var g={class:`settings`},_={class:`settings__section`},v={class:`theme-grid`,role:`radiogroup`,"aria-label":`Choose theme`},y=[`aria-checked`,`aria-label`,`onClick`],b={class:`theme-swatch__label`},x={key:0,class:`theme-swatch__check`,"aria-hidden":`true`},S={class:`settings__section`},C={class:`settings__row`},w={class:`settings__row-value`},T=u(r({__name:`SettingsView`,setup(r){let u=m(),{saveTheme:T}=p(),E=s(()=>u.user?.theme??`warm-craft`);function D(e){T(e)}return(r,s)=>(n(),l(`main`,g,[s[5]||=d(`h1`,{class:`settings__title`},`Settings`,-1),d(`section`,_,[s[1]||=d(`h2`,{class:`settings__section-title`},`Theme`,-1),d(`div`,v,[(n(!0),l(c,null,e(t(h),e=>(n(),l(`button`,{key:e.id,type:`button`,role:`radio`,"aria-checked":E.value===e.id,"aria-label":e.label,class:f([`theme-swatch`,{"theme-swatch--active":E.value===e.id}]),style:i({"--swatch-bg":e.bg,"--swatch-primary":e.primary,"--swatch-text":e.text}),onClick:t=>D(e.id)},[s[0]||=d(`span`,{class:`theme-swatch__preview`,"aria-hidden":`true`},[d(`span`,{class:`theme-swatch__bar`}),d(`span`,{class:`theme-swatch__dot`})],-1),d(`span`,b,o(e.label),1),E.value===e.id?(n(),l(`span`,x,`✓`)):a(``,!0)],14,y))),128))])]),d(`section`,S,[s[3]||=d(`h2`,{class:`settings__section-title`},`Account`,-1),d(`div`,C,[s[2]||=d(`span`,{class:`settings__row-label`},`Signed in as`,-1),d(`span`,w,o(t(u).user?.email),1)]),s[4]||=d(`a`,{href:`/logout`,class:`settings__logout`},`Sign out`,-1)])]))}}),[[`__scopeId`,`data-v-76ec3881`]]);export{T as default};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -14,8 +14,8 @@
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="apple-mobile-web-app-title" content="pictureFrame" />
|
||||
<script type="module" crossorigin src="/build/assets/index-DabTPrXi.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/build/assets/_plugin-vue_export-helper-CeYnMxKK.js">
|
||||
<script type="module" crossorigin src="/build/assets/index-CJkFKvc-.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/build/assets/_plugin-vue_export-helper-DRLwVS0w.js">
|
||||
<link rel="stylesheet" crossorigin href="/build/assets/index-BlLBHR1q.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user