feat: replace initial-data with full *.lafabriquedesgenies.be HTTPS services & direct URL editor

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
klemensandCursor committed 2026-10-22 00:23:54 +02:00
1 parent 91a57285b9
commit 205337f5c8
8 files changed
+844 -296

No files matched your search

+401 -2
View File
@@ -1,4 +1,403 @@
{ {
"version": "1.0.0", "version": "3.0.0",
"note": "Fichier de persistance locale pour le portail Home. Synchronisé avec src/data/initial-data.ts." "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"
}
]
}
]
}
]
} }
+11 -3
View File
@@ -12,7 +12,11 @@ async function readOrInitData(): Promise<DashboardData> {
await fs.mkdir(DATA_DIR, { recursive: true }); await fs.mkdir(DATA_DIR, { recursive: true });
const raw = await fs.readFile(DATA_FILE, "utf-8"); const raw = await fs.readFile(DATA_FILE, "utf-8");
const parsed = JSON.parse(raw); 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; return parsed as DashboardData;
} }
await fs.writeFile( await fs.writeFile(
@@ -46,9 +50,13 @@ export async function POST(request: Request) {
{ status: 400 } { status: 400 }
); );
} }
const payload: DashboardData = {
...body,
version: initialDashboardData.version,
};
await fs.mkdir(DATA_DIR, { recursive: true }); await fs.mkdir(DATA_DIR, { recursive: true });
await fs.writeFile(DATA_FILE, JSON.stringify(body, null, 2), "utf-8"); await fs.writeFile(DATA_FILE, JSON.stringify(payload, null, 2), "utf-8");
return NextResponse.json({ ok: true, data: body }); return NextResponse.json({ ok: true, data: payload });
} catch (error) { } catch (error) {
return NextResponse.json( return NextResponse.json(
{ error: "Impossible de sauvegarder data.json", details: String(error) }, { error: "Impossible de sauvegarder data.json", details: String(error) },
+38 -18
View File
@@ -12,12 +12,16 @@ import {
Layers, Layers,
ArrowLeft, ArrowLeft,
ArrowRight, ArrowRight,
Sparkles,
} from "lucide-react"; } from "lucide-react";
interface BentoGridProps { interface BentoGridProps {
tiles: ShortcutTile[]; tiles: ShortcutTile[];
isEditMode: boolean; isEditMode: boolean;
onLaunchTile: (tile: ShortcutTile, forceMode?: "workspace" | "popup" | "external") => void; onLaunchTile: (
tile: ShortcutTile,
forceMode?: "workspace" | "popup" | "external"
) => void;
onAddTile: () => void; onAddTile: () => void;
onEditTile: (tile: ShortcutTile) => void; onEditTile: (tile: ShortcutTile) => void;
onDeleteTile: (tileId: string) => void; onDeleteTile: (tileId: string) => void;
@@ -91,6 +95,7 @@ const accentStyles: Record<
}; };
function formatHostname(url: string) { function formatHostname(url: string) {
if (!url || url === "#gemini") return "Assistant intégré";
try { try {
const parsed = new URL(url); const parsed = new URL(url);
return parsed.host; return parsed.host;
@@ -120,19 +125,22 @@ export function BentoGrid({
const style = const style =
accentStyles[tile.accentColor || "indigo"] || accentStyles.indigo; accentStyles[tile.accentColor || "indigo"] || accentStyles.indigo;
const behavior = tile.openBehavior || "workspace"; const behavior = tile.openBehavior || "external";
const isGeminiWidget = tile.url === "#gemini";
return ( return (
<div <div
key={tile.id} key={tile.id}
onClick={() => { onClick={() => {
if (!isEditMode) { if (isEditMode) {
onEditTile(tile);
} else {
onLaunchTile(tile); 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} ${ 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 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` : `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({
<button <button
type="button" type="button"
onClick={() => onEditTile(tile)} onClick={() => onEditTile(tile)}
title="Modifier la tuile" title="Modifier l'URL ou les détails"
className="rounded p-1 text-amber-300 hover:bg-amber-500/20" className="flex items-center gap-1 rounded bg-amber-500/15 px-1.5 py-1 text-[10px] font-medium text-amber-300 hover:bg-amber-500/25"
> >
<Pencil className="size-3.5" /> <Pencil className="size-3" />
<span>URL</span>
</button> </button>
<button <button
type="button" type="button"
@@ -203,8 +212,26 @@ export function BentoGrid({
</Badge> </Badge>
)} )}
{/* Quick override buttons on hover */} {isGeminiWidget ? (
<span className="flex items-center gap-1 rounded-lg border border-blue-500/30 bg-blue-500/15 px-2 py-1 text-[10px] font-medium text-blue-300">
<Sparkles className="size-3" />
<span>Ouvrir IA</span>
</span>
) : (
/* Quick override buttons on hover */
<div className="flex items-center gap-0.5 rounded-lg border border-white/8 bg-zinc-950/70 p-0.5 opacity-80 sm:opacity-0 transition-opacity group-hover:opacity-100"> <div className="flex items-center gap-0.5 rounded-lg border border-white/8 bg-zinc-950/70 p-0.5 opacity-80 sm:opacity-0 transition-opacity group-hover:opacity-100">
<button
type="button"
onClick={() => onLaunchTile(tile, "external")}
title="Ouvrir dans un nouvel onglet"
className={`rounded p-1 transition-colors ${
behavior === "external"
? "text-emerald-400 bg-emerald-500/15"
: "text-zinc-400 hover:text-white"
}`}
>
<ExternalLink className="size-3" />
</button>
<button <button
type="button" type="button"
onClick={() => onLaunchTile(tile, "workspace")} onClick={() => onLaunchTile(tile, "workspace")}
@@ -220,7 +247,7 @@ export function BentoGrid({
<button <button
type="button" type="button"
onClick={() => onLaunchTile(tile, "popup")} onClick={() => onLaunchTile(tile, "popup")}
title="Ouvrir en fenêtre App Chromium (idéal Google Docs/Drive)" title="Ouvrir en fenêtre App dédiée"
className={`rounded p-1 transition-colors ${ className={`rounded p-1 transition-colors ${
behavior === "popup" behavior === "popup"
? "text-cyan-400 bg-cyan-500/15" ? "text-cyan-400 bg-cyan-500/15"
@@ -229,15 +256,8 @@ export function BentoGrid({
> >
<AppWindow className="size-3" /> <AppWindow className="size-3" />
</button> </button>
<button
type="button"
onClick={() => onLaunchTile(tile, "external")}
title="Ouvrir dans un nouvel onglet navigateur"
className="rounded p-1 text-zinc-400 hover:text-white"
>
<ExternalLink className="size-3" />
</button>
</div> </div>
)}
</div> </div>
)} )}
</div> </div>
@@ -248,7 +268,7 @@ export function BentoGrid({
<h3 className="text-base font-semibold tracking-tight text-zinc-100 group-hover:text-white truncate"> <h3 className="text-base font-semibold tracking-tight text-zinc-100 group-hover:text-white truncate">
{tile.title} {tile.title}
</h3> </h3>
<span className="font-mono text-[10px] text-zinc-500 truncate max-w-[130px] shrink-0"> <span className="font-mono text-[10px] text-zinc-500 truncate max-w-[175px] shrink-0">
{formatHostname(tile.url)} {formatHostname(tile.url)}
</span> </span>
</div> </div>
+10 -2
View File
@@ -32,6 +32,7 @@ export function DashboardShell({ initialData }: DashboardShellProps) {
>({}); >({});
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [isEditMode, setIsEditMode] = useState(false); const [isEditMode, setIsEditMode] = useState(false);
const [geminiTriggerCount, setGeminiTriggerCount] = useState(0);
// OS Window Manager state ("bento" or window.id) // OS Window Manager state ("bento" or window.id)
const [openWindows, setOpenWindows] = useState<OpenWindowTab[]>([]); const [openWindows, setOpenWindows] = useState<OpenWindowTab[]>([]);
@@ -122,7 +123,13 @@ export function DashboardShell({ initialData }: DashboardShellProps) {
tile: ShortcutTile, tile: ShortcutTile,
forceMode?: "workspace" | "popup" | "external" 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") { if (mode === "external") {
window.open(tile.url, "_blank", "noopener,noreferrer"); window.open(tile.url, "_blank", "noopener,noreferrer");
@@ -134,7 +141,7 @@ export function DashboardShell({ initialData }: DashboardShellProps) {
return; 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); const existing = openWindows.find((w) => w.tileId === tile.id);
if (existing) { if (existing) {
setActiveViewId(existing.id); setActiveViewId(existing.id);
@@ -398,6 +405,7 @@ export function DashboardShell({ initialData }: DashboardShellProps) {
searchQuery={searchQuery} searchQuery={searchQuery}
onSearchChange={setSearchQuery} onSearchChange={setSearchQuery}
onOpenCustomUrl={handleOpenCustomUrl} onOpenCustomUrl={handleOpenCustomUrl}
geminiTriggerCount={geminiTriggerCount}
/> />
<main className="flex-1 overflow-y-auto px-4 py-5 pb-24 sm:px-6 md:pb-8"> <main className="flex-1 overflow-y-auto px-4 py-5 pb-24 sm:px-6 md:pb-8">
+21 -6
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState } from "react"; import { useState, useRef, useEffect } from "react";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -18,6 +18,7 @@ interface HeaderOmniboxProps {
searchQuery: string; searchQuery: string;
onSearchChange: (q: string) => void; onSearchChange: (q: string) => void;
onOpenCustomUrl: (url: string, title?: string) => void; onOpenCustomUrl: (url: string, title?: string) => void;
geminiTriggerCount?: number;
} }
export function HeaderOmnibox({ export function HeaderOmnibox({
@@ -26,12 +27,23 @@ export function HeaderOmnibox({
searchQuery, searchQuery,
onSearchChange, onSearchChange,
onOpenCustomUrl, onOpenCustomUrl,
geminiTriggerCount = 0,
}: HeaderOmniboxProps) { }: HeaderOmniboxProps) {
const [mode, setMode] = useState<"search" | "gemini" | "url">("search"); const [mode, setMode] = useState<"search" | "gemini" | "url">("search");
const [geminiPrompt, setGeminiPrompt] = useState(""); const [geminiPrompt, setGeminiPrompt] = useState("");
const [geminiLoading, setGeminiLoading] = useState(false); const [geminiLoading, setGeminiLoading] = useState(false);
const [geminiAnswer, setGeminiAnswer] = useState<string | null>(null); const [geminiAnswer, setGeminiAnswer] = useState<string | null>(null);
const [customUrlInput, setCustomUrlInput] = useState(""); const [customUrlInput, setCustomUrlInput] = useState("");
const geminiInputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (geminiTriggerCount > 0) {
setMode("gemini");
setTimeout(() => {
geminiInputRef.current?.focus();
}, 50);
}
}, [geminiTriggerCount]);
const handleGeminiSubmit = async (e: React.FormEvent) => { const handleGeminiSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -65,7 +77,6 @@ export function HeaderOmnibox({
if (raw.includes(".") && !raw.includes(" ")) { if (raw.includes(".") && !raw.includes(" ")) {
finalUrl = `https://${raw}`; finalUrl = `https://${raw}`;
} else { } else {
// DuckDuckGo / Wikipedia embed-friendly search or direct URL
finalUrl = `https://fr.wikipedia.org/w/index.php?search=${encodeURIComponent(raw)}`; finalUrl = `https://fr.wikipedia.org/w/index.php?search=${encodeURIComponent(raw)}`;
} }
} }
@@ -122,7 +133,10 @@ export function HeaderOmnibox({
<button <button
type="button" type="button"
onClick={() => setMode("gemini")} onClick={() => {
setMode("gemini");
setTimeout(() => geminiInputRef.current?.focus(), 50);
}}
className={`flex items-center gap-1 rounded-lg px-2.5 py-1.5 text-xs font-medium transition-all ${ className={`flex items-center gap-1 rounded-lg px-2.5 py-1.5 text-xs font-medium transition-all ${
mode === "gemini" mode === "gemini"
? "bg-gradient-to-r from-indigo-500 to-blue-500 text-white shadow-xs" ? "bg-gradient-to-r from-indigo-500 to-blue-500 text-white shadow-xs"
@@ -138,7 +152,7 @@ export function HeaderOmnibox({
<Input <Input
value={searchQuery} value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)} onChange={(e) => onSearchChange(e.target.value)}
placeholder="Rechercher Nextcloud, Docs, Drive, Gitea..." placeholder="Rechercher un service (*.lafabriquedesgenies.be)..."
className="h-8 border-0 bg-transparent text-sm text-zinc-100 placeholder:text-zinc-500 focus-visible:ring-0 dark:bg-transparent" className="h-8 border-0 bg-transparent text-sm text-zinc-100 placeholder:text-zinc-500 focus-visible:ring-0 dark:bg-transparent"
/> />
)} )}
@@ -151,7 +165,7 @@ export function HeaderOmnibox({
<Input <Input
value={customUrlInput} value={customUrlInput}
onChange={(e) => setCustomUrlInput(e.target.value)} onChange={(e) => setCustomUrlInput(e.target.value)}
placeholder="Entrer une URL (ex: cloud.lafabriquedesgenies.be) à ouvrir en onglet..." placeholder="Entrer une URL (ex: nc.lafabriquedesgenies.be) à ouvrir..."
className="h-8 border-0 bg-transparent text-sm text-zinc-100 placeholder:text-zinc-500 focus-visible:ring-0 dark:bg-transparent" className="h-8 border-0 bg-transparent text-sm text-zinc-100 placeholder:text-zinc-500 focus-visible:ring-0 dark:bg-transparent"
/> />
<Button <Button
@@ -170,9 +184,10 @@ export function HeaderOmnibox({
className="flex flex-1 items-center" className="flex flex-1 items-center"
> >
<Input <Input
ref={geminiInputRef}
value={geminiPrompt} value={geminiPrompt}
onChange={(e) => setGeminiPrompt(e.target.value)} onChange={(e) => setGeminiPrompt(e.target.value)}
placeholder="Demander à Gemini (résumer un cours, traduire, formule Excel)..." placeholder="Poser une question à Google Gemini, traduire, générer une idée..."
className="h-8 border-0 bg-transparent text-sm text-zinc-100 placeholder:text-zinc-500 focus-visible:ring-0 dark:bg-transparent" className="h-8 border-0 bg-transparent text-sm text-zinc-100 placeholder:text-zinc-500 focus-visible:ring-0 dark:bg-transparent"
/> />
<Button <Button
+26
View File
@@ -5,12 +5,21 @@ import {
Wrench, Wrench,
Sparkles, Sparkles,
Container, Container,
Shield,
ShieldCheck, ShieldCheck,
Archive,
Activity, Activity,
Cloud, Cloud,
Key,
KeyRound, KeyRound,
Radar, Radar,
Film, Film,
Headphones,
Tv,
Video,
DownloadCloud,
Music,
HardDriveDownload,
Image, Image,
RefreshCw, RefreshCw,
Network, Network,
@@ -20,6 +29,8 @@ import {
Bookmark, Bookmark,
Wallet, Wallet,
FileText, FileText,
FileCode,
Home,
GitBranch, GitBranch,
Code2, Code2,
BookOpen, BookOpen,
@@ -36,11 +47,13 @@ import {
Folder, Folder,
Database, Database,
Layers, Layers,
LayoutGrid,
ExternalLink, ExternalLink,
HardDrive, HardDrive,
Calendar, Calendar,
GraduationCap, GraduationCap,
AppWindow, AppWindow,
Lock,
type LucideIcon, type LucideIcon,
} from "lucide-react"; } from "lucide-react";
@@ -51,12 +64,21 @@ export const iconRegistry: Record<string, LucideIcon> = {
Wrench, Wrench,
Sparkles, Sparkles,
Container, Container,
Shield,
ShieldCheck, ShieldCheck,
Archive,
Activity, Activity,
Cloud, Cloud,
Key,
KeyRound, KeyRound,
Radar, Radar,
Film, Film,
Headphones,
Tv,
Video,
DownloadCloud,
Music,
HardDriveDownload,
Image, Image,
RefreshCw, RefreshCw,
Network, Network,
@@ -66,6 +88,8 @@ export const iconRegistry: Record<string, LucideIcon> = {
Bookmark, Bookmark,
Wallet, Wallet,
FileText, FileText,
FileCode,
Home,
GitBranch, GitBranch,
Code2, Code2,
BookOpen, BookOpen,
@@ -82,11 +106,13 @@ export const iconRegistry: Record<string, LucideIcon> = {
Folder, Folder,
Database, Database,
Layers, Layers,
LayoutGrid,
ExternalLink, ExternalLink,
HardDrive, HardDrive,
Calendar, Calendar,
GraduationCap, GraduationCap,
AppWindow, AppWindow,
Lock,
}; };
export const availableIconNames = Object.keys(iconRegistry); export const availableIconNames = Object.keys(iconRegistry);
@@ -53,7 +53,7 @@ export function TileEditorModal({
initialTile?.accentColor || "indigo" initialTile?.accentColor || "indigo"
); );
const [openBehavior, setOpenBehavior] = useState<OpenBehavior>( const [openBehavior, setOpenBehavior] = useState<OpenBehavior>(
initialTile?.openBehavior || "workspace" initialTile?.openBehavior || "external"
); );
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
@@ -116,15 +116,16 @@ export function TileEditorModal({
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-xs font-medium text-zinc-300"> <label className="text-xs font-semibold text-amber-300">
Adresse URL complète Adresse URL complète (Sous-domaine HTTPS)
</label> </label>
<Input <Input
value={url} value={url}
onChange={(e) => setUrl(e.target.value)} onChange={(e) => setUrl(e.target.value)}
placeholder="https://cloud.lafabriquedesgenies.be" placeholder="https://service.lafabriquedesgenies.be"
required required
className="border-white/10 bg-zinc-900 font-mono text-xs text-zinc-100" autoFocus={Boolean(initialTile)}
className="border-amber-500/40 bg-zinc-900 font-mono text-xs text-zinc-100 focus-visible:ring-amber-500/30"
/> />
</div> </div>
+331 -260
View File
@@ -1,295 +1,366 @@
import { DashboardData } from "@/types/dashboard"; import { DashboardData } from "@/types/dashboard";
export const initialDashboardData: DashboardData = { export const initialDashboardData: DashboardData = {
version: "2.0.0", version: "3.0.0",
dockTileIds: [ dockTileIds: [
"nextcloud-files", "nextcloud",
"nextcloud-office",
"google-docs",
"google-drive",
"gitea-server",
"dockge", "dockge",
"gitea",
"jellyfin",
"open-webui",
"vaultwarden",
], ],
quickNote: quickNote:
"Bienvenue sur Home OS. Utilisez F11 sur CachyOS (Chromium) ou ajoutez l'application à l'écran d'accueil sur votre Galaxy S20 FE.", "Bienvenue sur Home OS (*.lafabriquedesgenies.be). Cliquez sur « Éditer » pour ajuster n'importe quelle URL ou sous-domaine à la volée.",
categories: [ categories: [
{ {
id: "work", id: "infrastructure",
name: "Bureautique & École", name: "Infrastructure & Serveur",
subtitle: "Nextcloud Office, Google Docs, Drive & Cours", subtitle: "Administration système, conteneurs Docker, sauvegardes & réseau",
icon: "Briefcase",
tabs: [
{
id: "cloud-office",
label: "Documents & Suites",
tiles: [
{
id: "nextcloud-files",
title: "Nextcloud Hub",
description: "Tous mes fichiers, dossiers personnels et synchronisation",
url: "https://cloud.lafabriquedesgenies.be",
icon: "Cloud",
size: "2x1",
badge: "Fichiers",
accentColor: "sky",
openBehavior: "workspace",
},
{
id: "nextcloud-office",
title: "Nextcloud Office",
description: "Traitement de texte (Word), Tableur (Excel) & Présentations",
url: "https://cloud.lafabriquedesgenies.be/apps/richdocuments",
icon: "FileSpreadsheet",
size: "2x1",
badge: "Word / Excel",
accentColor: "emerald",
openBehavior: "workspace",
},
{
id: "google-docs",
title: "Google Docs",
description: "Travaux d'école, rapports et rédaction collaborative",
url: "https://docs.google.com",
icon: "FileText",
size: "2x1",
badge: "École • Docs",
accentColor: "blue",
openBehavior: "popup",
},
{
id: "google-drive",
title: "Google Drive",
description: "Stockage scolaire, cours partagés et Google Sheets",
url: "https://drive.google.com",
icon: "HardDrive",
size: "1x1",
badge: "École • Drive",
accentColor: "amber",
openBehavior: "popup",
},
{
id: "nextcloud-notes",
title: "Notes & Aide-mémoire",
description: "Prise de notes rapide en Markdown sur Nextcloud",
url: "https://cloud.lafabriquedesgenies.be/apps/notes",
icon: "NotebookPen",
size: "1x1",
badge: "Markdown",
accentColor: "violet",
openBehavior: "workspace",
},
],
},
{
id: "dev-git",
label: "Code & Projets",
tiles: [
{
id: "gitea-server",
title: "Gitea — La Fabrique",
description: "Dépôts Git personnels (klemens/home) et CI",
url: "https://git.lafabriquedesgenies.be/klemens/home",
icon: "GitBranch",
size: "2x1",
badge: "Git Serveur",
accentColor: "emerald",
openBehavior: "workspace",
},
{
id: "github",
title: "GitHub",
description: "Projets open-source et dépôts externes",
url: "https://github.com",
icon: "Code2",
size: "1x1",
badge: "Externe",
accentColor: "slate",
openBehavior: "popup",
},
{
id: "excalidraw",
title: "Excalidraw",
description: "Schémas d'architecture, croquis et diagrammes de cours",
url: "https://excalidraw.com",
icon: "PenTool",
size: "1x1",
badge: "Tableau blanc",
accentColor: "indigo",
openBehavior: "workspace",
},
],
},
],
},
{
id: "server",
name: "Mon Serveur",
subtitle: "Infrastructure Docker (Core i3 / 8 Go RAM) & Réseau",
icon: "Server", icon: "Server",
tabs: [ tabs: [
{ {
id: "containers", id: "infra-main",
label: "Conteneurs & Infra", label: "Services Serveur",
tiles: [ tiles: [
{ {
id: "dockge", id: "dockge",
title: "Dockge", title: "Dockge",
description: "Gestionnaire de stacks Docker Compose ultra-léger", description: "Gestionnaire Docker Compose",
url: "http://localhost:5001", url: "https://dockge.lafabriquedesgenies.be",
icon: "Container", icon: "Terminal",
size: "2x1", size: "2x1",
badge: "Docker", badge: "Docker",
accentColor: "emerald", accentColor: "emerald",
openBehavior: "workspace", openBehavior: "external",
}, },
{ {
id: "nginx-proxy", id: "nginx-proxy-manager",
title: "Nginx Reverse Proxy", title: "Nginx Proxy Manager",
description: "Gestion des domaines *.lafabriquedesgenies.be et SSL", description: "Reverse Proxy & Certificats SSL",
url: "http://localhost:81", url: "https://proxy.lafabriquedesgenies.be",
icon: "ShieldCheck", icon: "Shield",
size: "1x1", size: "1x1",
badge: "Proxy SSL", badge: "Proxy SSL",
accentColor: "blue", accentColor: "blue",
openBehavior: "workspace", openBehavior: "external",
}, },
{ {
id: "beszel", id: "duplicati",
title: "Monitoring i3", title: "Duplicati",
description: "Charge CPU, mémoire 8 Go et stockage disque", description: "Sauvegardes du serveur",
url: "http://localhost:8090", 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", icon: "Activity",
size: "1x1", size: "1x1",
badge: "Système", 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", accentColor: "violet",
openBehavior: "workspace", 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", id: "vaultwarden",
title: "Vaultwarden", title: "Vaultwarden",
description: "Gestionnaire de mots de passe auto-hébergé", description: "Gestionnaire de mots de passe",
url: "http://localhost:8222", url: "https://vault.lafabriquedesgenies.be",
icon: "KeyRound", icon: "Key",
size: "1x1", size: "1x1",
badge: "Sécurité", badge: "Coffre-fort",
accentColor: "amber", accentColor: "amber",
openBehavior: "workspace", openBehavior: "external",
}, },
{ {
id: "adguard", id: "paperless-ngx",
title: "AdGuard DNS", title: "Paperless-ngx",
description: "Filtrage publicitaire et résolution DNS locale", description: "GED & Archivage intelligent",
url: "http://localhost:3000", url: "https://paperless.lafabriquedesgenies.be",
icon: "Radar", icon: "FileText",
size: "1x1", size: "1x1",
badge: "DNS", badge: "GED",
accentColor: "teal", accentColor: "emerald",
openBehavior: "workspace", openBehavior: "external",
},
],
},
],
}, },
{ {
id: "private", id: "wikijs",
name: "Privé", title: "Wiki.js",
subtitle: "Quotidien, Mails, Agenda & Médias", description: "Base de connaissances & notes",
icon: "User", url: "https://wiki.lafabriquedesgenies.be",
tabs: [ icon: "BookOpen",
{
id: "daily",
label: "Quotidien",
tiles: [
{
id: "nextcloud-calendar",
title: "Agenda & Tâches",
description: "Planning des cours, échéances et rendez-vous",
url: "https://cloud.lafabriquedesgenies.be/apps/calendar",
icon: "Calendar",
size: "2x1",
badge: "Planning",
accentColor: "sky",
openBehavior: "workspace",
},
{
id: "webmail",
title: "Messagerie",
description: "Boîte mail personnelle et scolaire",
url: "https://mail.proton.me",
icon: "Mail",
size: "1x1", size: "1x1",
badge: "Emails", badge: "Wiki",
accentColor: "violet", accentColor: "indigo",
openBehavior: "popup", openBehavior: "external",
},
{
id: "freshrss",
title: "Lecteur RSS",
description: "Veille technologique et actualités sans pub",
url: "http://localhost:8085",
icon: "Rss",
size: "1x1",
badge: "Flux",
accentColor: "amber",
openBehavior: "workspace",
},
{
id: "jellyfin",
title: "Médiathèque",
description: "Films, séries et musique en streaming local",
url: "http://localhost:8096",
icon: "Film",
size: "2x1",
badge: "Média",
accentColor: "purple",
openBehavior: "workspace",
},
],
},
],
},
{
id: "tools",
name: "Outils",
subtitle: "Utilitaires rapides PDF, Réseau & Partage S20 FE <-> CachyOS",
icon: "Wrench",
tabs: [
{
id: "utilities",
label: "Boîte à outils",
tiles: [
{
id: "pairdrop",
title: "PairDrop (CachyOS ↔ S20 FE)",
description: "Transfert instantané de fichiers entre le PC et le Galaxy S20 FE",
url: "https://pairdrop.net",
icon: "Share2",
size: "2x1",
badge: "AirDrop Local",
accentColor: "cyan",
openBehavior: "workspace",
}, },
{ {
id: "stirling-pdf", id: "stirling-pdf",
title: "Stirling PDF", title: "Stirling PDF",
description: "Convertir, fusionner et signer des PDF pour l'école", description: "Boîte à outils PDF complète",
url: "http://localhost:8083", url: "https://pdf.lafabriquedesgenies.be",
icon: "FileText", icon: "FileCode",
size: "1x1", size: "1x1",
badge: "PDF", badge: "PDF",
accentColor: "rose", accentColor: "rose",
openBehavior: "workspace", openBehavior: "external",
}, },
{ {
id: "it-tools", id: "home-assistant",
title: "IT-Tools", title: "Home Assistant",
description: "Convertisseurs, générateurs de tokens, JSON et réseau", description: "Domotique de la maison",
url: "https://it-tools.tech", url: "https://hass.lafabriquedesgenies.be",
icon: "Cpu", 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", size: "1x1",
badge: "DevTools", 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", accentColor: "teal",
openBehavior: "workspace", openBehavior: "external",
}, },
], ],
}, },
@@ -297,35 +368,35 @@ export const initialDashboardData: DashboardData = {
}, },
{ {
id: "ai", id: "ai",
name: "IA", name: "Intelligence Artificielle",
subtitle: "Assistant Gemini intégré & Outils de recherche", subtitle: "Modèles LLM auto-hébergés & Assistant Google Gemini intégré",
icon: "Sparkles", icon: "Sparkles",
tabs: [ tabs: [
{ {
id: "assistants", id: "ai-main",
label: "Studios IA", label: "Assistants IA",
tiles: [ tiles: [
{ {
id: "aistudio", id: "open-webui",
title: "Google AI Studio", title: "Open WebUI",
description: "Tests avancés Gemini et gestion de la clé API", description: "Interface modèles LLM",
url: "https://aistudio.google.com", url: "https://ai.lafabriquedesgenies.be",
icon: "Sparkles", icon: "Bot",
size: "2x1", size: "2x1",
badge: "Gemini", badge: "LLM",
accentColor: "blue", accentColor: "emerald",
openBehavior: "popup", openBehavior: "external",
}, },
{ {
id: "wikipedia", id: "google-gemini",
title: "Wikipédia FR", title: "Google Gemini",
description: "Encyclopédie libre consultable directement dans un onglet Home", description: "Assistant IA intégré",
url: "https://fr.wikipedia.org", url: "#gemini",
icon: "BookOpen", icon: "Sparkles",
size: "2x1", size: "2x1",
badge: "Encyclopédie", badge: "Omnibox IA",
accentColor: "slate", accentColor: "blue",
openBehavior: "workspace", openBehavior: "external",
}, },
], ],
}, },