core/frontend/src/views/Terminal.vue
enzo 5dbcb1df07 feat: initialisation complète du CORE ProxmoxPanel
Backend Go 1.23+ :
- API REST + WebSocket (chi, gorilla/websocket)
- Authentification PAM via SSH + JWT RS256
- Chiffrement AES-256-GCM pour secrets SQLite
- Pool SSH, client Proxmox REST, hub WebSocket pub/sub
- Système de modules compilés à initialisation conditionnelle
- Audit log, migrations SQLite versionnées

Frontend Vue 3 + Vite + TypeScript :
- Thème Neumorphism sombre/clair (CSS custom properties)
- Wizard d'installation, Dashboard drag-drop, Terminal xterm.js
- Toutes les vues CORE + stubs modules optionnels
- i18n EN/FR (vue-i18n v11)

Infrastructure :
- Docker multi-stage (Go → alpine, Node → nginx)
- docker-compose.yml, .gitattributes, LICENSE MIT, README

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 21:08:53 +01:00

162 lines
4.4 KiB
Vue

<template>
<div class="terminal-page">
<div class="page-header flex items-center justify-between">
<h2>{{ t('nav.terminal') }}</h2>
<div class="flex gap-sm">
<input v-model="customHost" class="neu-input" placeholder="host:port (défaut: config)" style="width:200px" />
<button class="neu-btn neu-btn--primary" @click="reconnect">
{{ connected ? t('terminal.reconnect') : t('terminal.connect') }}
</button>
</div>
</div>
<div class="neu-card terminal-container">
<div class="terminal-status flex items-center gap-sm">
<span :class="['ws-dot', connected ? 'ws-dot--connected' : 'ws-dot--disconnected']" />
<span class="text-muted" style="font-size:11px">
{{ connected ? t('terminal.connected', { host: currentHost }) : t('terminal.disconnected') }}
</span>
</div>
<!-- Conteneur xterm.js -->
<div ref="terminalContainer" class="terminal-xterm" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { Terminal } from '@xterm/xterm'
import { FitAddon } from '@xterm/addon-fit'
import { useAuthStore } from '@/stores/auth.store'
import '@xterm/xterm/css/xterm.css'
const { t } = useI18n()
const authStore = useAuthStore()
const terminalContainer = ref<HTMLElement | null>(null)
const customHost = ref('')
const connected = ref(false)
const currentHost = ref('')
let terminal: Terminal | null = null
let fitAddon: FitAddon | null = null
let ws: WebSocket | null = null
onMounted(() => {
// Initialiser xterm.js
terminal = new Terminal({
theme: {
background: 'var(--neu-bg, #1a1d2e)',
foreground: '#e2e6f6',
cursor: '#6c8ef4',
},
fontFamily: '"Courier New", Courier, monospace',
fontSize: 13,
cursorBlink: true,
scrollback: 1000,
})
fitAddon = new FitAddon()
terminal.loadAddon(fitAddon)
terminal.open(terminalContainer.value!)
fitAddon.fit()
// Observer le redimensionnement
const ro = new ResizeObserver(() => fitAddon?.fit())
if (terminalContainer.value) ro.observe(terminalContainer.value)
// Connexion automatique
connect()
})
onUnmounted(() => {
ws?.close()
terminal?.dispose()
})
function connect() {
ws?.close()
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const hostParam = customHost.value ? `&host=${encodeURIComponent(customHost.value)}` : ''
const url = `${proto}//${window.location.host}/ws/terminal?token=${authStore.accessToken}${hostParam}`
ws = new WebSocket(url)
ws.binaryType = 'arraybuffer'
ws.onopen = () => {
connected.value = true
currentHost.value = customHost.value || 'ssh_host configuré'
terminal?.write('\r\n\x1b[32mConnecté au terminal SSH\x1b[0m\r\n\r\n')
}
ws.onclose = () => {
connected.value = false
terminal?.write('\r\n\x1b[31mDéconnecté\x1b[0m\r\n')
}
ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
terminal?.write(new Uint8Array(event.data))
} else {
terminal?.write(event.data)
}
}
ws.onerror = () => {
terminal?.write('\r\n\x1b[31mErreur de connexion WebSocket\x1b[0m\r\n')
}
// Envoyer les frappes clavier au serveur SSH
terminal?.onData((data) => {
if (ws?.readyState === WebSocket.OPEN) {
ws.send(data)
}
})
// Envoyer le resize au serveur
terminal?.onResize(({ cols, rows }) => {
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'resize', cols, rows }))
}
})
}
function reconnect() {
terminal?.clear()
connect()
}
</script>
<style scoped>
.terminal-page { height: 100%; display: flex; flex-direction: column; max-width: 1200px; }
.page-header { margin-bottom: var(--neu-space-lg); flex-shrink: 0; }
.page-header h2 { font-size: var(--neu-font-xl); color: var(--neu-text); }
.text-muted { color: var(--neu-text-muted); }
.terminal-container {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
padding: var(--neu-space-sm);
}
.terminal-status {
padding: var(--neu-space-xs) var(--neu-space-sm);
border-bottom: 1px solid var(--neu-border);
margin-bottom: var(--neu-space-sm);
flex-shrink: 0;
}
.terminal-xterm {
flex: 1;
min-height: 400px;
}
.ws-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.ws-dot--connected { background: var(--neu-success); animation: neu-pulse 2s infinite; }
.ws-dot--disconnected { background: var(--neu-text-muted); }
</style>