feat: réécriture frontend Alpine.js + HTMX + Swup (branche frontend/alpine)
Remplace Vue 3 / Vite / TypeScript par une stack légère statique : - Alpine.js v3 : réactivité inline, stores auth/ui/i18n, composants par page - HTMX v2 : interactions serveur via attributs HTML - Swup v4 : transitions de page (bundlé via esbuild, IIFE browser-loadable) - xterm.js v5 : terminal PTY (bundlé via esbuild) Structure : HTML statiques + js/app.js + js/terminal.js + css/ + locales/ Build : esbuild (bundle Swup + xterm seulement) → dist/ → Nginx Dockerfile simplifié : node:22-alpine build → nginx:1.27-alpine serve Pages : index, install, login, dashboard, proxmox, updates, terminal, settings, modules URLs propres via nginx try_files $uri.html Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7ba0ff143c
commit
2098c80ec1
48 changed files with 2446 additions and 5317 deletions
110
frontend/js/terminal.js
Normal file
110
frontend/js/terminal.js
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/**
|
||||
* ProxmoxPanel — Terminal page logic
|
||||
* xterm.js + WebSocket PTY
|
||||
* Chargé uniquement sur terminal.html
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Les classes Terminal et FitAddon sont exposées par xterm.iife.js
|
||||
if (typeof Terminal === 'undefined' || typeof FitAddon === 'undefined') {
|
||||
console.error('xterm.js not loaded')
|
||||
return
|
||||
}
|
||||
|
||||
const term = new Terminal({
|
||||
cursorBlink: true,
|
||||
fontFamily: '"JetBrains Mono", "Fira Code", "Cascadia Code", monospace',
|
||||
fontSize: 14,
|
||||
theme: {
|
||||
background: 'var(--bg-secondary, #1a1a2e)',
|
||||
foreground: 'var(--text-primary, #e2e8f0)',
|
||||
cursor: 'var(--accent-primary, #6366f1)',
|
||||
},
|
||||
})
|
||||
|
||||
const fitAddon = new FitAddon()
|
||||
term.loadAddon(fitAddon)
|
||||
|
||||
const container = document.getElementById('terminal-container')
|
||||
if (!container) return
|
||||
|
||||
term.open(container)
|
||||
fitAddon.fit()
|
||||
|
||||
// Connexion WebSocket PTY
|
||||
const proto = location.protocol === 'https:' ? 'wss' : 'ws'
|
||||
const token = localStorage.getItem('pxp_token')
|
||||
const ws = new WebSocket(`${proto}://${location.host}/ws/terminal?token=${encodeURIComponent(token || '')}`)
|
||||
ws.binaryType = 'arraybuffer'
|
||||
|
||||
const statusEl = document.getElementById('terminal-status')
|
||||
function setStatus(text, cls) {
|
||||
if (statusEl) {
|
||||
statusEl.textContent = text
|
||||
statusEl.className = 'terminal-status ' + (cls || '')
|
||||
}
|
||||
}
|
||||
|
||||
setStatus('Connexion…', 'connecting')
|
||||
|
||||
ws.onopen = () => {
|
||||
setStatus('Connecté', 'connected')
|
||||
// Envoyer la taille initiale du terminal
|
||||
sendResize()
|
||||
}
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
if (e.data instanceof ArrayBuffer) {
|
||||
term.write(new Uint8Array(e.data))
|
||||
} else {
|
||||
term.write(e.data)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
setStatus('Déconnecté', 'disconnected')
|
||||
term.writeln('\r\n\x1b[31m[Connexion terminée]\x1b[0m')
|
||||
}
|
||||
|
||||
ws.onerror = () => {
|
||||
setStatus('Erreur', 'error')
|
||||
}
|
||||
|
||||
// Envoyer l'input utilisateur au serveur
|
||||
term.onData((data) => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(data)
|
||||
}
|
||||
})
|
||||
|
||||
// Envoyer la taille du terminal lors du redimensionnement
|
||||
function sendResize() {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'resize',
|
||||
cols: term.cols,
|
||||
rows: term.rows,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
fitAddon.fit()
|
||||
sendResize()
|
||||
})
|
||||
resizeObserver.observe(container)
|
||||
|
||||
// Nettoyage quand Swup navigue hors de la page
|
||||
document.addEventListener('swup:page:view', () => {
|
||||
if (!document.getElementById('terminal-container')) {
|
||||
ws.close()
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
})
|
||||
|
||||
// Bouton "Effacer"
|
||||
const clearBtn = document.getElementById('terminal-clear')
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', () => term.clear())
|
||||
}
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue