diff --git a/data/data.json b/data/data.json index 8f781a4..b1042a5 100644 --- a/data/data.json +++ b/data/data.json @@ -1,4 +1,403 @@ { - "version": "1.0.0", - "note": "Fichier de persistance locale pour le portail Home. Synchronisé avec src/data/initial-data.ts." -} + "version": "3.0.0", + "dockTileIds": [ + "nextcloud", + "dockge", + "gitea", + "jellyfin", + "open-webui", + "vaultwarden" + ], + "quickNote": "Bienvenue sur Home OS (*.lafabriquedesgenies.be). Cliquez sur « Éditer » pour ajuster n'importe quelle URL ou sous-domaine à la volée.", + "categories": [ + { + "id": "infrastructure", + "name": "Infrastructure & Serveur", + "subtitle": "Administration système, conteneurs Docker, sauvegardes & réseau", + "icon": "Server", + "tabs": [ + { + "id": "infra-main", + "label": "Services Serveur", + "tiles": [ + { + "id": "dockge", + "title": "Dockge", + "description": "Gestionnaire Docker Compose", + "url": "https://dockge.lafabriquedesgenies.be", + "icon": "Terminal", + "size": "2x1", + "badge": "Docker", + "accentColor": "emerald", + "openBehavior": "external" + }, + { + "id": "nginx-proxy-manager", + "title": "Nginx Proxy Manager", + "description": "Reverse Proxy & Certificats SSL", + "url": "https://proxy.lafabriquedesgenies.be", + "icon": "Shield", + "size": "1x1", + "badge": "Proxy SSL", + "accentColor": "blue", + "openBehavior": "external" + }, + { + "id": "duplicati", + "title": "Duplicati", + "description": "Sauvegardes du serveur", + "url": "https://backup.lafabriquedesgenies.be", + "icon": "Archive", + "size": "1x1", + "badge": "Backup", + "accentColor": "sky", + "openBehavior": "external" + }, + { + "id": "speedtest", + "title": "Speedtest", + "description": "Surveillance débit réseau", + "url": "https://speedtest.lafabriquedesgenies.be", + "icon": "Activity", + "size": "1x1", + "badge": "Réseau", + "accentColor": "cyan", + "openBehavior": "external" + }, + { + "id": "adminer", + "title": "Adminer", + "description": "Gestionnaire bases SQL", + "url": "https://db.lafabriquedesgenies.be", + "icon": "Database", + "size": "1x1", + "badge": "SQL", + "accentColor": "amber", + "openBehavior": "external" + }, + { + "id": "adguard-home", + "title": "AdGuard Home", + "description": "Filtrage DNS & Antipub", + "url": "https://adguard.lafabriquedesgenies.be", + "icon": "ShieldCheck", + "size": "1x1", + "badge": "DNS", + "accentColor": "teal", + "openBehavior": "external" + }, + { + "id": "watchtower", + "title": "Watchtower", + "description": "Mises à jour automatiques", + "url": "https://dockge.lafabriquedesgenies.be", + "icon": "RefreshCw", + "size": "1x1", + "badge": "Daemon Auto", + "accentColor": "violet", + "openBehavior": "external" + } + ] + } + ] + }, + { + "id": "private-security", + "name": "Privé & Sécurité", + "subtitle": "Cloud personnel, mots de passe, documents, notes & domotique", + "icon": "ShieldCheck", + "tabs": [ + { + "id": "private-main", + "label": "Espace Privé", + "tiles": [ + { + "id": "nextcloud", + "title": "Nextcloud", + "description": "Fichiers & Drive Cloud", + "url": "https://nc.lafabriquedesgenies.be", + "icon": "Cloud", + "size": "2x1", + "badge": "Cloud", + "accentColor": "sky", + "openBehavior": "external" + }, + { + "id": "vaultwarden", + "title": "Vaultwarden", + "description": "Gestionnaire de mots de passe", + "url": "https://vault.lafabriquedesgenies.be", + "icon": "Key", + "size": "1x1", + "badge": "Coffre-fort", + "accentColor": "amber", + "openBehavior": "external" + }, + { + "id": "paperless-ngx", + "title": "Paperless-ngx", + "description": "GED & Archivage intelligent", + "url": "https://paperless.lafabriquedesgenies.be", + "icon": "FileText", + "size": "1x1", + "badge": "GED", + "accentColor": "emerald", + "openBehavior": "external" + }, + { + "id": "wikijs", + "title": "Wiki.js", + "description": "Base de connaissances & notes", + "url": "https://wiki.lafabriquedesgenies.be", + "icon": "BookOpen", + "size": "1x1", + "badge": "Wiki", + "accentColor": "indigo", + "openBehavior": "external" + }, + { + "id": "stirling-pdf", + "title": "Stirling PDF", + "description": "Boîte à outils PDF complète", + "url": "https://pdf.lafabriquedesgenies.be", + "icon": "FileCode", + "size": "1x1", + "badge": "PDF", + "accentColor": "rose", + "openBehavior": "external" + }, + { + "id": "home-assistant", + "title": "Home Assistant", + "description": "Domotique de la maison", + "url": "https://hass.lafabriquedesgenies.be", + "icon": "Home", + "size": "2x1", + "badge": "Domotique", + "accentColor": "cyan", + "openBehavior": "external" + } + ] + } + ] + }, + { + "id": "multimedia", + "name": "Multimédia & Médias", + "subtitle": "Streaming vidéo, livres audio, musique & téléchargements", + "icon": "Film", + "tabs": [ + { + "id": "media-main", + "label": "Streaming & Téléchargements", + "tiles": [ + { + "id": "jellyfin", + "title": "Jellyfin", + "description": "Streaming Films & Séries", + "url": "https://jellyfin.lafabriquedesgenies.be", + "icon": "Film", + "size": "2x1", + "badge": "Streaming", + "accentColor": "purple", + "openBehavior": "external" + }, + { + "id": "audiobookshelf", + "title": "Audiobookshelf", + "description": "Livres audio & Podcasts", + "url": "https://audiobookshelf.lafabriquedesgenies.be", + "icon": "Headphones", + "size": "1x1", + "badge": "Audio", + "accentColor": "amber", + "openBehavior": "external" + }, + { + "id": "sonarr", + "title": "Sonarr", + "description": "Suivi & gestion des séries", + "url": "https://sonarr.lafabriquedesgenies.be", + "icon": "Tv", + "size": "1x1", + "badge": "Séries", + "accentColor": "sky", + "openBehavior": "external" + }, + { + "id": "radarr", + "title": "Radarr", + "description": "Suivi & gestion des films", + "url": "https://radarr.lafabriquedesgenies.be", + "icon": "Video", + "size": "1x1", + "badge": "Films", + "accentColor": "amber", + "openBehavior": "external" + }, + { + "id": "qbittorrent", + "title": "qBittorrent", + "description": "Téléchargements VPN (Gluetun)", + "url": "https://torrent.lafabriquedesgenies.be", + "icon": "DownloadCloud", + "size": "1x1", + "badge": "VPN Gluetun", + "accentColor": "blue", + "openBehavior": "external" + }, + { + "id": "deemix", + "title": "Deemix", + "description": "Téléchargement Musique", + "url": "https://deemix.lafabriquedesgenies.be", + "icon": "Music", + "size": "1x1", + "badge": "Musique", + "accentColor": "pink", + "openBehavior": "external" + }, + { + "id": "sabnzbd", + "title": "SABnzbd", + "description": "Client Usenet", + "url": "https://sabnzbd.lafabriquedesgenies.be", + "icon": "HardDriveDownload", + "size": "1x1", + "badge": "Usenet", + "accentColor": "emerald", + "openBehavior": "external" + } + ] + } + ] + }, + { + "id": "projects-work", + "name": "Projets & Travail", + "subtitle": "Forge Git, applications La Fabrique des Génies & outils métiers", + "icon": "Briefcase", + "tabs": [ + { + "id": "work-main", + "label": "Applications & Projets", + "tiles": [ + { + "id": "gitea", + "title": "Gitea", + "description": "Dépôts de code Git", + "url": "https://git.lafabriquedesgenies.be", + "icon": "GitBranch", + "size": "2x1", + "badge": "Git", + "accentColor": "emerald", + "openBehavior": "external" + }, + { + "id": "wordpress-lafabrique", + "title": "WordPress La Fabrique", + "description": "Site officiel La Fabrique", + "url": "https://lafabriquedesgenies.be", + "icon": "Globe", + "size": "1x1", + "badge": "Site Web", + "accentColor": "blue", + "openBehavior": "external" + }, + { + "id": "teacher-app", + "title": "Teacher App", + "description": "Plateforme pédagogique", + "url": "https://teacher.lafabriquedesgenies.be", + "icon": "GraduationCap", + "size": "1x1", + "badge": "Pédagogie", + "accentColor": "indigo", + "openBehavior": "external" + }, + { + "id": "creaprof", + "title": "CreaProf", + "description": "Outil création enseignant", + "url": "https://creaprof.lafabriquedesgenies.be", + "icon": "PenTool", + "size": "1x1", + "badge": "Création", + "accentColor": "violet", + "openBehavior": "external" + }, + { + "id": "ubicraft", + "title": "Ubicraft", + "description": "Application métier", + "url": "https://ubicraft.lafabriquedesgenies.be", + "icon": "LayoutGrid", + "size": "1x1", + "badge": "Métier", + "accentColor": "cyan", + "openBehavior": "external" + }, + { + "id": "lma-v2", + "title": "LMA V2", + "description": "Plateforme Last Minute Activities", + "url": "https://lma.lafabriquedesgenies.be", + "icon": "Briefcase", + "size": "1x1", + "badge": "LMA", + "accentColor": "amber", + "openBehavior": "external" + }, + { + "id": "coo-app", + "title": "Coo App", + "description": "Application de gestion", + "url": "https://coo.lafabriquedesgenies.be", + "icon": "Layers", + "size": "1x1", + "badge": "Gestion", + "accentColor": "teal", + "openBehavior": "external" + } + ] + } + ] + }, + { + "id": "ai", + "name": "Intelligence Artificielle", + "subtitle": "Modèles LLM auto-hébergés & Assistant Google Gemini intégré", + "icon": "Sparkles", + "tabs": [ + { + "id": "ai-main", + "label": "Assistants IA", + "tiles": [ + { + "id": "open-webui", + "title": "Open WebUI", + "description": "Interface modèles LLM", + "url": "https://ai.lafabriquedesgenies.be", + "icon": "Bot", + "size": "2x1", + "badge": "LLM", + "accentColor": "emerald", + "openBehavior": "external" + }, + { + "id": "google-gemini", + "title": "Google Gemini", + "description": "Assistant IA intégré", + "url": "#gemini", + "icon": "Sparkles", + "size": "2x1", + "badge": "Omnibox IA", + "accentColor": "blue", + "openBehavior": "external" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/app/api/dashboard/route.ts b/src/app/api/dashboard/route.ts index 4fd36e8..1b8bd18 100644 --- a/src/app/api/dashboard/route.ts +++ b/src/app/api/dashboard/route.ts @@ -12,7 +12,11 @@ async function readOrInitData(): Promise { await fs.mkdir(DATA_DIR, { recursive: true }); const raw = await fs.readFile(DATA_FILE, "utf-8"); const parsed = JSON.parse(raw); - if (parsed && Array.isArray(parsed.categories)) { + if ( + parsed && + Array.isArray(parsed.categories) && + parsed.version === initialDashboardData.version + ) { return parsed as DashboardData; } await fs.writeFile( @@ -46,9 +50,13 @@ export async function POST(request: Request) { { status: 400 } ); } + const payload: DashboardData = { + ...body, + version: initialDashboardData.version, + }; await fs.mkdir(DATA_DIR, { recursive: true }); - await fs.writeFile(DATA_FILE, JSON.stringify(body, null, 2), "utf-8"); - return NextResponse.json({ ok: true, data: body }); + await fs.writeFile(DATA_FILE, JSON.stringify(payload, null, 2), "utf-8"); + return NextResponse.json({ ok: true, data: payload }); } catch (error) { return NextResponse.json( { error: "Impossible de sauvegarder data.json", details: String(error) }, diff --git a/src/components/dashboard/bento-grid.tsx b/src/components/dashboard/bento-grid.tsx index ed7bacf..ab43790 100644 --- a/src/components/dashboard/bento-grid.tsx +++ b/src/components/dashboard/bento-grid.tsx @@ -12,12 +12,16 @@ import { Layers, ArrowLeft, ArrowRight, + Sparkles, } from "lucide-react"; interface BentoGridProps { tiles: ShortcutTile[]; isEditMode: boolean; - onLaunchTile: (tile: ShortcutTile, forceMode?: "workspace" | "popup" | "external") => void; + onLaunchTile: ( + tile: ShortcutTile, + forceMode?: "workspace" | "popup" | "external" + ) => void; onAddTile: () => void; onEditTile: (tile: ShortcutTile) => void; onDeleteTile: (tileId: string) => void; @@ -91,6 +95,7 @@ const accentStyles: Record< }; function formatHostname(url: string) { + if (!url || url === "#gemini") return "Assistant intégré"; try { const parsed = new URL(url); return parsed.host; @@ -120,19 +125,22 @@ export function BentoGrid({ const style = accentStyles[tile.accentColor || "indigo"] || accentStyles.indigo; - const behavior = tile.openBehavior || "workspace"; + const behavior = tile.openBehavior || "external"; + const isGeminiWidget = tile.url === "#gemini"; return (
{ - if (!isEditMode) { + if (isEditMode) { + onEditTile(tile); + } else { onLaunchTile(tile); } }} className={`group relative flex flex-col justify-between overflow-hidden rounded-2xl border border-white/10 bg-zinc-900/60 p-4 sm:p-5 backdrop-blur-md transition-all duration-200 select-none ${spanClass} ${ isEditMode - ? "border-dashed border-amber-500/40 bg-zinc-900/80" + ? "cursor-pointer border-dashed border-amber-500/40 bg-zinc-900/80 hover:border-amber-400" : `cursor-pointer ${style.borderHover} hover:-translate-y-0.5 hover:bg-zinc-900/90 hover:shadow-xl hover:shadow-black/40` }`} > @@ -175,10 +183,11 @@ export function BentoGrid({ - - -
+ {isGeminiWidget ? ( + + + Ouvrir IA + + ) : ( + /* Quick override buttons on hover */ +
+ + + +
+ )} )} @@ -248,7 +268,7 @@ export function BentoGrid({

{tile.title}

- + {formatHostname(tile.url)} diff --git a/src/components/dashboard/dashboard-shell.tsx b/src/components/dashboard/dashboard-shell.tsx index c6f16b2..1730f8c 100644 --- a/src/components/dashboard/dashboard-shell.tsx +++ b/src/components/dashboard/dashboard-shell.tsx @@ -32,6 +32,7 @@ export function DashboardShell({ initialData }: DashboardShellProps) { >({}); const [searchQuery, setSearchQuery] = useState(""); const [isEditMode, setIsEditMode] = useState(false); + const [geminiTriggerCount, setGeminiTriggerCount] = useState(0); // OS Window Manager state ("bento" or window.id) const [openWindows, setOpenWindows] = useState([]); @@ -122,7 +123,13 @@ export function DashboardShell({ initialData }: DashboardShellProps) { tile: ShortcutTile, forceMode?: "workspace" | "popup" | "external" ) => { - const mode = forceMode || tile.openBehavior || "workspace"; + if (tile.url === "#gemini") { + setActiveViewId("bento"); + setGeminiTriggerCount((c) => c + 1); + return; + } + + const mode = forceMode || tile.openBehavior || "external"; if (mode === "external") { window.open(tile.url, "_blank", "noopener,noreferrer"); @@ -134,7 +141,7 @@ export function DashboardShell({ initialData }: DashboardShellProps) { return; } - // Default: "workspace" -> Open or focus an in-OS tab + // "workspace" -> Open or focus an in-OS tab const existing = openWindows.find((w) => w.tileId === tile.id); if (existing) { setActiveViewId(existing.id); @@ -398,6 +405,7 @@ export function DashboardShell({ initialData }: DashboardShellProps) { searchQuery={searchQuery} onSearchChange={setSearchQuery} onOpenCustomUrl={handleOpenCustomUrl} + geminiTriggerCount={geminiTriggerCount} />
diff --git a/src/components/dashboard/header-omnibox.tsx b/src/components/dashboard/header-omnibox.tsx index 57e2597..87bdaf1 100644 --- a/src/components/dashboard/header-omnibox.tsx +++ b/src/components/dashboard/header-omnibox.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useRef, useEffect } from "react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { @@ -18,6 +18,7 @@ interface HeaderOmniboxProps { searchQuery: string; onSearchChange: (q: string) => void; onOpenCustomUrl: (url: string, title?: string) => void; + geminiTriggerCount?: number; } export function HeaderOmnibox({ @@ -26,12 +27,23 @@ export function HeaderOmnibox({ searchQuery, onSearchChange, onOpenCustomUrl, + geminiTriggerCount = 0, }: HeaderOmniboxProps) { const [mode, setMode] = useState<"search" | "gemini" | "url">("search"); const [geminiPrompt, setGeminiPrompt] = useState(""); const [geminiLoading, setGeminiLoading] = useState(false); const [geminiAnswer, setGeminiAnswer] = useState(null); const [customUrlInput, setCustomUrlInput] = useState(""); + const geminiInputRef = useRef(null); + + useEffect(() => { + if (geminiTriggerCount > 0) { + setMode("gemini"); + setTimeout(() => { + geminiInputRef.current?.focus(); + }, 50); + } + }, [geminiTriggerCount]); const handleGeminiSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -65,7 +77,6 @@ export function HeaderOmnibox({ if (raw.includes(".") && !raw.includes(" ")) { finalUrl = `https://${raw}`; } else { - // DuckDuckGo / Wikipedia embed-friendly search or direct URL finalUrl = `https://fr.wikipedia.org/w/index.php?search=${encodeURIComponent(raw)}`; } } @@ -122,7 +133,10 @@ export function HeaderOmnibox({