feat(frontend): Service Worker WS, mode édition dashboard, sidebar click-to-toggle
- WebSocket via Service Worker (ws.sw.js) : connexions persistantes entre navigations Swup, reconnexion exponentielle, protocole WS_SUBSCRIBE/WS_UNSUBSCRIBE/WS_SEND - WsProxy dans app.js : abstraction SW + fallback WebSocket direct - proxmoxPage migré vers WsProxy (identique au dashboardPage) - Dashboard : mode édition toggle — DnD, resize (x1/x2), masquer/afficher widget uniquement actifs en mode édition ; preview drag (is-dragging/drag-over) - Sidebar : suppression bouton hamburger, clic sur sidebar-header pour replier - pages.css : targets-grid 350px, styles edit mode, widget-size-2, drag preview - neu.css : sidebar-header cursor pointer, suppression .sidebar-toggle Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b6d6355c6c
commit
cbfb20505d
11 changed files with 359 additions and 128 deletions
86
frontend/js/ws.sw.js
Normal file
86
frontend/js/ws.sw.js
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
* ProxmoxPanel — WebSocket Service Worker
|
||||
* Maintient les connexions WS en vie entre les navigations Swup.
|
||||
* Les pages s'abonnent/désabonnent via postMessage.
|
||||
*/
|
||||
'use strict'
|
||||
|
||||
// channel → { ws: WebSocket|null, url: string, clientIds: Set<string>, retryDelay: number }
|
||||
const connections = new Map()
|
||||
|
||||
self.addEventListener('install', () => self.skipWaiting())
|
||||
|
||||
self.addEventListener('activate', e =>
|
||||
e.waitUntil(self.clients.claim())
|
||||
)
|
||||
|
||||
self.addEventListener('message', event => {
|
||||
const { type, channel, url, payload } = event.data || {}
|
||||
if (!channel) return
|
||||
const clientId = event.source?.id
|
||||
if (!clientId) return
|
||||
|
||||
switch (type) {
|
||||
case 'WS_SUBSCRIBE': {
|
||||
let conn = connections.get(channel)
|
||||
if (!conn) {
|
||||
conn = { ws: null, url: null, clientIds: new Set(), retryDelay: 2000 }
|
||||
connections.set(channel, conn)
|
||||
}
|
||||
conn.clientIds.add(clientId)
|
||||
if (url) conn.url = url
|
||||
ensureWS(channel)
|
||||
break
|
||||
}
|
||||
case 'WS_UNSUBSCRIBE': {
|
||||
const conn = connections.get(channel)
|
||||
if (conn) conn.clientIds.delete(clientId)
|
||||
break
|
||||
}
|
||||
case 'WS_SEND': {
|
||||
const conn = connections.get(channel)
|
||||
if (conn?.ws?.readyState === 1) conn.ws.send(payload)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function ensureWS(channel) {
|
||||
const conn = connections.get(channel)
|
||||
if (!conn?.url) return
|
||||
if (conn.ws && conn.ws.readyState < 2) return // CONNECTING (0) ou OPEN (1)
|
||||
|
||||
const ws = new WebSocket(conn.url)
|
||||
conn.ws = ws
|
||||
conn.retryDelay = 2000
|
||||
|
||||
notify(channel, 'WS_STATUS', { status: 'connecting' })
|
||||
|
||||
ws.onopen = () => {
|
||||
conn.retryDelay = 2000
|
||||
notify(channel, 'WS_STATUS', { status: 'connected' })
|
||||
}
|
||||
|
||||
ws.onmessage = e => notify(channel, 'WS_MESSAGE', { data: e.data })
|
||||
|
||||
ws.onerror = () => notify(channel, 'WS_STATUS', { status: 'error' })
|
||||
|
||||
ws.onclose = () => {
|
||||
notify(channel, 'WS_STATUS', { status: 'disconnected' })
|
||||
// Backoff exponentiel plafonné à 30s
|
||||
const delay = conn.retryDelay
|
||||
conn.retryDelay = Math.min(conn.retryDelay * 1.5, 30000)
|
||||
setTimeout(() => ensureWS(channel), delay)
|
||||
}
|
||||
}
|
||||
|
||||
async function notify(channel, type, extra) {
|
||||
const conn = connections.get(channel)
|
||||
if (!conn || conn.clientIds.size === 0) return
|
||||
const allClients = await self.clients.matchAll({ type: 'window' })
|
||||
for (const client of allClients) {
|
||||
if (conn.clientIds.has(client.id)) {
|
||||
client.postMessage({ channel, type, ...extra })
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue