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:
enzo 2026-03-21 16:19:24 +01:00
parent 7ba0ff143c
commit 2098c80ec1
48 changed files with 2446 additions and 5317 deletions

175
frontend/install.html Normal file
View file

@ -0,0 +1,175 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ProxmoxPanel — Installation</title>
<link rel="stylesheet" href="/css/neu.css" />
<link rel="stylesheet" href="/css/dark.css" />
<link rel="stylesheet" href="/css/light.css" />
<script src="/js/vendors/htmx.min.js"></script>
<script src="/js/vendors/swup.iife.js"></script>
<script src="/js/app.js"></script>
<script src="/js/vendors/alpine.min.js" defer></script>
</head>
<body x-data>
<div class="auth-layout" x-data="installPage()" x-cloak>
<div class="install-card neu-card">
<div class="auth-logo">
<span class="logo-icon"></span>
<h1 class="auth-title">ProxmoxPanel</h1>
<p class="auth-subtitle" x-text="t('install.subtitle')"></p>
</div>
<!-- Stepper -->
<div class="stepper">
<template x-for="i in totalSteps" :key="i">
<div class="step" :class="{ active: step === i, done: step > i }">
<div class="step-dot" x-text="step > i ? '✓' : i"></div>
<div class="step-label" x-text="t('install.step' + i + '.label')"></div>
</div>
</template>
</div>
<!-- Step 1: Général -->
<div x-show="step === 1" class="step-content">
<h2 x-text="t('install.step1.title')"></h2>
<p class="step-desc" x-text="t('install.step1.desc')"></p>
<div class="form-group">
<label class="form-label" x-text="t('install.instanceName')"></label>
<input class="neu-input" type="text" x-model="form.instance_name"
:placeholder="t('install.instanceNamePlaceholder')" />
</div>
<div class="form-group">
<label class="form-label" x-text="t('install.publicUrl')"></label>
<input class="neu-input" type="url" x-model="form.public_url" />
</div>
<div class="form-group">
<label class="form-label" x-text="t('install.defaultLang')"></label>
<select class="neu-input" x-model="form.default_lang">
<option value="fr">Français</option>
<option value="en">English</option>
</select>
</div>
</div>
<!-- Step 2: SSH -->
<div x-show="step === 2" class="step-content">
<h2 x-text="t('install.step2.title')"></h2>
<p class="step-desc" x-text="t('install.step2.desc')"></p>
<div class="form-group">
<label class="form-label" x-text="t('install.sshHost')"></label>
<input class="neu-input" type="text" x-model="form.ssh_host"
placeholder="10.0.0.1:22" />
</div>
<div class="form-group">
<label class="form-label" x-text="t('install.sshUsername')"></label>
<input class="neu-input" type="text" x-model="form.ssh_username"
placeholder="enzo" />
</div>
<div class="form-group">
<label class="form-label" x-text="t('install.sshPassword')"></label>
<input class="neu-input" type="password" x-model="form.ssh_password" />
</div>
<button class="neu-btn" type="button" @click="testSSH" :disabled="sshTesting">
<span x-show="!sshTesting" x-text="t('install.testSSH')"></span>
<span x-show="sshTesting">Test en cours…</span>
</button>
<div x-show="sshStatus === 'ok'" class="status-ok" x-text="t('install.sshSuccess')"></div>
<div x-show="sshStatus === 'error'" class="status-error" x-text="t('install.sshFailed')"></div>
</div>
<!-- Step 3: Proxmox API -->
<div x-show="step === 3" class="step-content">
<h2 x-text="t('install.step3.title')"></h2>
<p class="step-desc" x-text="t('install.step3.desc')"></p>
<div class="form-group">
<label class="form-label" x-text="t('install.proxmoxUrl')"></label>
<input class="neu-input" type="url" x-model="form.proxmox_url"
placeholder="https://proxmox.example.com:8006" />
</div>
<div class="form-group">
<label class="form-label" x-text="t('install.proxmoxTokenId')"></label>
<input class="neu-input" type="text" x-model="form.proxmox_token_id"
placeholder="enzo@pam!panel" />
</div>
<div class="form-group">
<label class="form-label" x-text="t('install.proxmoxTokenSecret')"></label>
<input class="neu-input" type="text" x-model="form.proxmox_token_secret"
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" />
</div>
<p class="form-hint" x-text="t('install.proxmoxTokenHint')"></p>
</div>
<!-- Step 4: Confirmation -->
<div x-show="step === 4" class="step-content">
<h2 x-text="t('install.step4.title')"></h2>
<div class="confirm-summary neu-inset">
<div class="confirm-row">
<span class="confirm-label">Instance</span>
<span x-text="form.instance_name"></span>
</div>
<div class="confirm-row">
<span class="confirm-label">URL</span>
<span x-text="form.public_url"></span>
</div>
<div class="confirm-row">
<span class="confirm-label">SSH</span>
<span x-text="form.ssh_username + '@' + form.ssh_host"></span>
</div>
<div class="confirm-row" x-show="form.proxmox_url">
<span class="confirm-label">Proxmox</span>
<span x-text="form.proxmox_url"></span>
</div>
</div>
<div x-show="error" class="status-error" x-text="error"></div>
</div>
<!-- Navigation -->
<div class="step-nav">
<button class="neu-btn" type="button" @click="prevStep"
x-show="step > 1" :disabled="loading">
<span x-text="t('install.back')"></span>
</button>
<button class="neu-btn neu-btn--primary" type="button"
@click="step < totalSteps ? nextStep() : finish()"
:disabled="loading">
<span x-show="!loading && step < totalSteps" x-text="t('install.next')"></span>
<span x-show="!loading && step === totalSteps" x-text="t('install.finish')"></span>
<span x-show="loading" class="spinner"></span>
</button>
</div>
</div>
</div>
<style>
.auth-layout { min-height:100vh;display:flex;align-items:center;justify-content:center;padding:1rem; }
.install-card { width:100%;max-width:560px;padding:2rem; }
.auth-logo { text-align:center;margin-bottom:1.5rem; }
.logo-icon { font-size:2.5rem;color:var(--accent-primary); }
.auth-title { font-size:1.5rem;font-weight:700;margin:.25rem 0; }
.auth-subtitle { font-size:.875rem;color:var(--text-secondary);margin:0; }
.stepper { display:flex;gap:.5rem;justify-content:center;margin:1.5rem 0; }
.step { display:flex;flex-direction:column;align-items:center;gap:.25rem;flex:1;max-width:100px; }
.step-dot { width:2rem;height:2rem;border-radius:50%;border:2px solid var(--border-color);display:flex;align-items:center;justify-content:center;font-size:.8rem;font-weight:700;transition:all .2s; }
.step.active .step-dot { border-color:var(--accent-primary);background:var(--accent-primary);color:#fff; }
.step.done .step-dot { border-color:var(--color-success,#22c55e);background:var(--color-success,#22c55e);color:#fff; }
.step-label { font-size:.7rem;color:var(--text-secondary);text-align:center; }
.step-content { display:flex;flex-direction:column;gap:1rem;min-height:200px; }
.step-content h2 { margin:0;font-size:1.125rem; }
.step-desc { margin:0;font-size:.875rem;color:var(--text-secondary); }
.form-group { display:flex;flex-direction:column;gap:.4rem; }
.form-label { font-size:.8rem;font-weight:600;color:var(--text-secondary); }
.form-hint { font-size:.75rem;color:var(--text-muted);margin:0; }
.step-nav { display:flex;justify-content:flex-end;gap:.75rem;margin-top:1.5rem; }
.status-ok { padding:.5rem .75rem;border-radius:.375rem;background:rgba(34,197,94,.1);border:1px solid var(--color-success,#22c55e);color:var(--color-success,#22c55e);font-size:.875rem; }
.status-error { padding:.5rem .75rem;border-radius:.375rem;background:rgba(239,68,68,.1);border:1px solid var(--color-error,#ef4444);color:var(--color-error,#ef4444);font-size:.875rem; }
.confirm-summary { padding:1rem;display:flex;flex-direction:column;gap:.5rem;border-radius:.5rem; }
.confirm-row { display:flex;gap:1rem;font-size:.875rem; }
.confirm-label { font-weight:600;min-width:80px;color:var(--text-secondary); }
.spinner { display:inline-block;width:1rem;height:1rem;border:2px solid transparent;border-top-color:currentColor;border-radius:50%;animation:spin .6s linear infinite; }
@keyframes spin { to { transform:rotate(360deg); } }
[x-cloak] { display:none!important; }
</style>
</body>
</html>