Compare commits

..
9 Commits
Author SHA1 Message Date
xavier c8a38f3eb8 feat: ajout de l'épinglage et de la suppression des projets avec mise à jour UI
Build and Push Docker Image / build-and-push (push) Successful in 43s
2026-10-07 08:33:52 +02:00
xavier cac70a5aaa ci: intégration de docker/metadata-action pour générer dynamiquement les tags SemVer et latest
Build and Push Docker Image / build-and-push (push) Failing after 27s
2026-10-07 08:24:14 +02:00
xavier d41edde9bb feat: création de l'API de gestion des projets et liaison dynamique avec le Dashboard
Build and Push Docker Image / build-and-push (push) Successful in 39s
2026-10-07 08:17:41 +02:00
xavier fd53c75dee modif QRCode
Build and Push Docker Image / build-and-push (push) Successful in 41s
2026-10-07 08:10:14 +02:00
xavier e1d7ae87e3 correction authentification
Build and Push Docker Image / build-and-push (push) Successful in 44s
2026-10-07 08:02:17 +02:00
xavier 74b87a3b27 feat: création des modèles de base de données Project et Message
Build and Push Docker Image / build-and-push (push) Successful in 38s
2026-10-07 07:42:24 +02:00
xavier 02ba8bfff3 fix: configuration de StaticFiles sur FastAPI et mise à jour des liens du logo en local
Build and Push Docker Image / build-and-push (push) Successful in 39s
2026-10-07 02:04:48 +02:00
xavier 554f00f2b1 feat: ajout de la barre latérale rétractable, du champ de texte auto-extensible et intégration des logos
Build and Push Docker Image / build-and-push (push) Successful in 37s
2026-10-07 01:49:23 +02:00
xavier defa47ae45 feat: création de la structure UI du Dashboard (style Gemini) et liaison avec la route protégée
Build and Push Docker Image / build-and-push (push) Successful in 37s
2026-10-07 01:26:21 +02:00
10 changed files with 389 additions and 26 deletions

No files matched your search

