From 10f9683e812ccd88a3aff35d1307cf65f19af626 Mon Sep 17 00:00:00 2001 From: jessikitty Date: Wed, 10 Jun 2026 07:02:57 +0000 Subject: [PATCH] v1.4.3 - persistent device ID via localStorage so refresh reuses same client slot --- docker-compose.yml | 26 +++++++++----------------- public/js/app.js | 3 ++- server.js | 10 ++++++++-- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b38d33e..e6cb8cd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,30 +9,22 @@ services: - "3030:3000" environment: # REQUIRED - - IMMICH_URL=http://your-immich-server:2283 - - IMMICH_API_KEY=your-api-key-here + - IMMICH_URL=http://10.0.0.40:2283 + - IMMICH_API_KEY=aQAm76nwCi5I68vFWz18irMmnyrzI5yzcb74rXvt4 + - ADMIN_USERNAME=jessikitty + - ADMIN_PASSWORD=23Pinkpr!ncesses + - FRAMBE_API_TOKEN=whosethatgirl-itsjess # Slideshow - - SLIDESHOW_INTERVAL=30 - - TRANSITION_DURATION=2 + - SLIDESHOW_INTERVAL=300 + - TRANSITION_DURATION=5 - IMAGE_FIT=contain - SHUFFLE=true - BACKGROUND_BLUR=true - - REFRESH_INTERVAL=300 # Seconds between album/person refresh checks + - REFRESH_INTERVAL=300 # Seconds between album/person refresh checks # Overlays - SHOW_CLOCK=true - SHOW_DATE=true - - SHOW_EXIF=true + - SHOW_EXIF=false - SHOW_PROGRESS=true - - # Admin Authentication (leave ADMIN_PASSWORD blank to disable login) - - ADMIN_USERNAME=admin - # - ADMIN_PASSWORD=changeme - - # API Token for external access (Home Assistant, scripts, etc.) - # - FRAMBE_API_TOKEN=your-secret-token-here - - # Auto-start (optional — or use URL params instead) - # - ALBUM_ID= - # - SHOW_FAVORITES_ONLY=false diff --git a/public/js/app.js b/public/js/app.js index 0d40255..4962955 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -7,10 +7,11 @@ var currentVideoPlaying = false, pileCanvas, pileCtx; var FRAME_PAD_RATIO = 0.03, FRAME_BOTTOM_RATIO = 0.10, FRAME_COLOR = '#ede8df'; var wsConn = null, clientId = null, isSleeping = false; + var persistentId = (function(){ var k='frambe_pid'; var v=localStorage.getItem(k); if(vi docker-compose.yml ){ v='fp-'+Math.random().toString(36).substr(2,9)+'-'+Date.now().toString(36); localStorage.setItem(k,v); } return v; })(); var $setupScreen=document.getElementById('setup-screen'),$slideshowScreen=document.getElementById('slideshow-screen'),$connectionStatus=document.getElementById('connection-status'),$setupContent=document.getElementById('setup-content'),$setupError=document.getElementById('setup-error'),$errorDetail=document.getElementById('error-detail'),$albumsList=document.getElementById('albums-list'),$btnStart=document.getElementById('btn-start'),$bgBlur=document.getElementById('bg-blur'),$mainFrame=document.getElementById('main-frame'),$mainPhoto=document.getElementById('main-photo'),$mainVideo=document.getElementById('main-video'),$clock=document.getElementById('clock'),$dateDisplay=document.getElementById('date-display'),$exifInfo=document.getElementById('exif-info'),$progressFill=document.getElementById('progress-fill'),$overlay=document.getElementById('overlay'),$btnSettings=document.getElementById('btn-settings'),$progressBar=document.getElementById('progress-bar'); // === WEBSOCKET === - function connectWebSocket(){var proto=location.protocol==='https:'?'wss:':'ws:';wsConn=new WebSocket(proto+'//'+location.host+'/ws');wsConn.onopen=function(){console.log('[Frambe] WebSocket connected');wsConn.send(JSON.stringify({type:'register',role:'frame',status:isRunning?'playing':(isSleeping?'sleeping':'idle'),config:getCurrentConfig()}));};wsConn.onmessage=function(e){try{var msg=JSON.parse(e.data);if(msg.type==='welcome'){clientId=msg.clientId;console.log('[Frambe] Registered as '+clientId);}else if(msg.type==='command'){handleRemoteCommand(msg.action,msg.payload||{});}}catch(err){}};wsConn.onclose=function(){setTimeout(connectWebSocket,5000);};} + function connectWebSocket(){var proto=location.protocol==='https:'?'wss:':'ws:';wsConn=new WebSocket(proto+'//'+location.host+'/ws');wsConn.onopen=function(){console.log('[Frambe] WebSocket connected');wsConn.send(JSON.stringify({type:'register',role:'frame',persistentId:persistentId,status:isRunning?'playing':(isSleeping?'sleeping':'idle'),config:getCurrentConfig()}));};wsConn.onmessage=function(e){try{var msg=JSON.parse(e.data);if(msg.type==='welcome'){clientId=msg.clientId;console.log('[Frambe] Registered as '+clientId);}else if(msg.type==='command'){handleRemoteCommand(msg.action,msg.payload||{});}}catch(err){}};wsConn.onclose=function(){setTimeout(connectWebSocket,5000);};} function sendStatus(s){if(wsConn&&wsConn.readyState===WebSocket.OPEN)wsConn.send(JSON.stringify({type:'status',status:s,currentAlbum:selectedAlbumId,config:getCurrentConfig()}));} function getCurrentConfig(){return{slideshowInterval:config.slideshowInterval,showClock:config.showClock,showDate:config.showDate,showExif:config.showExif,showProgress:config.showProgress};} function handleRemoteCommand(action,payload){console.log('[Frambe] Remote: '+action);switch(action){case'setSource':selectedSource=payload.source;selectedAlbumId=payload.albumId||null;selectedPersonId=payload.personId||null;if(isSleeping)wakeUp();if(isRunning){clearTimeout(slideshowTimer);stopVideo();}doStartSlideshow();break;case'start':if(isSleeping)wakeUp();if(!isRunning&&selectedSource)doStartSlideshow();break;case'stop':if(isRunning)exitSlideshowInternal();sendStatus('idle');break;case'next':if(isRunning)showNextAsset();break;case'prev':if(isRunning)showPrevAsset();break;case'sleep':goToSleep();break;case'wake':wakeUp();break;case'refresh':location.reload();break;case'setConfig':applyConfigChange(payload);break;}} diff --git a/server.js b/server.js index 683bf1e..d6e49ce 100644 --- a/server.js +++ b/server.js @@ -90,8 +90,14 @@ wss.on('connection', (ws, req) => { log('WS admin: ' + ip); ws.send(JSON.stringify({ type: 'clientList', clients: getClientList() })); } else { - clients.set(id, { id, ws, name:'', ip, userAgent:ua, connectedAt:now, firstSeen:now, lastSeen:Date.now(), status:msg.status||'idle', config:msg.config||{}, source:msg.source||null }); - log('WS frame: ' + id + ' (' + ip + ')'); + const pid = msg.persistentId || null; + const existing = pid ? Array.from(clients.values()).find(c => c.persistentId === pid) : null; + const effectiveId = existing ? existing.id : id; + const firstName = existing ? existing.name : ''; + const firstSeen = existing ? existing.firstSeen : now; + if (existing) { clients.delete(Array.from(clients.entries()).find(([,c]) => c.persistentId === pid)?.[0]); } + clients.set(effectiveId, { id:effectiveId, persistentId:pid, ws, name:firstName, ip, userAgent:ua, connectedAt:now, firstSeen, lastSeen:Date.now(), status:msg.status||'idle', config:msg.config||{}, source:msg.source||null }); + log('WS frame: ' + effectiveId + (pid?' [persistent]':'') + ' (' + ip + ')'); broadcastAdminClients(); } } else if (msg.type === 'ping') {