- 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>
86 lines
2.4 KiB
JavaScript
86 lines
2.4 KiB
JavaScript
/**
|
|
* 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 })
|
|
}
|
|
}
|
|
}
|