+14 -1
View File
@@ -4,6 +4,8 @@ on:
push: push:
branches: branches:
- main - main
tags:
- 'v*' # Déclenche aussi le pipeline lors de la création d'un tag
jobs: jobs:
build-and-push: build-and-push:
@@ -16,6 +18,16 @@ jobs:
- name: Checkout du code - name: Checkout du code
uses: actions/checkout@v3 uses: actions/checkout@v3
# NOUVEAU : Génération dynamique des tags (SemVer + Latest)
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: gitea.aethas38.duckdns.org/xavier/multi-ia-codage
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=semver,pattern={{version}}
- name: Connexion au registre Gitea - name: Connexion au registre Gitea
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
@@ -28,4 +40,5 @@ jobs:
with: with:
context: . context: .
push: true push: true
tags: gitea.aethas38.duckdns.org/xavier/multi-ia-codage:latest tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+15 -3
View File
@@ -2,7 +2,8 @@ name: Build and Publish Docker Image
on: on:
push: push:
branches: [ "main" ] # Modifiez si votre branche s'appelle "master" branches: [ "main" ]
tags: [ 'v*' ] # Déclencheur sur les tags ajouté ici aussi
env: env:
REGISTRY: ghcr.io REGISTRY: ghcr.io
@@ -13,12 +14,22 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
packages: write # Indispensable pour publier le paquet sur le dépôt packages: write
steps: steps:
- name: Checkout du code - name: Checkout du code
uses: actions/checkout@v4 uses: actions/checkout@v4
# NOUVEAU : Identique à Gitea, gère ghcr.io
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=semver,pattern={{version}}
- name: Connexion au registre GitHub (ghcr.io) - name: Connexion au registre GitHub (ghcr.io)
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
@@ -31,4 +42,5 @@ jobs:
with: with:
context: . context: .
push: true push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+4 -1
View File
@@ -29,4 +29,7 @@ env/
# IDE et OS # IDE et OS
.vscode/ .vscode/
.DS_Store .DS_Store
Thumbs.db Thumbs.db
#perso
consignes.txt
+1 -1
View File
@@ -28,7 +28,7 @@ from datetime import datetime, timedelta, timezone
import os import os
# Dans l'idéal, cette clé secrète sera à placer dans votre fichier .env plus tard # Dans l'idéal, cette clé secrète sera à placer dans votre fichier .env plus tard
SECRET_KEY = os.getenv("SECRET_KEY", "`*\$Tu*CCc5hglT$mX'HykxERDljBz") SECRET_KEY = os.getenv("SECRET_KEY", r"`*\$Tu*CCc5hglT$mX'HykxERDljBz")
ALGORITHM = "HS256" ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60 ACCESS_TOKEN_EXPIRE_MINUTES = 60
+71 -5
View File
@@ -1,5 +1,6 @@
from fastapi import FastAPI, Depends, HTTPException, status, Request, Response from fastapi import FastAPI, Depends, HTTPException, status, Request, Response
from fastapi.responses import RedirectResponse, FileResponse from fastapi.responses import RedirectResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
import os import os
@@ -9,11 +10,21 @@ from .models import User
from .auth import get_password_hash, generate_totp_secret, get_totp_uri, verify_password, verify_totp, create_access_token, verify_token from .auth import get_password_hash, generate_totp_secret, get_totp_uri, verify_password, verify_totp, create_access_token, verify_token
from .schemas import AdminCreate, LoginRequest from .schemas import AdminCreate, LoginRequest
# MAJ BDD
from typing import List
from .schemas import AdminCreate, LoginRequest, ProjectCreate, ProjectResponse
from .models import User, Project
# Création des tables dans la base de données # Création des tables dans la base de données
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
app = FastAPI(title="AETHAS38 - Orchestrateur Multi-IA") app = FastAPI(title="AETHAS38 - Orchestrateur Multi-IA")
# Configuration des fichiers statiques (images, css locaux, etc.)
assets_path = os.path.join(os.getcwd(), "frontend", "assets")
os.makedirs(assets_path, exist_ok=True) # Crée le dossier s'il n'existe pas
app.mount("/static", StaticFiles(directory=assets_path), name="static")
def is_setup_required(db: Session) -> bool: def is_setup_required(db: Session) -> bool:
"""Vérifie si la base de données contient au moins un administrateur.""" """Vérifie si la base de données contient au moins un administrateur."""
admin = db.query(User).filter(User.is_admin == True).first() admin = db.query(User).filter(User.is_admin == True).first()
@@ -31,7 +42,6 @@ def setup_page(db: Session = Depends(get_db)):
if not is_setup_required(db): if not is_setup_required(db):
return RedirectResponse(url="/login") return RedirectResponse(url="/login")
# Chemin vers le fichier HTML depuis la racine de l'application Docker (/app)
frontend_path = os.path.join(os.getcwd(), "frontend", "index.html") frontend_path = os.path.join(os.getcwd(), "frontend", "index.html")
if not os.path.exists(frontend_path): if not os.path.exists(frontend_path):
raise HTTPException(status_code=404, detail="Interface introuvable.") raise HTTPException(status_code=404, detail="Interface introuvable.")
@@ -119,15 +129,71 @@ def login(login_data: LoginRequest, response: Response, db: Session = Depends(ge
@app.get("/dashboard") @app.get("/dashboard")
def dashboard(request: Request): def dashboard(request: Request):
"""Route protégée : nécessite un cookie de session valide.""" """Route protégée : affiche l'interface principale si le cookie est valide."""
token = request.cookies.get("session_token") token = request.cookies.get("session_token")
if not token: if not token:
return RedirectResponse(url="/login") return RedirectResponse(url="/login")
payload = verify_token(token) payload = verify_token(token)
if not payload: if not payload:
# Si le token a expiré (60 min) ou est invalide, retour au login
return RedirectResponse(url="/login") return RedirectResponse(url="/login")
username = payload.get("sub") frontend_path = os.path.join(os.getcwd(), "frontend", "dashboard.html")
return {"message": f"Bienvenue sur le tableau de bord sécurisé, {username} !"} if not os.path.exists(frontend_path):
raise HTTPException(status_code=404, detail="Interface du tableau de bord introuvable.")
return FileResponse(frontend_path)
# --- GESTION DES PROJETS ---
def get_current_user(request: Request, db: Session = Depends(get_db)):
"""Extrait l'utilisateur actuel à partir du cookie de session JWT."""
token = request.cookies.get("session_token")
if not token:
raise HTTPException(status_code=401, detail="Non authentifié")
payload = verify_token(token)
if not payload:
raise HTTPException(status_code=401, detail="Session expirée")
user = db.query(User).filter(User.username == payload.get("sub")).first()
if not user:
raise HTTPException(status_code=401, detail="Utilisateur introuvable")
return user
@app.get("/api/projects", response_model=List[ProjectResponse])
def get_projects(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Récupère tous les projets de l'utilisateur connecté."""
return db.query(Project).filter(Project.user_id == current_user.id).order_by(Project.created_at.desc()).all()
@app.post("/api/projects", response_model=ProjectResponse)
def create_project(project: ProjectCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Crée un nouveau projet pour l'utilisateur connecté."""
new_project = Project(title=project.title, user_id=current_user.id)
db.add(new_project)
db.commit()
db.refresh(new_project)
return new_project
@app.put("/api/projects/{project_id}/pin", response_model=ProjectResponse)
def toggle_pin_project(project_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Bascule le statut épinglé (is_pinned) d'un projet."""
project = db.query(Project).filter(Project.id == project_id, Project.user_id == current_user.id).first()
if not project:
raise HTTPException(status_code=404, detail="Projet introuvable")
project.is_pinned = not project.is_pinned
db.commit()
db.refresh(project)
return project
@app.delete("/api/projects/{project_id}")
def delete_project(project_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Supprime définitivement un projet et tous ses messages associés."""
project = db.query(Project).filter(Project.id == project_id, Project.user_id == current_user.id).first()
if not project:
raise HTTPException(status_code=404, detail="Projet introuvable")
db.delete(project)
db.commit()
return {"message": "Projet supprimé"}
+32 -4
View File
@@ -1,4 +1,5 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text
from sqlalchemy.orm import relationship
from datetime import datetime, timezone from datetime import datetime, timezone
from .database import Base from .database import Base
@@ -9,8 +10,35 @@ class User(Base):
email = Column(String, unique=True, index=True, nullable=False) email = Column(String, unique=True, index=True, nullable=False)
username = Column(String, unique=True, index=True, nullable=False) username = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False) hashed_password = Column(String, nullable=False)
totp_secret = Column(String, nullable=True) # Secret pour Keepassium/Authenticator totp_secret = Column(String, nullable=False)
is_admin = Column(Boolean, default=False) is_admin = Column(Boolean, default=False)
is_active = Column(Boolean, default=True)
# Relation : Un utilisateur peut avoir plusieurs projets
projects = relationship("Project", back_populates="owner", cascade="all, delete-orphan")
class Project(Base):
__tablename__ = "projects"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, nullable=False)
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
last_activity = Column(DateTime, nullable=True) # Servira pour la déconnexion après 60 min is_pinned = Column(Boolean, default=False)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# Relations
owner = relationship("User", back_populates="projects")
messages = relationship("Message", back_populates="project", cascade="all, delete-orphan")
class Message(Base):
__tablename__ = "messages"
id = Column(Integer, primary_key=True, index=True)
role = Column(String, nullable=False) # Ex: 'user', 'assistant', 'worker_1', 'system'
content = Column(Text, nullable=False)
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
project_id = Column(Integer, ForeignKey("projects.id"), nullable=False)
# Relation
project = relationship("Project", back_populates="messages")
+18 -2
View File
@@ -1,12 +1,28 @@
from pydantic import BaseModel, EmailStr from pydantic import BaseModel, EmailStr
from datetime import datetime
from typing import Optional
class AdminCreate(BaseModel): class AdminCreate(BaseModel):
email: EmailStr email: EmailStr
username: str username: str
password: str password: str
# NOUVEAU : Schéma pour la connexion
class LoginRequest(BaseModel): class LoginRequest(BaseModel):
username: str username: str
password: str password: str
totp_code: str totp_code: str
# --- NOUVEAU : Schémas pour les Projets ---
class ProjectBase(BaseModel):
title: str
class ProjectCreate(ProjectBase):
pass
class ProjectResponse(ProjectBase):
id: int
created_at: datetime
is_pinned: bool
class Config:
from_attributes = True
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

+230
View File
@@ -0,0 +1,230 @@
<!DOCTYPE html>
<html lang="fr" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AETHAS38 - Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: { colors: { aethas: { dark: '#0f172a', panel: '#1e293b', cyan: '#06b6d4', purple: '#a855f7' } } }
}
}
</script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<style>
body { background-color: #0f172a; color: #f8fafc; font-family: 'Inter', sans-serif; }
.neon-text { background: linear-gradient(to right, #06b6d4, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #334155; border-radius: 10px; }
</style>
</head>
<body class="h-screen flex overflow-hidden">
<div id="app" class="flex w-full h-full relative bg-aethas-dark">
<div v-if="isSidebarOpen" @click="toggleSidebar" class="md:hidden fixed inset-0 bg-black/60 z-40 transition-opacity"></div>
<div :class="['transition-all duration-300 flex-shrink-0 relative z-50', isSidebarOpen ? 'w-72' : 'w-0']">
<aside :class="['absolute top-0 left-0 h-full w-72 bg-aethas-panel border-r border-gray-700 flex flex-col transition-transform duration-300', isSidebarOpen ? 'translate-x-0' : '-translate-x-full']">
<div class="p-4 border-b border-gray-700 flex items-center justify-between">
<div class="flex items-center gap-3">
<img src="/static/logo.png" alt="Logo" class="w-10 h-10 rounded-full border border-aethas-cyan object-cover bg-gray-800 flex-shrink-0">
<span class="font-bold text-lg tracking-wider neon-text">AETHAS38</span>
</div>
<button @click="toggleSidebar" class="md:hidden text-gray-400 hover:text-white">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<div class="p-4">
<button @click="createNewProject" :disabled="isLoading" class="w-full bg-gray-800 hover:bg-gray-700 text-white border border-gray-600 rounded-lg py-2.5 px-4 flex items-center justify-center gap-2 transition-colors disabled:opacity-50 shadow-md">
<i data-lucide="plus" class="w-5 h-5 text-aethas-cyan"></i>
Nouveau Projet IA
</button>
</div>
<div class="flex-1 overflow-y-auto p-4 space-y-6">
<!-- Projets Épinglés -->
<div v-if="pinnedProjects.length > 0">
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">Épinglés</h3>
<ul class="space-y-1">
<li v-for="project in pinnedProjects" :key="project.id" class="group flex items-center justify-between p-2 hover:bg-gray-800 rounded cursor-pointer text-sm text-gray-300 transition-colors">
<div class="flex items-center gap-2 truncate">
<i data-lucide="pin" class="w-4 h-4 text-aethas-cyan fill-aethas-cyan flex-shrink-0"></i>
<span class="truncate">{{ project.title }}</span>
</div>
<div class="hidden group-hover:flex items-center gap-1 flex-shrink-0">
<button @click.stop="togglePin(project.id)" class="text-gray-400 hover:text-white p-1 rounded hover:bg-gray-700" title="Désépingler"><i data-lucide="pin-off" class="w-3.5 h-3.5"></i></button>
<button @click.stop="deleteProject(project.id)" class="text-gray-400 hover:text-red-400 p-1 rounded hover:bg-gray-700" title="Supprimer"><i data-lucide="trash-2" class="w-3.5 h-3.5"></i></button>
</div>
</li>
</ul>
</div>
<!-- Projets Récents -->
<div>
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">Récents</h3>
<ul class="space-y-1">
<li v-if="recentProjects.length === 0 && pinnedProjects.length === 0" class="text-sm text-gray-500 italic p-2">Aucun projet pour le moment.</li>
<li v-for="project in recentProjects" :key="project.id" class="group flex items-center justify-between p-2 hover:bg-gray-800 rounded cursor-pointer text-sm text-gray-300 transition-colors">
<div class="flex items-center gap-2 truncate">
<i data-lucide="message-square" class="w-4 h-4 text-gray-500 flex-shrink-0"></i>
<span class="truncate">{{ project.title }}</span>
</div>
<div class="hidden group-hover:flex items-center gap-1 flex-shrink-0">
<button @click.stop="togglePin(project.id)" class="text-gray-400 hover:text-white p-1 rounded hover:bg-gray-700" title="Épingler"><i data-lucide="pin" class="w-3.5 h-3.5"></i></button>
<button @click.stop="deleteProject(project.id)" class="text-gray-400 hover:text-red-400 p-1 rounded hover:bg-gray-700" title="Supprimer"><i data-lucide="trash-2" class="w-3.5 h-3.5"></i></button>
</div>
</li>
</ul>
</div>
</div>
<div class="p-4 border-t border-gray-700 flex justify-between items-center">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-full bg-gradient-to-tr from-aethas-cyan to-aethas-purple flex items-center justify-center text-sm font-bold shadow-md">
{{ username.charAt(0).toUpperCase() }}
</div>
<span class="text-sm font-medium">{{ username }}</span>
</div>
<button class="text-gray-400 hover:text-white transition-colors"><i data-lucide="settings" class="w-5 h-5"></i></button>
</div>
</aside>
</div>
<main class="flex-1 flex flex-col min-w-0 z-10 transition-all duration-300">
<header class="flex items-center justify-between p-4 border-b border-gray-700 bg-aethas-panel h-[73px]">
<div class="flex items-center gap-3">
<button @click="toggleSidebar" class="text-gray-400 hover:text-white transition-colors p-1.5 rounded hover:bg-gray-700" title="Basculer le menu">
<i data-lucide="menu" class="w-6 h-6"></i>
</button>
<div v-show="!isSidebarOpen" class="flex items-center gap-2 transition-opacity duration-300">
<img src="/static/logo.png" alt="Logo" class="w-8 h-8 rounded-full border border-aethas-cyan object-cover bg-gray-800 flex-shrink-0">
<span class="font-bold text-lg neon-text">AETHAS38</span>
</div>
</div>
</header>
<div class="flex-1 overflow-y-auto p-4 md:p-8 flex flex-col items-center justify-center">
<img src="/static/logo.png" alt="AETHAS38 Logo" class="w-24 h-24 mb-6 rounded-full border-2 border-aethas-cyan shadow-[0_0_15px_rgba(6,182,212,0.5)] object-cover bg-gray-800 flex-shrink-0">
<h2 class="text-2xl font-bold text-gray-200 mb-2 text-center">Comment puis-je vous aider aujourd'hui ?</h2>
<p class="text-gray-500 text-sm text-center">L'orchestrateur Multi-IA est prêt.</p>
</div>
<div class="p-4 md:p-6 w-full max-w-4xl mx-auto">
<div class="relative flex items-end bg-gray-800 border border-gray-600 rounded-2xl p-2 shadow-lg focus-within:border-aethas-cyan focus-within:ring-1 focus-within:ring-aethas-cyan transition-all">
<button class="p-3 text-gray-400 hover:text-aethas-cyan transition-colors flex-shrink-0" title="Joindre un fichier">
<i data-lucide="paperclip" class="w-5 h-5"></i>
</button>
<textarea
v-model="prompt"
@input="autoResize"
ref="promptTextarea"
rows="1"
class="w-full bg-transparent text-gray-200 p-3 max-h-[200px] overflow-y-auto focus:outline-none resize-none leading-relaxed"
placeholder="Posez votre question à l'Orchestrateur..."></textarea>
<button class="p-3 text-aethas-cyan hover:text-white transition-colors flex-shrink-0" title="Envoyer">
<i data-lucide="send" class="w-5 h-5"></i>
</button>
</div>
<p class="text-center text-xs text-gray-500 mt-3">AETHAS38 peut faire des erreurs. Vérifiez toujours les informations importantes.</p>
</div>
</main>
</div>
<script>
// Import de "computed" ajouté pour filtrer les listes
const { createApp, ref, onMounted, nextTick, computed } = Vue;
createApp({
setup() {
const username = ref("Admin");
const isSidebarOpen = ref(true);
const prompt = ref("");
const promptTextarea = ref(null);
const projects = ref([]);
const isLoading = ref(false);
// Propriétés calculées pour séparer les projets épinglés et récents
const pinnedProjects = computed(() => projects.value.filter(p => p.is_pinned));
const recentProjects = computed(() => projects.value.filter(p => !p.is_pinned));
const toggleSidebar = () => { isSidebarOpen.value = !isSidebarOpen.value; };
const autoResize = () => {
const el = promptTextarea.value;
if (!el) return;
el.style.height = 'auto';
el.style.height = el.scrollHeight + 'px';
};
const fetchProjects = async () => {
try {
const response = await fetch('/api/projects');
if (response.ok) {
projects.value = await response.json();
nextTick(() => lucide.createIcons());
}
} catch (error) { console.error("Erreur", error); }
};
const createNewProject = async () => {
isLoading.value = true;
try {
const response = await fetch('/api/projects', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: "Nouvelle discussion" })
});
if (response.ok) {
const newProject = await response.json();
projects.value.unshift(newProject);
nextTick(() => lucide.createIcons());
}
} catch (error) { console.error("Erreur", error); }
finally { isLoading.value = false; }
};
// Fonction pour basculer l'état épinglé
const togglePin = async (projectId) => {
try {
const response = await fetch(`/api/projects/${projectId}/pin`, { method: 'PUT' });
if (response.ok) {
const updatedProject = await response.json();
const index = projects.value.findIndex(p => p.id === projectId);
if (index !== -1) projects.value[index] = updatedProject;
nextTick(() => lucide.createIcons());
}
} catch (error) { console.error("Erreur d'épinglage", error); }
};
// Fonction pour supprimer un projet
const deleteProject = async (projectId) => {
if (!confirm("Voulez-vous vraiment supprimer ce projet ?")) return;
try {
const response = await fetch(`/api/projects/${projectId}`, { method: 'DELETE' });
if (response.ok) {
projects.value = projects.value.filter(p => p.id !== projectId);
}
} catch (error) { console.error("Erreur de suppression", error); }
};
onMounted(() => {
lucide.createIcons();
fetchProjects();
});
return {
username, isSidebarOpen, toggleSidebar, prompt, autoResize, promptTextarea,
projects, pinnedProjects, recentProjects, createNewProject, isLoading, togglePin, deleteProject
};
}
}).mount('#app');
</script>
</body>
</html>
+4 -9
View File
@@ -14,7 +14,6 @@
} }
</script> </script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
<style> <style>
body { background-color: #0f172a; color: #f8fafc; font-family: 'Inter', sans-serif; } body { background-color: #0f172a; color: #f8fafc; font-family: 'Inter', sans-serif; }
.glass-panel { background: rgba(30, 41, 59, 0.7); backdrop-filter: blur(10px); border: 1px solid rgba(6, 182, 212, 0.2); box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5); } .glass-panel { background: rgba(30, 41, 59, 0.7); backdrop-filter: blur(10px); border: 1px solid rgba(6, 182, 212, 0.2); box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5); }
@@ -25,7 +24,8 @@
<div id="app" class="w-full max-w-md transition-all duration-500"> <div id="app" class="w-full max-w-md transition-all duration-500">
<div class="text-center mb-8"> <div class="text-center mb-8">
<img src="https://via.placeholder.com/150x150/0f172a/06b6d4?text=Logo+A38" alt="AETHAS38 Logo" class="w-24 h-24 mx-auto mb-4 rounded-full border-2 border-aethas-cyan shadow-[0_0_15px_rgba(6,182,212,0.5)]"> <!-- Chemin du logo corrigé -->
<img src="/static/logo.png" alt="AETHAS38 Logo" class="w-24 h-24 mx-auto mb-4 rounded-full border-2 border-aethas-cyan shadow-[0_0_15px_rgba(6,182,212,0.5)] object-cover bg-gray-800">
<h1 class="text-3xl font-bold tracking-wider neon-text">AETHAS38</h1> <h1 class="text-3xl font-bold tracking-wider neon-text">AETHAS38</h1>
<p class="text-gray-400 text-sm mt-2">Configuration de l'Orchestrateur Multi-IA</p> <p class="text-gray-400 text-sm mt-2">Configuration de l'Orchestrateur Multi-IA</p>
</div> </div>
@@ -58,7 +58,6 @@
</div> </div>
<h2 class="text-xl font-bold text-white">Compte créé avec succès</h2> <h2 class="text-xl font-bold text-white">Compte créé avec succès</h2>
<!-- CORRECTION ICI : Utilisation d'une balise <img> avec rendu direct -->
<div class="bg-white p-4 rounded-xl inline-block mx-auto min-w-[200px] min-h-[200px] flex items-center justify-center"> <div class="bg-white p-4 rounded-xl inline-block mx-auto min-w-[200px] min-h-[200px] flex items-center justify-center">
<img v-if="qrCodeUrl" :src="qrCodeUrl" alt="QR Code 2FA" class="w-full h-full object-contain"> <img v-if="qrCodeUrl" :src="qrCodeUrl" alt="QR Code 2FA" class="w-full h-full object-contain">
<span v-else class="text-gray-500 text-sm">Génération...</span> <span v-else class="text-gray-500 text-sm">Génération...</span>
@@ -108,12 +107,8 @@
totpSecret.value = data.totp_secret; totpSecret.value = data.totp_secret;
setupComplete.value = true; setupComplete.value = true;
// CORRECTION ICI : Génération d'une URL d'image (Data URL) au lieu du Canvas // CORRECTION ICI : Génération par API externe ultra fiable (plus besoin de librairie)
qrCodeUrl.value = await QRCode.toDataURL(data.totp_uri, { qrCodeUrl.value = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(data.totp_uri)}&color=0f172a&bgcolor=ffffff`;
width: 200,
margin: 1,
color: { dark: "#0f172a", light: "#ffffff" }
});
} catch (error) { } catch (error) {
errorMessage.value = error.message; errorMessage.value = error.message;