feat: LineIcons Duotone, page profil, widgets dashboard, sidebar gauche/droite
- Intégration LineIcons Duotone (css/ + toutes les pages) - Remplacement de tous les symboles Unicode par des icônes lnid-* - Page profile.html : préférences thème, position sidebar, langue - Dashboard : système de widgets add/remove + drag-and-drop natif - Sidebar gauche/droite configurable per-user (data-sidebar CSS + FOUC script) - Store ui : sidebarPosition, applySidebarPosition(), setSidebarPosition() - Composant profilePage() dans app.js - nav.profile ajouté dans fr.json et en.json - SUIVI.md mis à jour Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9739dbaee8
commit
5f6681dd17
19 changed files with 11717 additions and 148 deletions
9
SUIVI.md
9
SUIVI.md
|
|
@ -110,10 +110,11 @@ Référence : `instruction.md` | Mis à jour : 2026-03-21
|
|||
|
||||
| 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 |
|
||||
| Icônes LineIcons Duotone uniquement | ✅ Intégrés (css/ + toutes les pages) | Haute |
|
||||
| Dashboard widgets add/remove/drag-drop | ✅ Widget system + DnD natif HTML5 | Haute |
|
||||
| Sidebar gauche/droite per-user | ✅ `data-sidebar` CSS + profilePage | Moyenne |
|
||||
| Page profil préférences (thème/sidebar/langue) | ✅ profile.html créée | Moyenne |
|
||||
| Préférences utilisateur en DB | ❌ localStorage uniquement | Basse |
|
||||
| Historique mises à jour | ❌ Non affiché | Basse |
|
||||
| Mises à jour CORE/modules depuis interface | ❌ Non implémenté | Basse |
|
||||
| Page blocage migration | ❌ Non implémenté | Basse |
|
||||
|
|
|
|||
11023
frontend/css/lineicons-duotone.css
Normal file
11023
frontend/css/lineicons-duotone.css
Normal file
File diff suppressed because it is too large
Load diff
BIN
frontend/css/lineicons-duotone.ttf
Normal file
BIN
frontend/css/lineicons-duotone.ttf
Normal file
Binary file not shown.
BIN
frontend/css/lineicons-duotone.woff
Normal file
BIN
frontend/css/lineicons-duotone.woff
Normal file
Binary file not shown.
BIN
frontend/css/lineicons-duotone.woff2
Normal file
BIN
frontend/css/lineicons-duotone.woff2
Normal file
Binary file not shown.
|
|
@ -488,10 +488,13 @@ input, select, textarea { font-family: inherit; font-size: inherit; }
|
|||
}
|
||||
|
||||
.sidebar-icon {
|
||||
font-size: 1.1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
width: 1.25rem;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.sidebar-label {
|
||||
|
|
@ -594,3 +597,27 @@ html.is-animating .transition-fade {
|
|||
}
|
||||
|
||||
[x-cloak] { display: none !important; }
|
||||
|
||||
/* ── Sidebar droite ─────────────────────────────────────────────────────────── */
|
||||
[data-sidebar="right"] .sidebar {
|
||||
left: auto;
|
||||
right: 0;
|
||||
border-right: none;
|
||||
border-left: 1px solid var(--neu-border);
|
||||
}
|
||||
|
||||
[data-sidebar="right"] .main-layout {
|
||||
margin-left: 0;
|
||||
margin-right: var(--sidebar-width);
|
||||
transition: margin-right 0.2s ease;
|
||||
}
|
||||
|
||||
[data-sidebar="right"] .sidebar.collapsed ~ .main-layout {
|
||||
margin-right: var(--sidebar-width-collapsed);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
[data-sidebar="right"] .main-layout {
|
||||
margin-right: var(--sidebar-width-collapsed);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -445,3 +445,167 @@
|
|||
.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); }
|
||||
|
||||
/* ── Page header ─────────────────────────────────────────────────────────────── */
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1.25rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
.page-title { font-size: 1.25rem; font-weight: 700; }
|
||||
|
||||
/* ── Dashboard widgets ───────────────────────────────────────────────────────── */
|
||||
.widgets-grid {
|
||||
display: grid;
|
||||
gap: 1.25rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
}
|
||||
.widget {
|
||||
min-height: 160px;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
transition: var(--neu-transition);
|
||||
}
|
||||
.widget:active { cursor: grabbing; }
|
||||
.widget:hover { transform: translateY(-2px); }
|
||||
.widget-title {
|
||||
font-size: .875rem;
|
||||
font-weight: 700;
|
||||
color: var(--neu-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .5px;
|
||||
margin: 0 0 .75rem 0;
|
||||
}
|
||||
|
||||
/* Widget config panel */
|
||||
.widget-config {
|
||||
margin-bottom: 1.25rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
.widget-config-title {
|
||||
font-weight: 700;
|
||||
margin: 0 0 .75rem 0;
|
||||
font-size: .9rem;
|
||||
}
|
||||
.widget-config-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .75rem;
|
||||
padding: .5rem .25rem;
|
||||
border-radius: .375rem;
|
||||
cursor: grab;
|
||||
transition: background .15s;
|
||||
}
|
||||
.widget-config-row:hover { background: rgba(108, 142, 244, 0.06); }
|
||||
.widget-toggle-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
margin-left: auto;
|
||||
font-size: .8rem;
|
||||
color: var(--neu-text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
.drag-handle { color: var(--neu-text-muted); font-size: 1rem; }
|
||||
|
||||
/* Widget: status (stats) */
|
||||
.stat-rows { display: flex; flex-direction: column; gap: .5rem; }
|
||||
.stat-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .75rem;
|
||||
font-size: .9rem;
|
||||
}
|
||||
.stat-icon { font-size: 1.1rem; width: 1.25rem; flex-shrink: 0; }
|
||||
.stat-num { font-size: 1.5rem; font-weight: 700; min-width: 2rem; }
|
||||
.stat-label { color: var(--neu-text-muted); }
|
||||
|
||||
/* Widget: lxc-list */
|
||||
.lxc-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
padding: .375rem 0;
|
||||
border-bottom: 1px solid var(--neu-border);
|
||||
font-size: .85rem;
|
||||
}
|
||||
.lxc-row:last-child { border-bottom: none; }
|
||||
.lxc-name { flex: 1; font-weight: 500; }
|
||||
.lxc-cpu, .lxc-ram { color: var(--neu-text-muted); font-size: .8rem; min-width: 3.5rem; text-align: right; }
|
||||
|
||||
/* Widget: links */
|
||||
.links-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: .75rem;
|
||||
}
|
||||
.link-btn {
|
||||
flex: 1;
|
||||
min-width: 80px;
|
||||
justify-content: center;
|
||||
gap: .375rem;
|
||||
}
|
||||
|
||||
/* ── Profil / préférences ────────────────────────────────────────────────────── */
|
||||
.settings-section {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.settings-section h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem 0;
|
||||
padding-bottom: .75rem;
|
||||
border-bottom: 1px solid var(--neu-border);
|
||||
}
|
||||
|
||||
.profile-info { display: flex; flex-direction: column; gap: .5rem; }
|
||||
.profile-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
font-size: .9rem;
|
||||
padding: .375rem 0;
|
||||
}
|
||||
.profile-label {
|
||||
font-weight: 600;
|
||||
color: var(--neu-text-muted);
|
||||
min-width: 100px;
|
||||
}
|
||||
.profile-value { color: var(--neu-text); }
|
||||
|
||||
.badge-admin {
|
||||
background: rgba(108, 142, 244, 0.15);
|
||||
color: var(--neu-primary);
|
||||
padding: 2px 8px;
|
||||
border-radius: 9999px;
|
||||
font-size: .75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.badge-user {
|
||||
background: rgba(127, 136, 153, 0.15);
|
||||
color: var(--neu-text-muted);
|
||||
padding: 2px 8px;
|
||||
border-radius: 9999px;
|
||||
font-size: .75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Groupe de boutons (thème, sidebar position) */
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* ── Logo auth LineIcons ─────────────────────────────────────────────────────── */
|
||||
.logo-icon {
|
||||
font-size: 2.5rem;
|
||||
color: var(--neu-primary);
|
||||
display: block;
|
||||
line-height: 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark")})()</script>
|
||||
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark");document.documentElement.setAttribute("data-sidebar",localStorage.getItem("pxp_sidebar_pos")||"left")})()</script>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ProxmoxPanel — Dashboard</title>
|
||||
|
|
@ -9,119 +9,171 @@
|
|||
<link rel="stylesheet" href="/css/dark.css" />
|
||||
<link rel="stylesheet" href="/css/light.css" />
|
||||
<link rel="stylesheet" href="/css/pages.css" />
|
||||
<link rel="stylesheet" href="/css/lineicons-duotone.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>
|
||||
<body x-data x-init="$store.ui.init()">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
|
||||
<div class="sidebar-header">
|
||||
<span class="sidebar-logo">⬡</span>
|
||||
<i class="sidebar-logo lnid-layout-1"></i>
|
||||
<span class="sidebar-title" x-show="!collapsed">ProxmoxPanel</span>
|
||||
<button class="sidebar-toggle" @click="toggle()">☰</button>
|
||||
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()">
|
||||
<i class="lnid-menu-hamburger-1"></i>
|
||||
</button>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<template x-for="item in navItems" :key="item.id">
|
||||
<a class="sidebar-link"
|
||||
:class="{ active: isActive(item.id) }"
|
||||
:href="item.href"
|
||||
@click.prevent="navigate(item.href)">
|
||||
<span class="sidebar-icon" x-text="item.icon"></span>
|
||||
<a class="sidebar-link" :class="{ active: isActive(item.id) }" :href="item.href" @click.prevent="navigate(item.href)">
|
||||
<i class="sidebar-icon" :class="item.iconClass"></i>
|
||||
<span class="sidebar-label" x-show="!collapsed" x-text="t(item.labelKey)"></span>
|
||||
</a>
|
||||
</template>
|
||||
</nav>
|
||||
<div class="sidebar-footer" x-show="!collapsed">
|
||||
<span class="sidebar-user" x-text="$store.auth.user?.username || ''"></span>
|
||||
<button class="neu-btn neu-btn--sm" @click="$store.auth.logout()">⏻</button>
|
||||
<div class="sidebar-footer">
|
||||
<a class="sidebar-link" href="/profile.html" @click.prevent="navigate('/profile.html')">
|
||||
<i class="sidebar-icon lnid-user-circle-1"></i>
|
||||
<span class="sidebar-label" x-show="!collapsed" x-text="$store.auth.user?.username || t('nav.profile')"></span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="main-layout">
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar" x-data="navbar()" x-cloak>
|
||||
<h2 class="navbar-title" x-text="t('nav.dashboard')"></h2>
|
||||
<div class="navbar-actions">
|
||||
<select class="neu-input neu-input--sm" x-model="lang" @change="setLang($event.target.value)">
|
||||
<option value="fr">FR</option>
|
||||
<option value="en">EN</option>
|
||||
<option value="fr">FR</option><option value="en">EN</option>
|
||||
</select>
|
||||
<button class="neu-btn neu-btn--sm" @click="toggleTheme()"
|
||||
:title="theme === 'dark' ? t('navbar.lightMode') : t('navbar.darkMode')">
|
||||
<span x-text="theme === 'dark' ? '☀' : '🌙'"></span>
|
||||
:title="theme==='dark' ? t('navbar.lightMode') : t('navbar.darkMode')">
|
||||
<i :class="theme==='dark' ? 'lnid-sun-1' : 'lnid-moon-half-left-1'"></i>
|
||||
</button>
|
||||
<button class="neu-btn neu-btn--sm" @click="logout()" :title="t('navbar.logout')">
|
||||
<i class="lnid-power-button"></i>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Contenu swappé -->
|
||||
<main id="swup" class="page-content transition-fade" x-data="dashboardPage()" x-cloak>
|
||||
<!-- Stats -->
|
||||
<div class="stats-grid">
|
||||
<div class="neu-card stat-card">
|
||||
<div class="stat-icon">▶</div>
|
||||
<div class="stat-value" x-text="runningCount"></div>
|
||||
<div class="stat-label">En cours</div>
|
||||
</div>
|
||||
<div class="neu-card stat-card">
|
||||
<div class="stat-icon">⏹</div>
|
||||
<div class="stat-value" x-text="stoppedCount"></div>
|
||||
<div class="stat-label">Arrêtés</div>
|
||||
</div>
|
||||
<div class="neu-card stat-card">
|
||||
<div class="stat-icon">⬡</div>
|
||||
<div class="stat-value" x-text="lxcList.length"></div>
|
||||
<div class="stat-label">LXC</div>
|
||||
</div>
|
||||
<div class="neu-card stat-card">
|
||||
<div class="stat-icon">▫</div>
|
||||
<div class="stat-value" x-text="vmList.length"></div>
|
||||
<div class="stat-label">VM</div>
|
||||
</div>
|
||||
|
||||
<div class="page-header">
|
||||
<h2 class="page-title" x-text="t('dashboard.welcome').replace('{name}', $store.auth.user?.username || '')"></h2>
|
||||
<button class="neu-btn neu-btn--sm" @click="configOpen = !configOpen" :title="t('dashboard.addWidget')">
|
||||
<i class="lnid-layout-1"></i>
|
||||
<span x-show="!configOpen" x-text="t('dashboard.addWidget')"></span>
|
||||
<span x-show="configOpen">✕</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Status WS -->
|
||||
<div class="ws-status" :class="wsStatus">
|
||||
<!-- Config panel widgets -->
|
||||
<div class="widget-config neu-card" x-show="configOpen" x-transition>
|
||||
<h4 class="widget-config-title">Configurer les widgets</h4>
|
||||
<template x-for="(w, idx) in widgets" :key="w.id">
|
||||
<div class="widget-config-row"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart(idx)"
|
||||
@dragover.prevent
|
||||
@drop.prevent="onDrop(idx)">
|
||||
<i class="lnid-menu-hamburger-1 drag-handle"></i>
|
||||
<span x-text="w.label"></span>
|
||||
<label class="widget-toggle-label">
|
||||
<input type="checkbox" x-model="w.visible" @change="saveWidgets()" />
|
||||
<span x-text="w.visible ? 'Visible' : 'Masqué'"></span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- WS status -->
|
||||
<div class="ws-status" :class="wsStatus" x-show="wsStatus !== 'connected'">
|
||||
<span x-show="wsStatus === 'connecting'">⌛ Connexion…</span>
|
||||
<span x-show="wsStatus === 'ok'">● Live</span>
|
||||
<span x-show="wsStatus === 'disconnected'">⚠ Déconnecté (reconnexion…)</span>
|
||||
<span x-show="wsStatus === 'error'">✗ Erreur WebSocket</span>
|
||||
</div>
|
||||
|
||||
<!-- LXC List -->
|
||||
<div class="section">
|
||||
<h3 class="section-title">Containers LXC</h3>
|
||||
<div class="resource-grid" x-show="lxcList.length > 0">
|
||||
<template x-for="r in lxcList" :key="r.vmid">
|
||||
<div class="neu-card resource-card" :class="r.status">
|
||||
<div class="resource-header">
|
||||
<span class="resource-name" x-text="r.name || 'LXC ' + r.vmid"></span>
|
||||
<span class="resource-id" x-text="'#' + r.vmid"></span>
|
||||
<span class="resource-badge" :class="r.status" x-text="r.status"></span>
|
||||
<!-- Widgets -->
|
||||
<div class="widgets-grid">
|
||||
<template x-for="(w, idx) in widgets.filter(w => w.visible)" :key="w.id">
|
||||
<div class="neu-card widget"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart(widgets.indexOf(w))"
|
||||
@dragover.prevent
|
||||
@drop.prevent="onDrop(widgets.indexOf(w))">
|
||||
|
||||
<!-- Widget: status -->
|
||||
<template x-if="w.id === 'status'">
|
||||
<div class="widget-status">
|
||||
<h4 class="widget-title" x-text="t('dashboard.lxcStatus')"></h4>
|
||||
<div class="stat-rows">
|
||||
<div class="stat-row">
|
||||
<i class="lnid-play stat-icon" style="color:var(--neu-success)"></i>
|
||||
<span class="stat-num" x-text="running.length"></span>
|
||||
<span class="stat-label" x-text="t('dashboard.running')"></span>
|
||||
</div>
|
||||
<div class="resource-metrics" x-show="r.status === 'running'">
|
||||
<div class="metric">
|
||||
<span class="metric-label">CPU</span>
|
||||
<div class="metric-bar">
|
||||
<div class="metric-fill" :style="'width:' + Math.round((r.cpu||0)*100) + '%;background:' + cpuColor(Math.round((r.cpu||0)*100))"></div>
|
||||
<div class="stat-row">
|
||||
<i class="lnid-stop stat-icon" style="color:var(--neu-danger)"></i>
|
||||
<span class="stat-num" x-text="stopped.length"></span>
|
||||
<span class="stat-label">Arrêtés</span>
|
||||
</div>
|
||||
<span class="metric-val" x-text="Math.round((r.cpu||0)*100) + '%'"></span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">RAM</span>
|
||||
<div class="metric-bar">
|
||||
<div class="metric-fill" :style="'width:' + Math.round((r.mem||0)/(r.maxmem||1)*100) + '%'"></div>
|
||||
</div>
|
||||
<span class="metric-val" x-text="formatMem(r.mem)"></span>
|
||||
<div class="stat-row">
|
||||
<i class="lnid-server-1 stat-icon" style="color:var(--neu-info)"></i>
|
||||
<span class="stat-num" x-text="lxc.length"></span>
|
||||
<span class="stat-label" x-text="t('dashboard.lxcCount')"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Widget: lxc-list -->
|
||||
<template x-if="w.id === 'lxc-list'">
|
||||
<div class="widget-lxc">
|
||||
<h4 class="widget-title" x-text="t('dashboard.lxcStatus')"></h4>
|
||||
<div class="loading-state" x-show="wsStatus === 'connecting'">
|
||||
<div class="spinner-lg"></div>
|
||||
</div>
|
||||
<p class="empty-state" x-show="lxcList.length === 0 && wsStatus === 'ok'">Aucun container</p>
|
||||
<div class="loading" x-show="wsStatus === 'connecting'">Chargement…</div>
|
||||
<div x-show="wsStatus !== 'connecting'">
|
||||
<template x-for="item in lxc" :key="item.vmid">
|
||||
<div class="lxc-row">
|
||||
<i :class="item.status === 'running' ? 'lnid-play' : 'lnid-stop'"
|
||||
:style="'color:' + (item.status === 'running' ? 'var(--neu-success)' : 'var(--neu-danger)')"></i>
|
||||
<span class="lxc-name" x-text="item.name || 'CT' + item.vmid"></span>
|
||||
<span class="lxc-cpu" x-text="Math.round((item.cpu || 0) * 100) + '%'"></span>
|
||||
<span class="lxc-ram" x-text="Math.round((item.mem || 0) / 1048576) + 'M'"></span>
|
||||
</div>
|
||||
</template>
|
||||
<p class="empty-state" x-show="lxc.length === 0" x-text="t('dashboard.noData')"></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Widget: links -->
|
||||
<template x-if="w.id === 'links'">
|
||||
<div class="widget-links">
|
||||
<h4 class="widget-title" x-text="t('dashboard.widgetShortcut')"></h4>
|
||||
<div class="links-grid">
|
||||
<a class="neu-btn link-btn" href="/proxmox.html" @click.prevent="navigate('/proxmox.html')">
|
||||
<i class="lnid-server-1"></i> Proxmox
|
||||
</a>
|
||||
<a class="neu-btn link-btn" href="/terminal.html" @click.prevent="navigate('/terminal.html')">
|
||||
<i class="lnid-terminal"></i> Terminal
|
||||
</a>
|
||||
<a class="neu-btn link-btn" href="/updates.html" @click.prevent="navigate('/updates.html')">
|
||||
<i class="lnid-arrow-upward"></i> Updates
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<p class="empty-state" x-show="widgets.filter(w => w.visible).length === 0">
|
||||
Aucun widget actif. Cliquez sur "Configurer" pour en ajouter.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
<link rel="stylesheet" href="/css/dark.css" />
|
||||
<link rel="stylesheet" href="/css/light.css" />
|
||||
<link rel="stylesheet" href="/css/pages.css" />
|
||||
<link rel="stylesheet" href="/css/lineicons-duotone.css" />
|
||||
<script src="/js/vendors/htmx.min.js"></script>
|
||||
<script src="/js/vendors/swup.iife.js"></script>
|
||||
<script src="/js/app.js"></script>
|
||||
|
|
@ -18,7 +19,7 @@
|
|||
<div class="auth-layout" x-data="installPage()" x-cloak>
|
||||
<div class="install-card neu-card">
|
||||
<div class="auth-logo">
|
||||
<span class="logo-icon">⬡</span>
|
||||
<i class="logo-icon lnid-layout-1"></i>
|
||||
<h1 class="auth-title">ProxmoxPanel</h1>
|
||||
<p class="auth-subtitle" x-text="t('install.subtitle')"></p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -97,11 +97,12 @@ document.addEventListener('alpine:init', () => {
|
|||
Alpine.store('ui', {
|
||||
theme: localStorage.getItem('pxp_theme') || 'dark',
|
||||
sidebarCollapsed: localStorage.getItem('pxp_sidebar') === 'true',
|
||||
sidebarPosition: localStorage.getItem('pxp_sidebar_pos') || 'left',
|
||||
currentPage: '',
|
||||
|
||||
init() {
|
||||
this.applyTheme()
|
||||
// Detect current page from URL
|
||||
this.applySidebarPosition()
|
||||
const path = window.location.pathname
|
||||
this.currentPage = path.replace(/^\/|\.html$/g, '') || 'index'
|
||||
},
|
||||
|
|
@ -116,6 +117,16 @@ document.addEventListener('alpine:init', () => {
|
|||
this.applyTheme()
|
||||
},
|
||||
|
||||
applySidebarPosition() {
|
||||
document.documentElement.setAttribute('data-sidebar', this.sidebarPosition)
|
||||
},
|
||||
|
||||
setSidebarPosition(pos) {
|
||||
this.sidebarPosition = pos
|
||||
localStorage.setItem('pxp_sidebar_pos', pos)
|
||||
this.applySidebarPosition()
|
||||
},
|
||||
|
||||
toggleSidebar() {
|
||||
this.sidebarCollapsed = !this.sidebarCollapsed
|
||||
localStorage.setItem('pxp_sidebar', this.sidebarCollapsed)
|
||||
|
|
@ -168,12 +179,12 @@ document.addEventListener('alpine:init', () => {
|
|||
get currentPage() { return Alpine.store('ui').currentPage },
|
||||
|
||||
navItems: [
|
||||
{ id: 'dashboard', icon: '⊞', labelKey: 'nav.dashboard', href: '/dashboard.html' },
|
||||
{ id: 'proxmox', icon: '⬡', labelKey: 'nav.proxmox', href: '/proxmox.html' },
|
||||
{ id: 'updates', icon: '↑', labelKey: 'nav.updates', href: '/updates.html' },
|
||||
{ id: 'terminal', icon: '❯', labelKey: 'nav.terminal', href: '/terminal.html' },
|
||||
{ id: 'settings', icon: '⚙', labelKey: 'nav.settings', href: '/settings.html' },
|
||||
{ id: 'modules', icon: '⬡', labelKey: 'nav.modules', href: '/modules.html' },
|
||||
{ id: 'dashboard', iconClass: 'lnid-dashboard-square-1', labelKey: 'nav.dashboard', href: '/dashboard.html' },
|
||||
{ id: 'proxmox', iconClass: 'lnid-server-1', labelKey: 'nav.proxmox', href: '/proxmox.html' },
|
||||
{ id: 'updates', iconClass: 'lnid-arrow-upward', labelKey: 'nav.updates', href: '/updates.html' },
|
||||
{ id: 'terminal', iconClass: 'lnid-terminal', labelKey: 'nav.terminal', href: '/terminal.html' },
|
||||
{ id: 'settings', iconClass: 'lnid-gear-1', labelKey: 'nav.settings', href: '/settings.html' },
|
||||
{ id: 'modules', iconClass: 'lnid-puzzle', labelKey: 'nav.modules', href: '/modules.html' },
|
||||
],
|
||||
|
||||
isActive(id) {
|
||||
|
|
@ -305,9 +316,37 @@ document.addEventListener('alpine:init', () => {
|
|||
resources: [],
|
||||
ws: null,
|
||||
wsStatus: 'connecting',
|
||||
configOpen: false,
|
||||
dragSrcIdx: null,
|
||||
|
||||
widgets: (function() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem('pxp_widgets') || 'null') || [
|
||||
{ id: 'status', visible: true, label: 'Statut LXC' },
|
||||
{ id: 'lxc-list', visible: true, label: 'Liste LXC' },
|
||||
{ id: 'links', visible: false, label: 'Raccourcis' },
|
||||
]
|
||||
} catch(e) {
|
||||
return [
|
||||
{ id: 'status', visible: true, label: 'Statut LXC' },
|
||||
{ id: 'lxc-list', visible: true, label: 'Liste LXC' },
|
||||
{ id: 'links', visible: false, label: 'Raccourcis' },
|
||||
]
|
||||
}
|
||||
})(),
|
||||
|
||||
saveWidgets() { localStorage.setItem('pxp_widgets', JSON.stringify(this.widgets)) },
|
||||
|
||||
onDragStart(idx) { this.dragSrcIdx = idx },
|
||||
onDrop(idx) {
|
||||
if (this.dragSrcIdx === null || this.dragSrcIdx === idx) return
|
||||
const moved = this.widgets.splice(this.dragSrcIdx, 1)[0]
|
||||
this.widgets.splice(idx, 0, moved)
|
||||
this.dragSrcIdx = null
|
||||
this.saveWidgets()
|
||||
},
|
||||
|
||||
async init() {
|
||||
// Chargement immédiat via HTTP, puis WS pour le temps réel
|
||||
await this.fetchResources()
|
||||
this.connectWS()
|
||||
},
|
||||
|
|
@ -321,7 +360,7 @@ document.addEventListener('alpine:init', () => {
|
|||
const res = await apiFetch('/api/proxmox/resources')
|
||||
if (res.ok) {
|
||||
this.resources = await res.json() || []
|
||||
this.wsStatus = 'ok'
|
||||
this.wsStatus = 'connected'
|
||||
}
|
||||
} catch (e) { /* WS prendra le relais */ }
|
||||
},
|
||||
|
|
@ -332,10 +371,9 @@ document.addEventListener('alpine:init', () => {
|
|||
this.ws = new WebSocket(`${proto}://${location.host}/ws/proxmox?token=${token}`)
|
||||
this.ws.onmessage = (e) => {
|
||||
const msg = JSON.parse(e.data)
|
||||
// Le backend publie type="resources_update", payload=[...]
|
||||
if (msg.type === 'resources_update') {
|
||||
this.resources = msg.payload || []
|
||||
this.wsStatus = 'ok'
|
||||
this.wsStatus = 'connected'
|
||||
}
|
||||
}
|
||||
this.ws.onclose = () => {
|
||||
|
|
@ -345,14 +383,51 @@ document.addEventListener('alpine:init', () => {
|
|||
this.ws.onerror = () => { this.wsStatus = 'error' }
|
||||
},
|
||||
|
||||
get runningCount() { return this.resources.filter(r => r.status === 'running').length },
|
||||
get stoppedCount() { return this.resources.filter(r => r.status !== 'running').length },
|
||||
get lxcList() { return this.resources.filter(r => r.type === 'lxc') },
|
||||
get lxc() { return this.resources.filter(r => r.type === 'lxc') },
|
||||
get running() { return this.lxc.filter(r => r.status === 'running') },
|
||||
get stopped() { return this.lxc.filter(r => r.status !== 'running') },
|
||||
// compat
|
||||
get runningCount() { return this.running.length },
|
||||
get stoppedCount() { return this.stopped.length },
|
||||
get lxcList() { return this.lxc },
|
||||
get vmList() { return this.resources.filter(r => r.type === 'qemu') },
|
||||
|
||||
t(key) { return Alpine.store('i18n').t(key) },
|
||||
}))
|
||||
|
||||
// ── Composant: profilePage ──────────────────────────────────────────────
|
||||
Alpine.data('profilePage', () => ({
|
||||
theme: '',
|
||||
sidebarPosition: '',
|
||||
lang: '',
|
||||
|
||||
init() {
|
||||
this.theme = Alpine.store('ui').theme
|
||||
this.sidebarPosition = Alpine.store('ui').sidebarPosition
|
||||
this.lang = Alpine.store('i18n').lang
|
||||
},
|
||||
|
||||
setTheme(t) {
|
||||
this.theme = t
|
||||
Alpine.store('ui').theme = t
|
||||
Alpine.store('ui').applyTheme()
|
||||
localStorage.setItem('pxp_theme', t)
|
||||
},
|
||||
|
||||
setSidebarPosition(pos) {
|
||||
this.sidebarPosition = pos
|
||||
Alpine.store('ui').setSidebarPosition(pos)
|
||||
},
|
||||
|
||||
async setLang(lang) {
|
||||
this.lang = lang
|
||||
await Alpine.store('i18n').setLang(lang)
|
||||
},
|
||||
|
||||
get user() { return Alpine.store('auth').user },
|
||||
t(key) { return Alpine.store('i18n').t(key) },
|
||||
}))
|
||||
|
||||
// ── Composant: proxmoxPage ──────────────────────────────────────────────
|
||||
Alpine.data('proxmoxPage', () => ({
|
||||
resources: [],
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@
|
|||
"logs": "Logs",
|
||||
"services": "Services",
|
||||
"settings": "Settings",
|
||||
"modules": "Modules"
|
||||
"modules": "Modules",
|
||||
"profile": "Profile"
|
||||
},
|
||||
"navbar": {
|
||||
"darkMode": "Dark mode",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@
|
|||
"logs": "Journaux",
|
||||
"services": "Services",
|
||||
"settings": "Paramètres",
|
||||
"modules": "Modules"
|
||||
"modules": "Modules",
|
||||
"profile": "Profil"
|
||||
},
|
||||
"navbar": {
|
||||
"darkMode": "Mode sombre",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
<link rel="stylesheet" href="/css/dark.css" />
|
||||
<link rel="stylesheet" href="/css/light.css" />
|
||||
<link rel="stylesheet" href="/css/pages.css" />
|
||||
<link rel="stylesheet" href="/css/lineicons-duotone.css" />
|
||||
<script src="/js/vendors/htmx.min.js"></script>
|
||||
<script src="/js/vendors/swup.iife.js"></script>
|
||||
<script src="/js/app.js"></script>
|
||||
|
|
@ -18,7 +19,7 @@
|
|||
<div class="auth-layout" x-data="loginPage()" x-cloak>
|
||||
<div class="auth-card neu-card">
|
||||
<div class="auth-logo">
|
||||
<span class="logo-icon">⬡</span>
|
||||
<i class="logo-icon lnid-layout-1"></i>
|
||||
<h1 class="auth-title">ProxmoxPanel</h1>
|
||||
<p class="auth-subtitle" x-text="t('login.subtitle')"></p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark")})()</script>
|
||||
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark");document.documentElement.setAttribute("data-sidebar",localStorage.getItem("pxp_sidebar_pos")||"left")})()</script>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ProxmoxPanel — Modules</title>
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
<link rel="stylesheet" href="/css/dark.css" />
|
||||
<link rel="stylesheet" href="/css/light.css" />
|
||||
<link rel="stylesheet" href="/css/pages.css" />
|
||||
<link rel="stylesheet" href="/css/lineicons-duotone.css" />
|
||||
<script src="/js/vendors/htmx.min.js"></script>
|
||||
<script src="/js/vendors/swup.iife.js"></script>
|
||||
<script src="/js/app.js"></script>
|
||||
|
|
@ -18,18 +19,26 @@
|
|||
|
||||
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
|
||||
<div class="sidebar-header">
|
||||
<span class="sidebar-logo">⬡</span>
|
||||
<i class="sidebar-logo lnid-layout-1"></i>
|
||||
<span class="sidebar-title" x-show="!collapsed">ProxmoxPanel</span>
|
||||
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()">☰</button>
|
||||
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()">
|
||||
<i class="lnid-menu-hamburger-1"></i>
|
||||
</button>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<template x-for="item in navItems" :key="item.id">
|
||||
<a class="sidebar-link" :class="{ active: isActive(item.id) }" :href="item.href" @click.prevent="navigate(item.href)">
|
||||
<span class="sidebar-icon" x-text="item.icon"></span>
|
||||
<i class="sidebar-icon" :class="item.iconClass"></i>
|
||||
<span class="sidebar-label" x-show="!collapsed" x-text="t(item.labelKey)"></span>
|
||||
</a>
|
||||
</template>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<a class="sidebar-link" href="/profile.html" @click.prevent="navigate('/profile.html')">
|
||||
<i class="sidebar-icon lnid-user-circle-1"></i>
|
||||
<span class="sidebar-label" x-show="!collapsed" x-text="$store.auth.user?.username || t('nav.profile')"></span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="main-layout">
|
||||
|
|
@ -39,10 +48,12 @@
|
|||
<select class="neu-input neu-input--sm" x-model="lang" @change="setLang($event.target.value)">
|
||||
<option value="fr">FR</option><option value="en">EN</option>
|
||||
</select>
|
||||
<button class="neu-btn neu-btn--sm" @click="toggleTheme()">
|
||||
<span x-text="theme==='dark'?'☀':'🌙'"></span>
|
||||
<button class="neu-btn neu-btn--sm" @click="toggleTheme()" :title="theme==='dark'?t('navbar.lightMode'):t('navbar.darkMode')">
|
||||
<i :class="theme==='dark' ? 'lnid-sun-1' : 'lnid-moon-half-left-1'"></i>
|
||||
</button>
|
||||
<button class="neu-btn neu-btn--sm" @click="logout()" :title="t('navbar.logout')">
|
||||
<i class="lnid-power-button"></i>
|
||||
</button>
|
||||
<button class="neu-btn neu-btn--sm" @click="logout()">⏻</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
|
@ -58,7 +69,9 @@
|
|||
<template x-for="mod in modules" :key="mod.id">
|
||||
<div class="neu-card module-card" :class="{ disabled: !mod.is_enabled }">
|
||||
<div class="module-header">
|
||||
<div class="module-icon" x-text="mod.icon || '⬡'"></div>
|
||||
<div class="module-icon">
|
||||
<i :class="mod.icon || 'lnid-puzzle'"></i>
|
||||
</div>
|
||||
<div class="module-info">
|
||||
<span class="module-name" x-text="mod.name || mod.id"></span>
|
||||
<span class="module-desc" x-text="mod.description || ''"></span>
|
||||
|
|
|
|||
160
frontend/profile.html
Normal file
160
frontend/profile.html
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark");document.documentElement.setAttribute("data-sidebar",localStorage.getItem("pxp_sidebar_pos")||"left")})()</script>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ProxmoxPanel — Profil</title>
|
||||
<link rel="stylesheet" href="/css/neu.css" />
|
||||
<link rel="stylesheet" href="/css/dark.css" />
|
||||
<link rel="stylesheet" href="/css/light.css" />
|
||||
<link rel="stylesheet" href="/css/pages.css" />
|
||||
<link rel="stylesheet" href="/css/lineicons-duotone.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 x-init="$store.ui.init()">
|
||||
|
||||
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
|
||||
<div class="sidebar-header">
|
||||
<i class="sidebar-logo lnid-layout-1"></i>
|
||||
<span class="sidebar-title" x-show="!collapsed">ProxmoxPanel</span>
|
||||
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()">
|
||||
<i class="lnid-menu-hamburger-1"></i>
|
||||
</button>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<template x-for="item in navItems" :key="item.id">
|
||||
<a class="sidebar-link" :class="{ active: isActive(item.id) }" :href="item.href" @click.prevent="navigate(item.href)">
|
||||
<i class="sidebar-icon" :class="item.iconClass"></i>
|
||||
<span class="sidebar-label" x-show="!collapsed" x-text="t(item.labelKey)"></span>
|
||||
</a>
|
||||
</template>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<a class="sidebar-link active" href="/profile.html" @click.prevent="navigate('/profile.html')">
|
||||
<i class="sidebar-icon lnid-user-circle-1"></i>
|
||||
<span class="sidebar-label" x-show="!collapsed" x-text="$store.auth.user?.username || t('nav.profile')"></span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="main-layout">
|
||||
<nav class="navbar" x-data="navbar()" x-cloak>
|
||||
<h2 class="navbar-title" x-text="t('nav.profile')"></h2>
|
||||
<div class="navbar-actions">
|
||||
<select class="neu-input neu-input--sm" x-model="lang" @change="setLang($event.target.value)">
|
||||
<option value="fr">FR</option><option value="en">EN</option>
|
||||
</select>
|
||||
<button class="neu-btn neu-btn--sm" @click="toggleTheme()" :title="theme==='dark'?t('navbar.lightMode'):t('navbar.darkMode')">
|
||||
<i :class="theme==='dark' ? 'lnid-sun-1' : 'lnid-moon-half-left-1'"></i>
|
||||
</button>
|
||||
<button class="neu-btn neu-btn--sm" @click="logout()" :title="t('navbar.logout')">
|
||||
<i class="lnid-power-button"></i>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main id="swup" class="page-content transition-fade">
|
||||
<div x-data="profilePage()" x-cloak>
|
||||
|
||||
<!-- Informations du compte -->
|
||||
<div class="neu-card settings-section" x-show="user">
|
||||
<h3 class="section-title">
|
||||
<i class="lnid-user-circle-1"></i>
|
||||
Compte
|
||||
</h3>
|
||||
<div class="profile-info">
|
||||
<div class="profile-row">
|
||||
<span class="profile-label">Utilisateur</span>
|
||||
<span class="profile-value" x-text="user?.username"></span>
|
||||
</div>
|
||||
<div class="profile-row">
|
||||
<span class="profile-label">Rôle</span>
|
||||
<span class="profile-value">
|
||||
<span class="badge" :class="user?.is_admin ? 'badge-admin' : 'badge-user'"
|
||||
x-text="user?.is_admin ? 'Administrateur' : 'Utilisateur'"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Apparence -->
|
||||
<div class="neu-card settings-section">
|
||||
<h3 class="section-title">
|
||||
<i class="lnid-sun-1"></i>
|
||||
Apparence
|
||||
</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Thème</label>
|
||||
<div class="btn-group">
|
||||
<button class="neu-btn"
|
||||
:class="{ 'neu-btn--primary': theme === 'dark' }"
|
||||
@click="setTheme('dark')">
|
||||
<i class="lnid-moon-half-left-1"></i> Sombre
|
||||
</button>
|
||||
<button class="neu-btn"
|
||||
:class="{ 'neu-btn--primary': theme === 'light' }"
|
||||
@click="setTheme('light')">
|
||||
<i class="lnid-sun-1"></i> Clair
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" x-text="t('settings.sidebarPosition')"></label>
|
||||
<div class="btn-group">
|
||||
<button class="neu-btn"
|
||||
:class="{ 'neu-btn--primary': sidebarPosition === 'left' }"
|
||||
@click="setSidebarPosition('left')">
|
||||
<i class="lnid-layout-1"></i> <span x-text="t('settings.left')"></span>
|
||||
</button>
|
||||
<button class="neu-btn"
|
||||
:class="{ 'neu-btn--primary': sidebarPosition === 'right' }"
|
||||
@click="setSidebarPosition('right')">
|
||||
<i class="lnid-layout-2"></i> <span x-text="t('settings.right')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Langue -->
|
||||
<div class="neu-card settings-section">
|
||||
<h3 class="section-title">
|
||||
<i class="lnid-gear-1"></i>
|
||||
Langue
|
||||
</h3>
|
||||
<div class="form-group">
|
||||
<label class="form-label" x-text="t('settings.defaultLang')"></label>
|
||||
<div class="btn-group">
|
||||
<button class="neu-btn"
|
||||
:class="{ 'neu-btn--primary': lang === 'fr' }"
|
||||
@click="setLang('fr')">
|
||||
Français
|
||||
</button>
|
||||
<button class="neu-btn"
|
||||
:class="{ 'neu-btn--primary': lang === 'en' }"
|
||||
@click="setLang('en')">
|
||||
English
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Déconnexion -->
|
||||
<div class="neu-card settings-section">
|
||||
<button class="neu-btn neu-btn--danger" @click="$store.auth.logout()">
|
||||
<i class="lnid-power-button"></i>
|
||||
<span x-text="t('navbar.logout')"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark")})()</script>
|
||||
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark");document.documentElement.setAttribute("data-sidebar",localStorage.getItem("pxp_sidebar_pos")||"left")})()</script>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ProxmoxPanel — Proxmox</title>
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
<link rel="stylesheet" href="/css/dark.css" />
|
||||
<link rel="stylesheet" href="/css/light.css" />
|
||||
<link rel="stylesheet" href="/css/pages.css" />
|
||||
<link rel="stylesheet" href="/css/lineicons-duotone.css" />
|
||||
<script src="/js/vendors/htmx.min.js"></script>
|
||||
<script src="/js/vendors/swup.iife.js"></script>
|
||||
<script src="/js/app.js"></script>
|
||||
|
|
@ -18,18 +19,26 @@
|
|||
|
||||
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
|
||||
<div class="sidebar-header">
|
||||
<span class="sidebar-logo">⬡</span>
|
||||
<i class="sidebar-logo lnid-layout-1"></i>
|
||||
<span class="sidebar-title" x-show="!collapsed">ProxmoxPanel</span>
|
||||
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()">☰</button>
|
||||
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()">
|
||||
<i class="lnid-menu-hamburger-1"></i>
|
||||
</button>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<template x-for="item in navItems" :key="item.id">
|
||||
<a class="sidebar-link" :class="{ active: isActive(item.id) }" :href="item.href" @click.prevent="navigate(item.href)">
|
||||
<span class="sidebar-icon" x-text="item.icon"></span>
|
||||
<i class="sidebar-icon" :class="item.iconClass"></i>
|
||||
<span class="sidebar-label" x-show="!collapsed" x-text="t(item.labelKey)"></span>
|
||||
</a>
|
||||
</template>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<a class="sidebar-link" href="/profile.html" @click.prevent="navigate('/profile.html')">
|
||||
<i class="sidebar-icon lnid-user-circle-1"></i>
|
||||
<span class="sidebar-label" x-show="!collapsed" x-text="$store.auth.user?.username || t('nav.profile')"></span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="main-layout">
|
||||
|
|
@ -40,9 +49,11 @@
|
|||
<option value="fr">FR</option><option value="en">EN</option>
|
||||
</select>
|
||||
<button class="neu-btn neu-btn--sm" @click="toggleTheme()" :title="theme==='dark'?t('navbar.lightMode'):t('navbar.darkMode')">
|
||||
<span x-text="theme==='dark'?'☀':'🌙'"></span>
|
||||
<i :class="theme==='dark' ? 'lnid-sun-1' : 'lnid-moon-half-left-1'"></i>
|
||||
</button>
|
||||
<button class="neu-btn neu-btn--sm" @click="logout()" :title="t('navbar.logout')">
|
||||
<i class="lnid-power-button"></i>
|
||||
</button>
|
||||
<button class="neu-btn neu-btn--sm" @click="logout()" :title="t('navbar.logout')">⏻</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
|
@ -51,6 +62,7 @@
|
|||
|
||||
<div class="ws-status" :class="wsStatus">
|
||||
<span x-show="wsStatus==='connecting'">⌛ Connexion WebSocket…</span>
|
||||
<span x-show="wsStatus==='connected'" style="color:var(--neu-success)">● Live</span>
|
||||
<span x-show="wsStatus==='ok'" style="color:var(--neu-success)">● Live</span>
|
||||
<span x-show="wsStatus==='disconnected'" style="color:var(--neu-warning)">⚠ Reconnexion…</span>
|
||||
<span x-show="wsStatus==='error'" style="color:var(--neu-danger)">✗ Erreur WebSocket</span>
|
||||
|
|
@ -81,17 +93,17 @@
|
|||
<div class="resource-actions">
|
||||
<button class="neu-btn neu-btn--sm neu-btn--success" x-show="r.status!=='running'"
|
||||
@click="action(r.vmid,'lxc','start')" :disabled="actionLoading[r.vmid+'-start']">
|
||||
▶ Start
|
||||
<i class="lnid-play"></i> <span x-text="t('proxmox.start')"></span>
|
||||
</button>
|
||||
<button class="neu-btn neu-btn--sm neu-btn--danger" x-show="r.status==='running'"
|
||||
@click="action(r.vmid,'lxc','stop')" :disabled="actionLoading[r.vmid+'-stop']">
|
||||
⏹ Stop
|
||||
<i class="lnid-stop"></i> <span x-text="t('proxmox.stop')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<p class="empty-state" x-show="resources.filter(r=>r.type==='lxc').length===0&&wsStatus==='ok'">Aucun container LXC</p>
|
||||
<p class="empty-state" x-show="resources.filter(r=>r.type==='lxc').length===0&&wsStatus!=='connecting'">Aucun container LXC</p>
|
||||
</div>
|
||||
|
||||
<!-- VMs -->
|
||||
|
|
@ -114,17 +126,17 @@
|
|||
<div class="resource-actions">
|
||||
<button class="neu-btn neu-btn--sm neu-btn--success" x-show="r.status!=='running'"
|
||||
@click="action(r.vmid,'qemu','start')" :disabled="actionLoading[r.vmid+'-start']">
|
||||
▶ Start
|
||||
<i class="lnid-play"></i> <span x-text="t('proxmox.start')"></span>
|
||||
</button>
|
||||
<button class="neu-btn neu-btn--sm neu-btn--danger" x-show="r.status==='running'"
|
||||
@click="action(r.vmid,'qemu','stop')" :disabled="actionLoading[r.vmid+'-stop']">
|
||||
⏹ Stop
|
||||
<i class="lnid-stop"></i> <span x-text="t('proxmox.stop')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<p class="empty-state" x-show="resources.filter(r=>r.type==='qemu').length===0&&wsStatus==='ok'">Aucune VM</p>
|
||||
<p class="empty-state" x-show="resources.filter(r=>r.type==='qemu').length===0&&wsStatus!=='connecting'">Aucune VM</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark")})()</script>
|
||||
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark");document.documentElement.setAttribute("data-sidebar",localStorage.getItem("pxp_sidebar_pos")||"left")})()</script>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ProxmoxPanel — Paramètres</title>
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
<link rel="stylesheet" href="/css/dark.css" />
|
||||
<link rel="stylesheet" href="/css/light.css" />
|
||||
<link rel="stylesheet" href="/css/pages.css" />
|
||||
<link rel="stylesheet" href="/css/lineicons-duotone.css" />
|
||||
<script src="/js/vendors/htmx.min.js"></script>
|
||||
<script src="/js/vendors/swup.iife.js"></script>
|
||||
<script src="/js/app.js"></script>
|
||||
|
|
@ -18,18 +19,26 @@
|
|||
|
||||
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
|
||||
<div class="sidebar-header">
|
||||
<span class="sidebar-logo">⬡</span>
|
||||
<i class="sidebar-logo lnid-layout-1"></i>
|
||||
<span class="sidebar-title" x-show="!collapsed">ProxmoxPanel</span>
|
||||
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()">☰</button>
|
||||
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()">
|
||||
<i class="lnid-menu-hamburger-1"></i>
|
||||
</button>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<template x-for="item in navItems" :key="item.id">
|
||||
<a class="sidebar-link" :class="{ active: isActive(item.id) }" :href="item.href" @click.prevent="navigate(item.href)">
|
||||
<span class="sidebar-icon" x-text="item.icon"></span>
|
||||
<i class="sidebar-icon" :class="item.iconClass"></i>
|
||||
<span class="sidebar-label" x-show="!collapsed" x-text="t(item.labelKey)"></span>
|
||||
</a>
|
||||
</template>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<a class="sidebar-link" href="/profile.html" @click.prevent="navigate('/profile.html')">
|
||||
<i class="sidebar-icon lnid-user-circle-1"></i>
|
||||
<span class="sidebar-label" x-show="!collapsed" x-text="$store.auth.user?.username || t('nav.profile')"></span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="main-layout">
|
||||
|
|
@ -39,10 +48,12 @@
|
|||
<select class="neu-input neu-input--sm" x-model="lang" @change="setLang($event.target.value)">
|
||||
<option value="fr">FR</option><option value="en">EN</option>
|
||||
</select>
|
||||
<button class="neu-btn neu-btn--sm" @click="toggleTheme()">
|
||||
<span x-text="theme==='dark'?'☀':'🌙'"></span>
|
||||
<button class="neu-btn neu-btn--sm" @click="toggleTheme()" :title="theme==='dark'?t('navbar.lightMode'):t('navbar.darkMode')">
|
||||
<i :class="theme==='dark' ? 'lnid-sun-1' : 'lnid-moon-half-left-1'"></i>
|
||||
</button>
|
||||
<button class="neu-btn neu-btn--sm" @click="logout()" :title="t('navbar.logout')">
|
||||
<i class="lnid-power-button"></i>
|
||||
</button>
|
||||
<button class="neu-btn neu-btn--sm" @click="logout()">⏻</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
|
@ -125,11 +136,13 @@
|
|||
<!-- Save actions -->
|
||||
<div class="save-bar">
|
||||
<div class="save-feedback">
|
||||
<span class="save-success" x-show="saved">✓ Paramètres sauvegardés</span>
|
||||
<span class="save-success" x-show="saved">
|
||||
<i class="lnid-check-circle-1"></i> Paramètres sauvegardés
|
||||
</span>
|
||||
<span class="save-error" x-show="error" x-text="error"></span>
|
||||
</div>
|
||||
<button class="neu-btn neu-btn--primary" @click="save()" :disabled="saving">
|
||||
<span x-show="!saving">💾 Sauvegarder</span>
|
||||
<span x-show="!saving"><i class="lnid-check-circle-1"></i> Sauvegarder</span>
|
||||
<span x-show="saving"><span class="spinner-sm"></span></span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark")})()</script>
|
||||
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark");document.documentElement.setAttribute("data-sidebar",localStorage.getItem("pxp_sidebar_pos")||"left")})()</script>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ProxmoxPanel — Terminal</title>
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
<link rel="stylesheet" href="/css/dark.css" />
|
||||
<link rel="stylesheet" href="/css/light.css" />
|
||||
<link rel="stylesheet" href="/css/pages.css" />
|
||||
<link rel="stylesheet" href="/css/lineicons-duotone.css" />
|
||||
<link rel="stylesheet" href="/css/xterm.css" />
|
||||
<script src="/js/vendors/htmx.min.js"></script>
|
||||
<script src="/js/vendors/swup.iife.js"></script>
|
||||
|
|
@ -21,18 +22,26 @@
|
|||
|
||||
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
|
||||
<div class="sidebar-header">
|
||||
<span class="sidebar-logo">⬡</span>
|
||||
<i class="sidebar-logo lnid-layout-1"></i>
|
||||
<span class="sidebar-title" x-show="!collapsed">ProxmoxPanel</span>
|
||||
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()">☰</button>
|
||||
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()">
|
||||
<i class="lnid-menu-hamburger-1"></i>
|
||||
</button>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<template x-for="item in navItems" :key="item.id">
|
||||
<a class="sidebar-link" :class="{ active: isActive(item.id) }" :href="item.href" @click.prevent="navigate(item.href)">
|
||||
<span class="sidebar-icon" x-text="item.icon"></span>
|
||||
<i class="sidebar-icon" :class="item.iconClass"></i>
|
||||
<span class="sidebar-label" x-show="!collapsed" x-text="t(item.labelKey)"></span>
|
||||
</a>
|
||||
</template>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<a class="sidebar-link" href="/profile.html" @click.prevent="navigate('/profile.html')">
|
||||
<i class="sidebar-icon lnid-user-circle-1"></i>
|
||||
<span class="sidebar-label" x-show="!collapsed" x-text="$store.auth.user?.username || t('nav.profile')"></span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="main-layout terminal-wrapper">
|
||||
|
|
@ -42,17 +51,21 @@
|
|||
<select class="neu-input neu-input--sm" x-model="lang" @change="setLang($event.target.value)">
|
||||
<option value="fr">FR</option><option value="en">EN</option>
|
||||
</select>
|
||||
<button class="neu-btn neu-btn--sm" @click="toggleTheme()">
|
||||
<span x-text="theme==='dark'?'☀':'🌙'"></span>
|
||||
<button class="neu-btn neu-btn--sm" @click="toggleTheme()" :title="theme==='dark'?t('navbar.lightMode'):t('navbar.darkMode')">
|
||||
<i :class="theme==='dark' ? 'lnid-sun-1' : 'lnid-moon-half-left-1'"></i>
|
||||
</button>
|
||||
<button class="neu-btn neu-btn--sm" @click="logout()" :title="t('navbar.logout')">
|
||||
<i class="lnid-power-button"></i>
|
||||
</button>
|
||||
<button class="neu-btn neu-btn--sm" @click="logout()">⏻</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main id="swup" class="page-content transition-fade terminal-layout">
|
||||
<div class="terminal-toolbar">
|
||||
<span id="terminal-status" class="terminal-status">⌛ Connexion…</span>
|
||||
<button id="terminal-clear" class="neu-btn neu-btn--sm">✕ Effacer</button>
|
||||
<button id="terminal-clear" class="neu-btn neu-btn--sm">
|
||||
<i class="lnid-trash-1"></i> Effacer
|
||||
</button>
|
||||
</div>
|
||||
<div id="terminal-container" class="terminal-container"></div>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark")})()</script>
|
||||
<script>(function(){document.documentElement.setAttribute("data-theme",localStorage.getItem("pxp_theme")||"dark");document.documentElement.setAttribute("data-sidebar",localStorage.getItem("pxp_sidebar_pos")||"left")})()</script>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ProxmoxPanel — Mises à jour</title>
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
<link rel="stylesheet" href="/css/dark.css" />
|
||||
<link rel="stylesheet" href="/css/light.css" />
|
||||
<link rel="stylesheet" href="/css/pages.css" />
|
||||
<link rel="stylesheet" href="/css/lineicons-duotone.css" />
|
||||
<script src="/js/vendors/htmx.min.js"></script>
|
||||
<script src="/js/vendors/swup.iife.js"></script>
|
||||
<script src="/js/app.js"></script>
|
||||
|
|
@ -18,18 +19,26 @@
|
|||
|
||||
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
|
||||
<div class="sidebar-header">
|
||||
<span class="sidebar-logo">⬡</span>
|
||||
<i class="sidebar-logo lnid-layout-1"></i>
|
||||
<span class="sidebar-title" x-show="!collapsed">ProxmoxPanel</span>
|
||||
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()">☰</button>
|
||||
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()">
|
||||
<i class="lnid-menu-hamburger-1"></i>
|
||||
</button>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<template x-for="item in navItems" :key="item.id">
|
||||
<a class="sidebar-link" :class="{ active: isActive(item.id) }" :href="item.href" @click.prevent="navigate(item.href)">
|
||||
<span class="sidebar-icon" x-text="item.icon"></span>
|
||||
<i class="sidebar-icon" :class="item.iconClass"></i>
|
||||
<span class="sidebar-label" x-show="!collapsed" x-text="t(item.labelKey)"></span>
|
||||
</a>
|
||||
</template>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<a class="sidebar-link" href="/profile.html" @click.prevent="navigate('/profile.html')">
|
||||
<i class="sidebar-icon lnid-user-circle-1"></i>
|
||||
<span class="sidebar-label" x-show="!collapsed" x-text="$store.auth.user?.username || t('nav.profile')"></span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="main-layout">
|
||||
|
|
@ -39,10 +48,12 @@
|
|||
<select class="neu-input neu-input--sm" x-model="lang" @change="setLang($event.target.value)">
|
||||
<option value="fr">FR</option><option value="en">EN</option>
|
||||
</select>
|
||||
<button class="neu-btn neu-btn--sm" @click="toggleTheme()">
|
||||
<span x-text="theme==='dark'?'☀':'🌙'"></span>
|
||||
<button class="neu-btn neu-btn--sm" @click="toggleTheme()" :title="theme==='dark'?t('navbar.lightMode'):t('navbar.darkMode')">
|
||||
<i :class="theme==='dark' ? 'lnid-sun-1' : 'lnid-moon-half-left-1'"></i>
|
||||
</button>
|
||||
<button class="neu-btn neu-btn--sm" @click="logout()" :title="t('navbar.logout')">
|
||||
<i class="lnid-power-button"></i>
|
||||
</button>
|
||||
<button class="neu-btn neu-btn--sm" @click="logout()">⏻</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
|
@ -58,11 +69,11 @@
|
|||
</div>
|
||||
<div class="page-actions-right">
|
||||
<button class="neu-btn" @click="checkAll()" :disabled="loading || targets.some(t=>t.checking)">
|
||||
↻ Tout vérifier
|
||||
<i class="lnid-refresh-circle-1-clockwise"></i> Tout vérifier
|
||||
</button>
|
||||
<button class="neu-btn neu-btn--primary" @click="updateAll()"
|
||||
:disabled="loading || jobStatus === 'running' || totalPackages === 0">
|
||||
↑ Tout mettre à jour
|
||||
<i class="lnid-arrow-upward"></i> Tout mettre à jour
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -96,14 +107,16 @@
|
|||
Non vérifié
|
||||
</span>
|
||||
<span x-show="!target.checking && target.packages !== null && target.packages.length === 0"
|
||||
class="up-to-date">✓ À jour</span>
|
||||
class="up-to-date">
|
||||
<i class="lnid-check-circle-1"></i> À jour
|
||||
</span>
|
||||
<span x-show="!target.checking && target.packages !== null && target.packages.length > 0"
|
||||
class="has-updates">
|
||||
<span x-text="target.packages.length"></span> paquet(s) à mettre à jour
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Package list (collapsible) -->
|
||||
<!-- Package list -->
|
||||
<div class="package-list" x-show="target.packages && target.packages.length > 0">
|
||||
<template x-for="pkg in target.packages" :key="pkg.name">
|
||||
<div class="package-row">
|
||||
|
|
@ -119,15 +132,14 @@
|
|||
|
||||
<!-- Card actions -->
|
||||
<div class="target-actions">
|
||||
<button class="neu-btn neu-btn--sm" @click="checkTarget(target)"
|
||||
:disabled="target.checking">
|
||||
↻ Vérifier
|
||||
<button class="neu-btn neu-btn--sm" @click="checkTarget(target)" :disabled="target.checking">
|
||||
<i class="lnid-refresh-circle-1-clockwise"></i> Vérifier
|
||||
</button>
|
||||
<button class="neu-btn neu-btn--sm neu-btn--primary"
|
||||
@click="updateTarget(target)"
|
||||
:disabled="target.updating || jobStatus === 'running' || !target.packages || target.packages.length === 0"
|
||||
x-show="target.status !== 'stopped'">
|
||||
<span x-show="!target.updating">↑ Mettre à jour</span>
|
||||
<span x-show="!target.updating"><i class="lnid-arrow-upward"></i> Mettre à jour</span>
|
||||
<span x-show="target.updating"><span class="spinner-sm"></span> En cours…</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue