feat: Add Docker image update system (TrueNAS Scale inspired)

- Implement UpdateService for image version checking and atomic updates
- Add DockerComposeManager for centralized docker-compose management
- Create 12 docker-compose references in /home/innotex/Docker
- Add 13 new API endpoints (6 for images, 7 for compose management)
- Add comprehensive documentation and examples
This commit is contained in:
innotex
2026-01-16 19:37:23 +01:00
parent 9ec63a8aa2
commit c51592c7ea
23 changed files with 3780 additions and 132 deletions

View File

@@ -1,63 +1,86 @@
<template>
<div id="app" class="min-h-screen bg-gray-900">
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
<h1 class="text-2xl font-bold text-blue-500">InnotexBoard</h1>
</div>
<div class="flex items-center space-x-4">
<span v-if="authStore.isAuthenticated" class="text-gray-300">
{{ authStore.username }}
<div id="app" class="min-h-screen bg-gray-50">
<!-- Navigation -->
<nav class="sticky top-0 z-50 bg-white border-b border-gray-300 shadow-md">
<div class="max-w-full mx-auto px-6 sm:px-8 lg:px-10">
<div class="flex justify-between items-center h-24">
<!-- Logo -->
<router-link to="/" class="flex items-center space-x-4 group">
<img
src="./assets/images/logo.png"
alt="InnotexBoard Logo"
class="w-32 h-20 group-hover:scale-110 transition-transform rounded-lg shadow-lg object-contain bg-white rounded-lg p-1"
style="background: transparent;"
/>
<div class="flex flex-col">
<span class="text-3xl font-bold bg-gradient-to-r from-blue-600 to-cyan-500 bg-clip-text text-transparent group-hover:from-blue-500 group-hover:to-cyan-400 transition-all">
InnotexBoard
</span>
<span class="text-xs text-gray-500 mt-1">Votre Homelab Personnel</span>
</div>
</router-link>
<!-- User & Actions -->
<div class="flex items-center space-x-6">
<span v-if="authStore.isAuthenticated" class="text-sm text-gray-700 px-3 py-1 bg-blue-100 rounded-full border border-blue-200">
👤 {{ authStore.username }}
</span>
<button
v-if="authStore.isAuthenticated"
@click="handleLogout"
class="btn btn-danger btn-small"
v-if="!authStore.isAuthenticated"
@click="goToLogin"
class="px-4 py-2 text-sm font-semibold text-white bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-500 hover:to-blue-600 rounded-lg transition-all duration-300 hover:shadow-lg hover:shadow-blue-500/30"
>
Déconnexion
🔐 Connexion
</button>
<button
v-else
@click="handleLogout"
class="px-4 py-2 text-sm font-semibold text-red-600 bg-red-100 hover:bg-red-200 border border-red-300 rounded-lg transition-all duration-300"
>
🚪 Déconnexion
</button>
</div>
</div>
</div>
</nav>
<!-- Layout -->
<div v-if="authStore.isAuthenticated" class="flex">
<!-- Sidebar -->
<aside class="w-64 bg-gray-800 border-r border-gray-700 min-h-screen">
<nav class="p-4 space-y-2">
<aside class="w-64 bg-white border-r border-gray-200 min-h-screen sticky top-20">
<nav class="p-6 space-y-2">
<router-link
to="/dashboard"
class="block px-4 py-2 rounded-lg hover:bg-gray-700 transition"
:class="{ 'bg-gray-700': $route.path === '/dashboard' }"
class="block px-4 py-3 rounded-lg font-medium text-gray-700 hover:text-blue-600 hover:bg-blue-50 transition-all duration-300 border border-transparent hover:border-blue-200"
:class="{ 'bg-blue-50 border-blue-300 text-blue-600': $route.path === '/dashboard' }"
>
📊 Dashboard
</router-link>
<router-link
to="/containers"
class="block px-4 py-2 rounded-lg hover:bg-gray-700 transition"
:class="{ 'bg-gray-700': $route.path === '/containers' }"
class="block px-4 py-3 rounded-lg font-medium text-gray-700 hover:text-blue-600 hover:bg-blue-50 transition-all duration-300 border border-transparent hover:border-blue-200"
:class="{ 'bg-blue-50 border-blue-300 text-blue-600': $route.path === '/containers' }"
>
🐳 Conteneurs Docker
</router-link>
<router-link
to="/disks"
class="block px-4 py-2 rounded-lg hover:bg-gray-700 transition"
:class="{ 'bg-gray-700': $route.path === '/disks' }"
class="block px-4 py-3 rounded-lg font-medium text-gray-700 hover:text-blue-600 hover:bg-blue-50 transition-all duration-300 border border-transparent hover:border-blue-200"
:class="{ 'bg-blue-50 border-blue-300 text-blue-600': $route.path === '/disks' }"
>
💾 Disques et Partitions
</router-link>
<router-link
to="/packages"
class="block px-4 py-2 rounded-lg hover:bg-gray-700 transition"
:class="{ 'bg-gray-700': $route.path === '/packages' }"
class="block px-4 py-3 rounded-lg font-medium text-gray-700 hover:text-blue-600 hover:bg-blue-50 transition-all duration-300 border border-transparent hover:border-blue-200"
:class="{ 'bg-blue-50 border-blue-300 text-blue-600': $route.path === '/packages' }"
>
📦 App Store
</router-link>
<router-link
to="/shortcuts"
class="block px-4 py-2 rounded-lg hover:bg-gray-700 transition"
:class="{ 'bg-gray-700': $route.path === '/shortcuts' }"
class="block px-4 py-3 rounded-lg font-medium text-gray-700 hover:text-blue-600 hover:bg-blue-50 transition-all duration-300 border border-transparent hover:border-blue-200"
:class="{ 'bg-blue-50 border-blue-300 text-blue-600': $route.path === '/shortcuts' }"
>
🔗 Raccourcis Services
</router-link>
@@ -65,7 +88,7 @@
</aside>
<!-- Main Content -->
<main class="flex-1">
<main class="flex-1 overflow-auto">
<router-view />
</main>
</div>
@@ -80,6 +103,10 @@ import { useRouter } from 'vue-router'
const authStore = useAuthStore()
const router = useRouter()
const goToLogin = () => {
router.push('/login')
}
const handleLogout = () => {
authStore.logout()
router.push('/login')

View File

@@ -0,0 +1,18 @@
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<!-- Background -->
<circle cx="100" cy="100" r="100" fill="#1a1a2e"/>
<!-- Outer ring -->
<circle cx="100" cy="100" r="95" fill="none" stroke="#00d9ff" stroke-width="2"/>
<circle cx="100" cy="100" r="90" fill="none" stroke="#0066ff" stroke-width="1" opacity="0.5"/>
<!-- Inner circle -->
<circle cx="100" cy="100" r="75" fill="none" stroke="#ffffff" stroke-width="3"/>
<!-- Text/Letter I -->
<text x="100" y="115" font-size="80" font-weight="bold" fill="#ffffff" text-anchor="middle" font-family="Arial, sans-serif">I</text>
<!-- Accent dots -->
<circle cx="55" cy="40" r="4" fill="#00d9ff"/>
<circle cx="145" cy="160" r="4" fill="#0066ff"/>
</svg>

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,394 @@
/* ============================================
CHARTE GRAPHIQUE INNOTEX
============================================ */
:root {
/* Couleurs primaires Innotex - Blanc avec accents bleu/cyan */
--color-innotex-white: #ffffff;
--color-innotex-light-gray: #f8f9fa;
--color-innotex-gray-light: #e9ecef;
--color-innotex-gray-medium: #dee2e6;
--color-innotex-gray-dark: #495057;
/* Accents de couleur */
--color-accent-primary: #0066ff; /* Bleu électrique */
--color-accent-secondary: #00d9ff; /* Cyan vif */
--color-accent-tertiary: #0052cc; /* Bleu foncé */
/* Palettes fonctionnelles */
--color-success: #00c896;
--color-warning: #ffa500;
--color-error: #ff4444;
--color-info: #0066ff;
/* Backgrounds */
--bg-primary: #f5f7fb;
--bg-secondary: #ffffff;
--bg-tertiary: #f0f3f7;
--bg-surface: #e9ecef;
/* Text */
--text-primary: #1a1a2e;
--text-secondary: #495057;
--text-muted: #868e96;
/* Borders */
--border-color: #dee2e6;
--border-color-light: #e9ecef;
/* Shadows */
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.08);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.12);
--shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.15);
--shadow-xl: 0 20px 50px rgba(0, 0, 0, 0.2);
/* Gradients */
--gradient-accent: linear-gradient(135deg, var(--color-accent-primary) 0%, var(--color-accent-secondary) 100%);
--gradient-light: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%);
--gradient-neon: linear-gradient(135deg, var(--color-accent-primary) 0%, var(--color-accent-tertiary) 100%);
/* Transitions */
--transition-fast: 150ms ease-in-out;
--transition-normal: 300ms ease-in-out;
--transition-slow: 500ms ease-in-out;
/* Border radius */
--radius-sm: 6px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 20px;
}
/* ============================================
RESET & BASE STYLES
============================================ */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
}
/* ============================================
SCROLLBAR STYLING
============================================ */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--color-accent-primary);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-accent-secondary);
}
/* ============================================
TYPOGRAPHY
============================================ */
h1, h2, h3, h4, h5, h6 {
font-weight: 700;
letter-spacing: -0.5px;
}
h1 {
font-size: 2.5rem;
line-height: 1.2;
}
h2 {
font-size: 2rem;
line-height: 1.3;
}
h3 {
font-size: 1.5rem;
line-height: 1.4;
}
p {
color: var(--text-secondary);
}
a {
color: var(--color-accent-primary);
text-decoration: none;
transition: color var(--transition-fast);
}
a:hover {
color: var(--color-accent-secondary);
}
/* ============================================
BUTTONS
============================================ */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.75rem 1.5rem;
border: none;
border-radius: var(--radius-md);
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all var(--transition-fast);
text-transform: uppercase;
letter-spacing: 0.5px;
position: relative;
overflow: hidden;
}
.btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.1);
transition: left var(--transition-fast);
border-radius: inherit;
}
.btn:hover::before {
left: 100%;
}
.btn-primary {
background: var(--gradient-accent);
color: var(--text-primary);
border: none;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0, 102, 255, 0.3);
}
.btn-secondary {
background: var(--bg-surface);
color: var(--text-primary);
border: 1px solid var(--border-color-light);
}
.btn-secondary:hover {
background: var(--bg-tertiary);
border-color: var(--color-accent-primary);
}
.btn-danger {
background: rgba(255, 68, 68, 0.1);
color: #ff4444;
border: 1px solid rgba(255, 68, 68, 0.3);
}
.btn-danger:hover {
background: rgba(255, 68, 68, 0.2);
border-color: #ff4444;
}
.btn-small {
padding: 0.5rem 1rem;
font-size: 0.85rem;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
}
/* ============================================
INPUTS & FORMS
============================================ */
input,
textarea,
select {
background: var(--bg-surface);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 0.75rem 1rem;
font-size: 0.95rem;
transition: all var(--transition-fast);
font-family: inherit;
}
input:focus,
textarea:focus,
select:focus {
outline: none;
border-color: var(--color-accent-primary);
box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1);
background: rgba(0, 102, 255, 0.05);
}
input::placeholder {
color: var(--text-muted);
}
/* ============================================
CARDS & CONTAINERS
============================================ */
.card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 1.5rem;
transition: all var(--transition-normal);
}
.card:hover {
border-color: var(--border-color-light);
box-shadow: var(--shadow-lg);
}
.card-elevated {
box-shadow: var(--shadow-md);
}
.card-accent {
border-left: 4px solid var(--color-accent-primary);
}
/* ============================================
BADGES
============================================ */
.badge {
display: inline-block;
padding: 0.4rem 0.8rem;
border-radius: var(--radius-sm);
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.badge-success {
background: rgba(0, 200, 150, 0.15);
color: var(--color-success);
}
.badge-warning {
background: rgba(255, 165, 0, 0.15);
color: var(--color-warning);
}
.badge-error {
background: rgba(255, 68, 68, 0.15);
color: var(--color-error);
}
.badge-info {
background: rgba(0, 102, 255, 0.15);
color: var(--color-info);
}
/* ============================================
UTILITIES
============================================ */
.glow {
animation: glow 2s ease-in-out infinite;
}
@keyframes glow {
0%, 100% {
text-shadow: 0 0 5px rgba(0, 102, 255, 0.5);
}
50% {
text-shadow: 0 0 20px rgba(0, 102, 255, 0.8);
}
}
.pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.fade-in {
animation: fadeIn 0.5s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.slide-in {
animation: slideIn 0.5s ease-in-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* ============================================
RESPONSIVE
============================================ */
@media (max-width: 768px) {
h1 {
font-size: 1.75rem;
}
h2 {
font-size: 1.5rem;
}
h3 {
font-size: 1.25rem;
}
.btn {
padding: 0.6rem 1.2rem;
font-size: 0.9rem;
}
}

View File

@@ -2,6 +2,7 @@ import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import './assets/innotex-theme.css'
import './assets/styles.css'
const app = createApp(App)

View File

@@ -1,77 +1,35 @@
<template>
<div class="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900">
<!-- Header -->
<div class="sticky top-0 z-40 backdrop-blur-xl bg-gray-900/80 border-b border-gray-700/50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div class="flex justify-between items-center">
<div>
<h1 class="text-4xl font-bold text-white mb-2">
<span class="bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent">
InnotexBoard
</span>
</h1>
<p class="text-gray-400">Votre Homelab Personnel</p>
</div>
<div class="flex items-center space-x-4">
<span v-if="authStore.isAuthenticated" class="text-sm text-gray-300 bg-gray-800 px-3 py-1 rounded-full">
{{ authStore.username }}
</span>
<button
v-if="authStore.isAuthenticated"
@click="goToAdmin"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition text-sm font-medium"
>
Gérer
</button>
<button
v-if="!authStore.isAuthenticated"
@click="goToLogin"
class="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition text-sm font-medium"
>
Connexion
</button>
<button
v-else
@click="handleLogout"
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition text-sm font-medium"
>
Déconnexion
</button>
</div>
</div>
</div>
</div>
<div class="min-h-screen bg-gray-50">
<!-- Main Content -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<main class="max-w-full mx-auto px-4 sm:px-6 lg:px-8 py-12 bg-gray-50 min-h-screen">
<!-- Quick Stats (if authenticated) -->
<div v-if="authStore.isAuthenticated" class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-12">
<div class="bg-gray-800/50 backdrop-blur border border-gray-700/50 rounded-xl p-4">
<div class="text-gray-400 text-sm mb-1">Total Services</div>
<div class="text-3xl font-bold text-white">{{ shortcuts.length }}</div>
<div class="bg-white border border-gray-200 rounded-lg p-4 hover:border-blue-400 hover:shadow-md transition">
<div class="text-gray-600 text-sm mb-1">📊 Total Services</div>
<div class="text-3xl font-bold text-gray-900">{{ shortcuts.length }}</div>
</div>
<div class="bg-gray-800/50 backdrop-blur border border-gray-700/50 rounded-xl p-4">
<div class="text-gray-400 text-sm mb-1">Catégories</div>
<div class="text-3xl font-bold text-white">{{ categories.size }}</div>
<div class="bg-white border border-gray-200 rounded-lg p-4 hover:border-cyan-400 hover:shadow-md transition">
<div class="text-gray-600 text-sm mb-1">🏷 Catégories</div>
<div class="text-3xl font-bold text-gray-900">{{ categories.size }}</div>
</div>
<div class="bg-gray-800/50 backdrop-blur border border-gray-700/50 rounded-xl p-4">
<div class="text-gray-400 text-sm mb-1">État</div>
<div class="text-3xl font-bold text-green-400"> Actif</div>
<div class="bg-white border border-gray-200 rounded-lg p-4 hover:border-green-400 hover:shadow-md transition">
<div class="text-gray-600 text-sm mb-1"> État</div>
<div class="text-3xl font-bold text-green-600">Actif</div>
</div>
<div class="bg-gray-800/50 backdrop-blur border border-gray-700/50 rounded-xl p-4">
<div class="text-gray-400 text-sm mb-1">Dernier Update</div>
<div class="text-lg font-bold text-white">{{ lastUpdate }}</div>
<div class="bg-white border border-gray-200 rounded-lg p-4 hover:border-purple-400 hover:shadow-md transition">
<div class="text-gray-600 text-sm mb-1"> Dernier Update</div>
<div class="text-lg font-bold text-gray-900">{{ lastUpdate }}</div>
</div>
</div>
<!-- Shortcuts Grid by Category -->
<div v-if="shortcuts.length > 0">
<div v-for="(categoryShortcuts, category) in groupedShortcuts" :key="category" class="mb-12">
<div class="flex items-center mb-6">
<div class="h-1 w-8 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full mr-3"></div>
<h2 class="text-2xl font-bold text-white capitalize">{{ getCategoryTitle(category) }}</h2>
<span class="ml-3 px-3 py-1 bg-gray-700/50 text-gray-300 text-sm rounded-full">
{{ categoryShortcuts.length }}
<div v-for="(categoryShortcuts, category) in groupedShortcuts" :key="category" class="mb-16">
<div class="flex items-center mb-8">
<div class="h-1 w-2 bg-gradient-to-r from-blue-600 to-cyan-500 rounded-full mr-4"></div>
<h2 class="text-3xl font-bold text-gray-900 capitalize">{{ getCategoryTitle(category) }}</h2>
<span class="ml-4 px-3 py-1 bg-blue-100 text-blue-700 text-sm rounded-full border border-blue-300">
{{ categoryShortcuts.length }} service<span v-if="categoryShortcuts.length > 1">s</span>
</span>
</div>
@@ -82,34 +40,34 @@
:href="shortcut.url"
target="_blank"
rel="noopener noreferrer"
class="group relative bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700/50 hover:border-gray-600 rounded-xl p-6 transition-all duration-300 hover:shadow-2xl hover:shadow-blue-500/20 overflow-hidden"
:style="{ borderLeftColor: shortcut.color || '#3b82f6', borderLeftWidth: '4px' }"
class="group relative block bg-white border-2 border-gray-200 hover:border-blue-500 rounded-xl p-6 transition-all duration-300 hover:shadow-xl overflow-hidden cursor-pointer"
:style="{ borderTopColor: shortcut.color || '#0066ff', borderTopWidth: '3px' }"
>
<!-- Background gradient on hover -->
<div class="absolute inset-0 bg-gradient-to-br from-blue-500/5 to-purple-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div class="absolute inset-0 bg-gradient-to-br from-blue-50/50 to-cyan-50/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<!-- Content -->
<div class="relative z-10">
<!-- Icon -->
<div class="text-4xl mb-4 inline-block p-3 bg-gray-700/50 group-hover:bg-gray-600/50 rounded-lg transition">
<div class="text-6xl mb-4 inline-block p-4 bg-blue-50 group-hover:bg-blue-100 rounded-xl transition transform group-hover:scale-110 border border-blue-200 group-hover:border-blue-400">
{{ shortcut.icon }}
</div>
<!-- Title -->
<h3 class="font-bold text-white text-lg mb-2 group-hover:text-blue-400 transition truncate">
<h3 class="font-bold text-gray-900 text-lg mb-2 group-hover:text-blue-600 transition truncate">
{{ shortcut.name }}
</h3>
<!-- Description -->
<p v-if="shortcut.description" class="text-gray-400 text-sm mb-4 line-clamp-2">
<p v-if="shortcut.description" class="text-gray-600 text-sm mb-4 line-clamp-2 group-hover:text-gray-700 transition">
{{ shortcut.description }}
</p>
<!-- URL -->
<div class="flex items-center text-gray-500 text-xs group-hover:text-gray-300 transition">
<div class="flex items-center text-gray-500 text-xs group-hover:text-blue-600 transition">
<span class="truncate">{{ getHostname(shortcut.url) }}</span>
<svg class="w-4 h-4 ml-2 opacity-0 group-hover:opacity-100 transition transform group-hover:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-13.5-2.5H21m0 0l-3-3m3 3l-3 3" />
<svg class="w-4 h-4 ml-2 opacity-50 group-hover:opacity-100 transition transform group-hover:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4m-4-4l4-4m0 0l4 4m-4-4v12" />
</svg>
</div>
</div>
@@ -118,10 +76,10 @@
<button
v-if="authStore.isAuthenticated"
@click.prevent.stop="deleteShortcut(shortcut.id)"
class="absolute top-2 right-2 p-2 bg-red-500/0 hover:bg-red-500 text-red-400 hover:text-white rounded-lg transition opacity-0 group-hover:opacity-100 duration-300"
title="Supprimer"
class="absolute top-3 right-3 p-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition opacity-0 group-hover:opacity-100 duration-300 hover:scale-110 transform"
title="Supprimer ce raccourci"
>
🗑
</button>
</a>
</div>
@@ -129,31 +87,36 @@
</div>
<!-- Empty State -->
<div v-else class="flex flex-col items-center justify-center py-20">
<div class="text-6xl mb-4">🔗</div>
<h3 class="text-2xl font-bold text-white mb-2">Aucun Service Configuré</h3>
<p class="text-gray-400 mb-4">
<div v-else class="flex flex-col items-center justify-center py-32">
<div class="text-8xl mb-6">🔗</div>
<h3 class="text-3xl font-bold text-gray-900 mb-3">Aucun Service Configuré</h3>
<p class="text-gray-600 mb-8 text-center max-w-md">
<span v-if="authStore.isAuthenticated">
Ajoutez vos premiers services pour les afficher ici
Ajoutez vos premiers services pour commencer à centraliser vos accès
</span>
<span v-else>
Connectez-vous pour gérer vos services
Connectez-vous pour gérer et configurer vos services
</span>
</p>
<button
v-if="authStore.isAuthenticated"
@click="goToAdmin"
class="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition font-medium"
class="px-8 py-3 bg-gradient-to-r from-blue-600 to-cyan-500 hover:from-blue-500 hover:to-cyan-400 text-white rounded-lg transition font-medium shadow-lg shadow-blue-600/30 hover:shadow-xl hover:shadow-blue-600/50"
>
Ajouter un Service
Ajouter un Service
</button>
</div>
</main>
<!-- Footer -->
<footer class="border-t border-gray-800 mt-20 py-8">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center text-gray-500 text-sm">
<p>InnotexBoard v1.0 Votre centre de contrôle homelab personnel</p>
<footer class="border-t border-gray-200 mt-20 py-8 bg-white shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<p class="text-gray-700 text-sm font-semibold">
InnotexBoard v1.0
</p>
<p class="text-gray-600 text-xs">
Technologie : FastAPI Vue.js 3 TailwindCSS WebSocket
</p>
</div>
</footer>
</div>

View File

@@ -1,55 +1,107 @@
<template>
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-900 to-gray-800">
<div class="w-full max-w-md">
<div class="card border-blue-600/50">
<h2 class="text-3xl font-bold text-center mb-8 text-blue-400">InnotexBoard</h2>
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-white to-gray-100 relative overflow-hidden">
<!-- Animated background elements -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<div class="absolute top-20 left-10 w-96 h-96 bg-blue-200/20 rounded-full blur-3xl"></div>
<div class="absolute bottom-20 right-10 w-96 h-96 bg-cyan-200/20 rounded-full blur-3xl"></div>
<div class="absolute top-1/2 left-1/2 w-96 h-96 bg-blue-100/10 rounded-full blur-3xl -translate-x-1/2 -translate-y-1/2"></div>
</div>
<div class="w-full max-w-md px-4 relative z-10">
<!-- Card Container -->
<div class="bg-white border border-gray-200 rounded-2xl shadow-lg p-8 backdrop-blur-sm">
<!-- Header -->
<div class="text-center mb-8">
<img
src="../assets/images/logo.png"
alt="InnotexBoard Logo"
class="w-32 h-20 mx-auto mb-4 object-contain"
style="background: transparent;"
/>
<h1 class="text-3xl font-bold mb-2">
<span class="bg-gradient-to-r from-blue-600 to-cyan-500 bg-clip-text text-transparent">
InnotexBoard
</span>
</h1>
<p class="text-gray-600 text-sm">Accédez à votre centre de contrôle</p>
</div>
<!-- Login Form -->
<form @submit.prevent="handleLogin" class="space-y-6">
<!-- Username Input -->
<div>
<label for="username" class="block text-sm font-medium text-gray-300 mb-2">
Nom d'utilisateur
<label for="username" class="block text-sm font-semibold text-gray-700 mb-2">
👤 Identifiant
</label>
<input
id="username"
v-model="credentials.username"
type="text"
required
class="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20"
class="w-full px-4 py-3 bg-gray-50 border border-gray-300 rounded-lg text-gray-900 placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 transition"
placeholder="votre_utilisateur"
autofocus
/>
</div>
<!-- Password Input -->
<div>
<label for="password" class="block text-sm font-medium text-gray-300 mb-2">
Mot de passe
<label for="password" class="block text-sm font-semibold text-gray-700 mb-2">
🔐 Mot de passe
</label>
<input
id="password"
v-model="credentials.password"
type="password"
required
class="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20"
class="w-full px-4 py-3 bg-gray-50 border border-gray-300 rounded-lg text-gray-900 placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 transition"
placeholder="••••••••"
/>
</div>
<div v-if="error" class="p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-red-400 text-sm">
{{ error }}
<!-- Error Message -->
<div v-if="error" class="p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm flex items-start gap-3">
<span class="text-lg"></span>
<div>{{ error }}</div>
</div>
<!-- Submit Button -->
<button
type="submit"
:disabled="loading"
class="w-full btn btn-primary py-2 font-medium"
class="w-full py-3 bg-gradient-to-r from-blue-600 to-cyan-500 hover:from-blue-500 hover:to-cyan-400 disabled:from-gray-400 disabled:to-gray-400 text-white font-semibold rounded-lg transition-all duration-300 transform hover:scale-105 disabled:scale-100 flex items-center justify-center gap-2 shadow-lg shadow-blue-600/30 hover:shadow-xl hover:shadow-blue-600/50"
>
<span v-if="loading">Connexion en cours...</span>
<span v-else>Se connecter</span>
<span v-if="loading"> Connexion...</span>
<span v-else>🚀 Se connecter</span>
</button>
</form>
<p class="text-center text-gray-400 text-sm mt-6">
⚙️ Authentification via PAM du système Debian
</p>
<!-- Divider -->
<div class="mt-6 relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-3 bg-white text-gray-500">ou</span>
</div>
</div>
<!-- Info Footer -->
<div class="mt-6 pt-6 space-y-2 text-center border-t border-gray-200">
<p class="text-gray-600 text-xs flex items-center justify-center gap-2">
<span></span>
<span>Authentification via PAM du système</span>
</p>
<p class="text-gray-500 text-xs">
Vos données sont sécurisées et chiffrées
</p>
</div>
</div>
<!-- Help Text -->
<div class="mt-6 text-center text-gray-500 text-xs">
<p>Besoin d'aide ? Contactez votre administrateur système</p>
</div>
</div>
</div>
@@ -79,7 +131,7 @@ const handleLogin = async () => {
console.log('Login success result:', success)
if (success) {
console.log('Navigating to dashboard')
console.log('Navigating to homelab')
await router.push('/')
} else {
error.value = 'Identifiants incorrects. Veuillez réessayer.'