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>
110 lines
2.8 KiB
JavaScript
110 lines
2.8 KiB
JavaScript
/**
|
|
* 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())
|
|
}
|
|
})
|