diff --git a/SUIVI.md b/SUIVI.md new file mode 100644 index 0000000..7d20259 --- /dev/null +++ b/SUIVI.md @@ -0,0 +1,148 @@ +# Suivi d'implémentation — ProxmoxPanel CORE + +Référence : `instruction.md` | Mis à jour : 2026-03-21 + +--- + +## Stack technique actuelle + +| Composant | Choix | Statut | +|-----------|-------|--------| +| Backend | Go 1.23 + chi v5 + gorilla/websocket | ✅ Implémenté | +| Base de données | SQLite via modernc.org/sqlite (sans CGO) | ✅ Implémenté | +| Auth | PAM via SSH + JWT RS256 | ✅ Implémenté | +| Chiffrement | AES-256-GCM (master.key) | ✅ Implémenté | +| Frontend | Alpine.js v3 + HTMX v2 + Swup v4 | ✅ Branche `frontend/alpine` | +| Icônes | **LineIcons Duotone** | ⚠️ Non intégré (symboles Unicode utilisés) | +| Terminal | xterm.js v5 + addon-fit + WS PTY | ✅ Implémenté | +| Build frontend | esbuild (bundler Swup ESM → IIFE) | ✅ Implémenté | +| Serveur statique | Nginx 1.27 | ✅ Dockerfile simplifié | + +--- + +## Fonctionnalités CORE — État d'avancement + +### 1. Page d'installation ✅ +- Wizard 4 étapes (Alpine.js) +- Pré-remplissage URL automatique depuis `window.location.origin` +- Test SSH (fetch POST /api/install/test-ssh) +- Configuration Proxmox API (token unique format `user@realm!tokenid=secret`) +- **Manque** : pré-remplissage du port depuis la requête entrante + +### 2. Gestion des comptes utilisateurs ✅ +- Création automatique au premier login (PAM SSH → upsert SQLite) +- JWT access (15min) + refresh cookie httpOnly (7j) +- Deux niveaux : Admin (groupe sudo/wheel) et Utilisateur +- **Manque** : page profil utilisateur, préférences per-user en DB + +### 3. Détection LXC/VM ✅ +- API Proxmox REST (client Go, InsecureSkipVerify) +- WS `/ws/proxmox` — polling 10s, type `resources_update`, payload `[...]` +- Affichage en temps réel dans les pages Dashboard et Proxmox +- **Fix appliqué** : données immédiates via HTTP + WS pour live updates + +### 4. Dashboard ⚠️ Partiel +- Compteurs LXC running/stopped ✅ +- Liste LXC avec CPU/RAM ✅ +- WebSocket live ✅ +- **Manque** : widgets configurables (add/remove/drag-drop) ← requis par instruction.md +- **Manque** : raccourcis vers services (Grafana, Proxmox, Traefik…) +- **Manque** : personnalisation per-user sauvegardée en DB + +### 5. Thème Neumorphism ✅ +- neu.css (dark/light mode via CSS custom properties) ✅ +- Toggle dark/light dans la navbar ✅ +- Sidebar repliable ✅ +- Responsive (media queries) ✅ +- **Manque** : sidebar gauche/droite per-user ← requis par instruction.md +- **Manque** : LineIcons Duotone (actuellement Unicode symbols) ← requis par instruction.md + +### 6. Gestion des langues ✅ +- fr.json / en.json via Alpine store i18n ✅ +- Sélecteur dans la navbar ✅ +- Sauvegarde dans localStorage ✅ +- **Manque** : sauvegarde per-user en DB + +### 7. Mises à jour paquets ✅ +- Liste par cible (host + LXC) ✅ +- `apt update + apt full-upgrade -y` via SSH ✅ +- Streaming WS live (type `update_output` / `update_done` / `update_error`) ✅ +- **Fix appliqué** : `msg.payload` (pas `msg.data`) +- **Manque** : historique des mises à jour + +### 8. Système de paramètres ✅ +- Onglets Général / SSH / Proxmox API ✅ +- `PUT /api/settings/{key}` per-key ✅ +- Secrets chiffrés AES-256-GCM en DB ✅ +- **Manque** : onglet Apparence (sidebar position, thème par défaut) +- **Manque** : onglet Langues disponibles + +### 9. Mises à jour CORE/modules ❌ Non implémenté +- Vérification nouvelles versions Forgejo +- Affichage changelog +- Détection migrations post-update + +### 10. Store de modules ⚠️ Partiel +- Liste des modules (CORE + optionnels stubs) ✅ +- Toggle enable/disable ✅ +- **Manque** : installation/désinstallation de modules externes +- **Manque** : registre officiel + registres supplémentaires + +### 11. Page post-installation/migration ❌ Non implémenté +- Blocage total de l'accès si migration en attente + +--- + +## Bugs résolus dans cette session + +| Bug | Fix | +|-----|-----| +| CSS cassé au changement de page via Swup | Extraction de tous les styles inline en `css/pages.css` global | +| Dashboard/Proxmox WS bloqué à "⌛ Connexion…" | Type WS était `proxmox_resources` → corrigé en `resources_update` | +| WS lit `msg.data` au lieu de `msg.payload` | Corrigé pour dashboard, proxmox, updates | +| Délai 10s avant 1er affichage données | Ajout fetch HTTP immédiat + WS pour updates live | +| `access_token` null au login | Corrigé en session précédente | +| CSS variables incorrectes (`--bg-primary`) | Corrigé en session précédente | + +--- + +## Non-conformités instruction.md à corriger + +| Règle | État | Priorité | +|-------|------|----------| +| Icônes LineIcons Duotone uniquement | ❌ Symbols Unicode utilisés | Haute | +| Dashboard widgets add/remove/drag-drop | ❌ Non implémenté | Haute | +| Sidebar gauche/droite per-user | ❌ Gauche fixe uniquement | Moyenne | +| Préférences utilisateur en DB | ❌ localStorage uniquement | Moyenne | +| Historique mises à jour | ❌ Non affiché | Basse | +| Mises à jour CORE/modules depuis interface | ❌ Non implémenté | Basse | +| Page blocage migration | ❌ Non implémenté | Basse | + +--- + +## Architecture Docker + +``` +core/ +├── backend/ (Go — port 3001 interne) +├── frontend/ (Alpine.js — build → Nginx) +│ ├── js/app.js (stores + composants Alpine) +│ ├── css/neu.css (neumorphism + layout) +│ ├── css/pages.css (styles spécifiques pages) +│ └── *.html (pages statiques) +└── docker-compose.yml +``` + +## Commandes utiles + +```bash +# Build frontend (sur LXC ou machine dev) +cd core/frontend && npm run build + +# Déploiement sur LXC 112 +docker compose pull && docker compose up -d --build + +# Logs +docker compose logs -f backend +docker compose logs -f frontend +``` diff --git a/frontend/css/pages.css b/frontend/css/pages.css new file mode 100644 index 0000000..3dd87e9 --- /dev/null +++ b/frontend/css/pages.css @@ -0,0 +1,447 @@ +/* ============================================================================= + ProxmoxPanel — Styles spécifiques aux pages + Chargé sur toutes les pages. Persiste à travers les navigations Swup. + ============================================================================= */ + +/* ── Spinners ────────────────────────────────────────────────────────────────── */ +@keyframes spin { to { transform: rotate(360deg); } } + +.spinner { + display: inline-block; + width: 1rem; + height: 1rem; + border: 2px solid transparent; + border-top-color: currentColor; + border-radius: 50%; + animation: spin 0.6s linear infinite; +} + +.spinner-sm { + display: inline-block; + width: .875rem; + height: .875rem; + border: 2px solid transparent; + border-top-color: currentColor; + border-radius: 50%; + animation: spin .6s linear infinite; +} + +.spinner-lg { + width: 2rem; + height: 2rem; + border: 3px solid transparent; + border-top-color: var(--neu-primary); + border-radius: 50%; + animation: spin .6s linear infinite; +} + +/* ── États communs ───────────────────────────────────────────────────────────── */ +.loading-state { + display: flex; + align-items: center; + gap: .75rem; + padding: 2rem; + color: var(--neu-text-muted); +} + +.empty-state { + color: var(--neu-text-muted); + font-size: .875rem; +} + +.loading { + color: var(--neu-text-muted); + font-size: .875rem; +} + +/* ── Badge statut ────────────────────────────────────────────────────────────── */ +.badge { + font-size: .7rem; + padding: .2rem .5rem; + border-radius: .25rem; + text-transform: uppercase; + font-weight: 600; +} +.badge.running, .resource-badge.running { + background: rgba(34,197,94,.15); + color: var(--neu-success); +} +.badge.stopped, .resource-badge.stopped { + background: rgba(239,68,68,.1); + color: var(--neu-danger); +} +.resource-badge { + font-size: .7rem; + padding: .2rem .5rem; + border-radius: .25rem; + text-transform: uppercase; +} + +/* ── Formulaires ─────────────────────────────────────────────────────────────── */ +.form-group { + display: flex; + flex-direction: column; + gap: .4rem; +} +.form-label { + font-size: .8rem; + font-weight: 600; + color: var(--neu-text-muted); +} +.form-error { + background: rgba(239,68,68,.1); + border: 1px solid var(--neu-danger); + border-radius: .5rem; + padding: .75rem; + font-size: .875rem; + color: var(--neu-danger); +} +.form-hint { + font-size: .75rem; + color: var(--neu-text-muted); + margin: 0; +} + +/* ── Sections ────────────────────────────────────────────────────────────────── */ +.section { + margin-bottom: 2rem; +} +.section-title { + font-size: .875rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .05em; + color: var(--neu-text-muted); + margin-bottom: .75rem; +} + +/* ── WebSocket status ────────────────────────────────────────────────────────── */ +.ws-status { + padding: .5rem 1rem; + border-radius: .5rem; + font-size: .8rem; + margin-bottom: 1rem; + background: var(--neu-surface); +} +.ws-status.ok { color: var(--neu-success); } +.ws-status.disconnected, +.ws-status.error { color: var(--neu-warning); } + +/* ── Métriques ───────────────────────────────────────────────────────────────── */ +.resource-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1rem; +} +.resource-card { padding: 1rem; } +.resource-header { + display: flex; + align-items: center; + gap: .5rem; + margin-bottom: .75rem; +} +.resource-name { + font-weight: 600; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.resource-id { + font-size: .75rem; + color: var(--neu-text-muted); +} +.resource-metrics { margin-bottom: .75rem; } +.resource-actions { display: flex; gap: .5rem; flex-wrap: wrap; } +.metric { display: flex; align-items: center; gap: .5rem; margin-bottom: .4rem; } +.metric-label { font-size: .7rem; color: var(--neu-text-muted); min-width: 30px; } +.metric-bar { + flex: 1; + height: 6px; + border-radius: 3px; + background: var(--neu-surface); + overflow: hidden; +} +.metric-fill { + height: 100%; + border-radius: 3px; + background: var(--neu-primary); + transition: width .5s; +} +.metric-val { font-size: .7rem; min-width: 36px; text-align: right; } + +/* ── Dashboard — Stats ───────────────────────────────────────────────────────── */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 1rem; + margin-bottom: 1.5rem; +} +.stat-card { padding: 1.25rem; text-align: center; } +.stat-icon { font-size: 1.5rem; margin-bottom: .5rem; color: var(--neu-primary); } +.stat-value { font-size: 2rem; font-weight: 700; } +.stat-label { font-size: .8rem; color: var(--neu-text-muted); } + +/* ── Updates page ────────────────────────────────────────────────────────────── */ +.page-actions { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1.5rem; + gap: 1rem; + flex-wrap: wrap; +} +.page-actions-left, +.page-actions-right { display: flex; align-items: center; gap: .75rem; } +.total-badge { font-size: .875rem; font-weight: 600; color: var(--neu-primary); } +.targets-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; +} +.target-card { padding: 1rem; display: flex; flex-direction: column; gap: .75rem; } +.target-header { display: flex; justify-content: space-between; align-items: flex-start; } +.target-info { display: flex; flex-direction: column; gap: .2rem; } +.target-name { font-weight: 700; font-size: .95rem; } +.target-id { font-size: .7rem; color: var(--neu-text-muted); font-family: monospace; } +.target-status {} +.target-actions { display: flex; gap: .5rem; margin-top: auto; } +.package-summary { font-size: .875rem; } +.muted { color: var(--neu-text-muted); } +.up-to-date { color: var(--neu-success); } +.has-updates { color: var(--neu-warning); font-weight: 600; } +.checking-text { display: flex; align-items: center; gap: .4rem; color: var(--neu-text-muted); } +.package-list { + max-height: 160px; + overflow-y: auto; + border-top: 1px solid var(--neu-border); + padding-top: .5rem; + display: flex; + flex-direction: column; + gap: .25rem; +} +.package-row { display: flex; justify-content: space-between; gap: .5rem; font-size: .75rem; } +.pkg-name { font-weight: 600; font-family: monospace; color: var(--neu-primary); } +.pkg-version { display: flex; align-items: center; gap: .25rem; color: var(--neu-text-muted); } +.old-ver { text-decoration: line-through; opacity: .6; } +.arrow { color: var(--neu-primary); } +.new-ver { color: var(--neu-success); font-weight: 600; } +.output-panel { margin-top: 1.5rem; border-radius: .75rem; overflow: hidden; } +.output-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: .75rem 1rem; + border-bottom: 1px solid var(--neu-border); +} +.output-title { font-weight: 600; font-size: .875rem; font-family: monospace; } +.job-status { font-size: .75rem; padding: .2rem .5rem; border-radius: .25rem; text-transform: uppercase; } +.job-status.running { background: rgba(99,102,241,.15); color: var(--neu-primary); } +.job-status.success { background: rgba(34,197,94,.15); color: var(--neu-success); } +.job-status.error { background: rgba(239,68,68,.1); color: var(--neu-danger); } +.output-content { + padding: 1rem; + font-family: monospace; + font-size: .75rem; + white-space: pre-wrap; + word-break: break-all; + max-height: 400px; + overflow-y: auto; + margin: 0; + color: var(--neu-text); +} + +/* ── Settings page ───────────────────────────────────────────────────────────── */ +.tabs { + display: flex; + gap: .5rem; + margin-bottom: 1.5rem; + border-bottom: 1px solid var(--neu-border); + padding-bottom: .5rem; +} +.tab-btn { + padding: .5rem 1rem; + border-radius: .375rem .375rem 0 0; + font-size: .875rem; + font-weight: 600; + color: var(--neu-text-muted); + background: transparent; + border: none; + cursor: pointer; + transition: all .15s; +} +.tab-btn.active { + color: var(--neu-primary); + background: var(--neu-surface); + border-bottom: 2px solid var(--neu-primary); +} +.tab-btn:hover:not(.active) { color: var(--neu-text); } +.tab-panel { animation: fadeIn .15s ease; } +@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } } +.form-grid { + display: grid; + gap: 1.25rem; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + margin-bottom: 1.5rem; +} +.save-bar { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 1rem; + padding-top: 1rem; + border-top: 1px solid var(--neu-border); +} +.save-feedback { flex: 1; } +.save-success { color: var(--neu-success); font-size: .875rem; } +.save-error { color: var(--neu-danger); font-size: .875rem; } + +/* ── Modules page ────────────────────────────────────────────────────────────── */ +.modules-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; +} +.module-card { padding: 1rem; transition: opacity .2s; } +.module-card.disabled { opacity: .6; } +.module-header { display: flex; align-items: center; gap: .75rem; } +.module-icon { font-size: 1.75rem; flex-shrink: 0; } +.module-info { flex: 1; } +.module-name { font-weight: 700; display: block; margin-bottom: .2rem; } +.module-desc { font-size: .8rem; color: var(--neu-text-muted); display: block; } +.core-badge { + font-size: .65rem; + padding: .15rem .4rem; + border-radius: .2rem; + background: rgba(99,102,241,.15); + color: var(--neu-primary); + font-weight: 700; + text-transform: uppercase; +} +.toggle-btn { + display: flex; + align-items: center; + gap: .4rem; + background: none; + border: none; + cursor: pointer; + color: var(--neu-text-muted); + font-size: .8rem; +} +.toggle-btn:disabled { cursor: not-allowed; opacity: .5; } +.toggle-track { + width: 2.5rem; + height: 1.25rem; + background: var(--neu-surface); + border-radius: .625rem; + position: relative; + transition: background .2s; + border: 1px solid var(--neu-border); +} +.toggle-thumb { + position: absolute; + top: .125rem; + left: .125rem; + width: .875rem; + height: .875rem; + background: var(--neu-text-muted); + border-radius: 50%; + transition: transform .2s, background .2s; +} +.toggle-btn.on .toggle-track { background: var(--neu-primary); border-color: var(--neu-primary); } +.toggle-btn.on .toggle-thumb { transform: translateX(1.25rem); background: #fff; } +.module-toggle { margin-left: auto; } + +/* ── Terminal page ───────────────────────────────────────────────────────────── */ +.main-layout.terminal-wrapper { + height: 100vh; + overflow: hidden; +} +.terminal-layout { + flex: 1; + display: flex; + flex-direction: column; + padding: 0 !important; + overflow: hidden; +} +.terminal-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: .5rem 1rem; + border-bottom: 1px solid var(--neu-border); + background: var(--neu-surface); + flex-shrink: 0; +} +.terminal-status { font-size: .8rem; font-family: monospace; } +.terminal-status.connected { color: var(--neu-success); } +.terminal-status.disconnected, +.terminal-status.error { color: var(--neu-danger); } +.terminal-status.connecting { color: var(--neu-text-muted); } +.terminal-container { + flex: 1; + overflow: hidden; + background: #1a1a2e; + padding: .5rem; +} +.terminal-container .xterm { height: 100%; } + +/* ── Auth pages (login + install) ────────────────────────────────────────────── */ +.auth-card { + width: 100%; + max-width: 400px; + padding: 2rem; +} +.install-card { + width: 100%; + max-width: 560px; + padding: 2rem; +} +.auth-logo { text-align: center; margin-bottom: 2rem; } +.logo-icon { font-size: 3rem; color: var(--neu-primary); } +.auth-title { font-size: 1.5rem; font-weight: 700; margin: .5rem 0 .25rem; } +.auth-subtitle { font-size: .875rem; color: var(--neu-text-muted); margin: 0; } +.auth-form { display: flex; flex-direction: column; gap: 1.25rem; } +.auth-submit { width: 100%; padding: .875rem; margin-top: .5rem; } + +/* ── Install wizard ──────────────────────────────────────────────────────────── */ +.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(--neu-border); + display: flex; + align-items: center; + justify-content: center; + font-size: .8rem; + font-weight: 700; + transition: all .2s; +} +.step.active .step-dot { border-color: var(--neu-primary); background: var(--neu-primary); color: #fff; } +.step.done .step-dot { border-color: var(--neu-success); background: var(--neu-success); color: #fff; } +.step-label { font-size: .7rem; color: var(--neu-text-muted); 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(--neu-text-muted); } +.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(--neu-success); + color: var(--neu-success); + font-size: .875rem; +} +.status-error { + padding: .5rem .75rem; + border-radius: .375rem; + background: rgba(239,68,68,.1); + border: 1px solid var(--neu-danger); + color: var(--neu-danger); + 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(--neu-text-muted); } diff --git a/frontend/dashboard.html b/frontend/dashboard.html index 7a9843f..1488aca 100644 --- a/frontend/dashboard.html +++ b/frontend/dashboard.html @@ -8,6 +8,7 @@ + @@ -125,35 +126,5 @@ - diff --git a/frontend/install.html b/frontend/install.html index c675e75..d381994 100644 --- a/frontend/install.html +++ b/frontend/install.html @@ -8,6 +8,7 @@ + @@ -143,34 +144,5 @@ - diff --git a/frontend/js/app.js b/frontend/js/app.js index b65f715..c2bb2d1 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -306,7 +306,9 @@ document.addEventListener('alpine:init', () => { ws: null, wsStatus: 'connecting', - init() { + async init() { + // Chargement immédiat via HTTP, puis WS pour le temps réel + await this.fetchResources() this.connectWS() }, @@ -314,14 +316,25 @@ document.addEventListener('alpine:init', () => { if (this.ws) this.ws.close() }, + async fetchResources() { + try { + const res = await apiFetch('/api/proxmox/resources') + if (res.ok) { + this.resources = await res.json() || [] + this.wsStatus = 'ok' + } + } catch (e) { /* WS prendra le relais */ } + }, + connectWS() { const proto = location.protocol === 'https:' ? 'wss' : 'ws' const token = encodeURIComponent(localStorage.getItem('pxp_token') || '') this.ws = new WebSocket(`${proto}://${location.host}/ws/proxmox?token=${token}`) this.ws.onmessage = (e) => { const msg = JSON.parse(e.data) - if (msg.type === 'proxmox_resources') { - this.resources = msg.data || [] + // Le backend publie type="resources_update", payload=[...] + if (msg.type === 'resources_update') { + this.resources = msg.payload || [] this.wsStatus = 'ok' } } @@ -347,17 +360,31 @@ document.addEventListener('alpine:init', () => { wsStatus: 'connecting', actionLoading: {}, - init() { this.connectWS() }, + async init() { + await this.fetchResources() + this.connectWS() + }, destroy() { if (this.ws) this.ws.close() }, + async fetchResources() { + try { + const res = await apiFetch('/api/proxmox/resources') + if (res.ok) { + this.resources = await res.json() || [] + this.wsStatus = 'ok' + } + } catch (e) { /* WS prendra le relais */ } + }, + connectWS() { const proto = location.protocol === 'https:' ? 'wss' : 'ws' const token = encodeURIComponent(localStorage.getItem('pxp_token') || '') this.ws = new WebSocket(`${proto}://${location.host}/ws/proxmox?token=${token}`) this.ws.onmessage = (e) => { const msg = JSON.parse(e.data) - if (msg.type === 'proxmox_resources') { - this.resources = msg.data || [] + // Le backend publie type="resources_update", payload=[...] + if (msg.type === 'resources_update') { + this.resources = msg.payload || [] this.wsStatus = 'ok' } } @@ -508,14 +535,15 @@ document.addEventListener('alpine:init', () => { this.ws = new WebSocket(`${proto}://${location.host}/ws/updates/${jobId}?token=${token}`) this.ws.onmessage = (e) => { const msg = JSON.parse(e.data) + // Le backend publie dans msg.payload (pas msg.data) if (msg.type === 'update_output') { - this.output += msg.data?.chunk || '' + this.output += msg.payload?.chunk || '' } else if (msg.type === 'update_done') { this.jobStatus = 'success' resolve() } else if (msg.type === 'update_error') { this.jobStatus = 'error' - this.output += '\n[ERREUR] ' + (msg.data?.error || '') + this.output += '\n[ERREUR] ' + (msg.payload?.error || '') resolve() } } diff --git a/frontend/login.html b/frontend/login.html index 5d8749a..85cbfd7 100644 --- a/frontend/login.html +++ b/frontend/login.html @@ -8,6 +8,7 @@ + @@ -57,76 +58,5 @@ - diff --git a/frontend/modules.html b/frontend/modules.html index 894972e..3f4c301 100644 --- a/frontend/modules.html +++ b/frontend/modules.html @@ -8,6 +8,7 @@ + @@ -83,28 +84,5 @@ - diff --git a/frontend/proxmox.html b/frontend/proxmox.html index c05cefc..9080b6b 100644 --- a/frontend/proxmox.html +++ b/frontend/proxmox.html @@ -8,6 +8,7 @@ + @@ -129,27 +130,5 @@ - diff --git a/frontend/settings.html b/frontend/settings.html index d1f7054..e94e4ee 100644 --- a/frontend/settings.html +++ b/frontend/settings.html @@ -8,6 +8,7 @@ + @@ -138,26 +139,5 @@ - diff --git a/frontend/terminal.html b/frontend/terminal.html index 898246b..8d7452c 100644 --- a/frontend/terminal.html +++ b/frontend/terminal.html @@ -8,6 +8,7 @@ + @@ -34,7 +35,7 @@ -
+
-