From 098e377d271d1d66df6803a6e9fd82cd3965e92d Mon Sep 17 00:00:00 2001 From: bolbol Date: Wed, 19 Aug 2026 23:29:25 +0100 Subject: [PATCH] feat: integrate DSH Filebrowser (pinned v2.32.0, dark mode, toggle selector UI from Claude Design, /u/nabil/files/ proxy) --- .env.example | 5 +++ app/config.py | 19 +++++++---- app/routers/proxy.py | 23 ++++++++++++- app/templates/index.html | 16 ++++++++- docker-compose.yml | 33 +++++++++++++++++- static/css/style.css | 67 +++++++++++++++++++++++++++++++++++++ static/js/hub.js | 72 +++++++++++++++++++++++++++++++++++----- 7 files changed, 218 insertions(+), 17 deletions(-) diff --git a/.env.example b/.env.example index 9f5c7b1..b502446 100644 --- a/.env.example +++ b/.env.example @@ -13,3 +13,8 @@ HERMES_TT_URL=http://100.86.197.88:3010 HERMES_NYORA_URL=http://100.86.197.88:3020 HERMES_PERSO_URL=http://100.86.197.88:3031 HERMES_NABIL_URL=http://127.0.0.1:8642 +DSH_FILEBROWSER_URL=http://dsh-filebrowser:80 + +# User IDs +DSH_UID=1001 +DSH_GID=1001 diff --git a/app/config.py b/app/config.py index 60651ec..1bc8e51 100644 --- a/app/config.py +++ b/app/config.py @@ -1,6 +1,6 @@ import os from pathlib import Path -from typing import Dict, Any, List +from typing import Dict, Any, List, Optional from pydantic import BaseModel, Field, field_validator from pydantic_settings import BaseSettings @@ -20,6 +20,8 @@ class UniverseConfig(BaseModel): icon: str persona_file: str enabled: bool = True + supports_files: bool = False + files_url: Optional[str] = None class Settings(BaseSettings): app_name: str = "Hermes Hub" @@ -36,6 +38,7 @@ class Settings(BaseSettings): hermes_nyora_url: str = os.getenv("HERMES_NYORA_URL", "http://100.86.197.88:3020") hermes_perso_url: str = os.getenv("HERMES_PERSO_URL", "http://100.86.197.88:3031") hermes_nabil_url: str = os.getenv("HERMES_NABIL_URL", "http://127.0.0.1:8642") + dsh_filebrowser_url: str = os.getenv("DSH_FILEBROWSER_URL", "http://127.0.0.1:8901") # Hub Secret — OBLIGATOIRE, aucun fallback codé en dur (Fail-Fast au démarrage) hub_secret: str = Field(..., min_length=16, description="Clé secrète maîtresse requise pour Hermes Hub") @@ -58,7 +61,6 @@ except Exception as e: # but runtime will fail fast if HUB_SECRET is absent if "HUB_SECRET" in os.environ: raise e - # Fallback only if running build/compile check with dummy env settings = None UNIVERSES: Dict[str, UniverseConfig] = { @@ -71,7 +73,8 @@ UNIVERSES: Dict[str, UniverseConfig] = { scope="tt", accent_token="--accent-tt", icon="briefcase", - persona_file="tt.yaml" + persona_file="tt.yaml", + supports_files=False ), "nyora": UniverseConfig( id="nyora", @@ -82,7 +85,8 @@ UNIVERSES: Dict[str, UniverseConfig] = { scope="nyora", accent_token="--accent-nyora", icon="sparkles", - persona_file="nyora.yaml" + persona_file="nyora.yaml", + supports_files=False ), "perso": UniverseConfig( id="perso", @@ -93,7 +97,8 @@ UNIVERSES: Dict[str, UniverseConfig] = { scope="perso", accent_token="--accent-perso", icon="home", - persona_file="perso.yaml" + persona_file="perso.yaml", + supports_files=False ), "nabil": UniverseConfig( id="nabil", @@ -104,7 +109,9 @@ UNIVERSES: Dict[str, UniverseConfig] = { scope="nabil", accent_token="--accent-nabil", icon="terminal", - persona_file="nabil.yaml" + persona_file="nabil.yaml", + supports_files=True, + files_url=os.getenv("DSH_FILEBROWSER_URL", "http://127.0.0.1:8901") ) } diff --git a/app/routers/proxy.py b/app/routers/proxy.py index ba36da2..399dd80 100644 --- a/app/routers/proxy.py +++ b/app/routers/proxy.py @@ -1,9 +1,30 @@ from fastapi import APIRouter, Request, HTTPException -from app.config import UNIVERSES +from app.config import UNIVERSES, get_universe from app.proxy import proxy_request router = APIRouter(prefix="/u", tags=["Universe Proxy"]) +@router.api_route("/{universe_id}/files", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]) +@router.api_route("/{universe_id}/files/", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]) +@router.api_route("/{universe_id}/files/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]) +async def dynamic_files_proxy(universe_id: str, request: Request, path: str = ""): + if universe_id not in UNIVERSES: + raise HTTPException(status_code=404, detail=f"Universe '{universe_id}' not found.") + + universe = UNIVERSES[universe_id] + if not universe.supports_files or not universe.files_url: + raise HTTPException(status_code=404, detail=f"Universe '{universe_id}' does not have an active filebrowser.") + + # Filebrowser runs with --baseurl /u/{universe_id}/files + full_subpath = f"u/{universe_id}/files/{path}".rstrip("/") if path else f"u/{universe_id}/files/" + + return await proxy_request( + request=request, + backend_url=universe.files_url, + path=full_subpath, + universe_id=universe_id + ) + @router.api_route("/{universe_id}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]) @router.api_route("/{universe_id}/", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]) @router.api_route("/{universe_id}/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]) diff --git a/app/templates/index.html b/app/templates/index.html index a29fd31..1943d23 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -21,6 +21,7 @@ data-name="{{ u.name }}" data-scope="{{ u.scope }}" data-tagline="{{ u.tagline }}" + data-supports-files="{{ 'true' if u.supports_files else 'false' }}" style="--item-accent: var({{ u.accent_token }});" >
@@ -60,7 +61,20 @@
- Session sécurisée VPS ↔ NAS + +
+ +
+ +
+ + Session sécurisée VPS ↔ NAS
diff --git a/docker-compose.yml b/docker-compose.yml index bd22d9c..0f42cfe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: restart: unless-stopped user: "1026:100" ports: - # Bind strictement sur loopback pour n'etre accessible QUE par Cloudflare Tunnel / Tailscale + # Bind strictement sur loopback pour n'etre accessible QUE par Cloudflare Tunnel - "127.0.0.1:8080:8080" volumes: - ./data:/app/data @@ -21,8 +21,39 @@ services: - HERMES_NYORA_URL=http://100.86.197.88:3020 - HERMES_PERSO_URL=http://100.86.197.88:3031 - HERMES_NABIL_URL=http://127.0.0.1:8642 + - DSH_FILEBROWSER_URL=http://dsh-filebrowser:80 + depends_on: + - dsh-filebrowser dns: - 1.1.1.1 - 8.8.8.8 + networks: + - hub-net labels: com.centurylinklabs.watchtower.enable: "false" + + dsh-filebrowser: + image: filebrowser/filebrowser:v2.32.0 + container_name: dsh-filebrowser + restart: unless-stopped + user: "${DSH_UID:-1001}:${DSH_GID:-1001}" + command: + - "--noauth" + - "--root=/srv" + - "--baseurl=/u/nabil/files" + - "--address=0.0.0.0" + - "--port=80" + - "--branding.theme=dark" + volumes: + - /home/dsh-agent/dsh-vps/workspace:/srv + ports: + # Bind strictement loopback + - "127.0.0.1:8901:80" + networks: + - hub-net + labels: + com.centurylinklabs.watchtower.enable: "false" + +networks: + hub-net: + driver: bridge diff --git a/static/css/style.css b/static/css/style.css index ccb2bcb..b974949 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -443,3 +443,70 @@ html, body { background-color: var(--hub-bg-card-hover); color: var(--hub-text-primary); } + +/* Mode Toggle Selector (Claude Design) */ +.hub-mode-toggle { + display: none; + align-items: center; + border: 1px solid var(--hub-border-medium); + height: 36px; + border-radius: var(--hub-radius-md); + overflow: hidden; + background-color: var(--hub-bg-card); +} + +.hub-mode-toggle.visible { + display: flex; +} + +.hub-mode-tab { + display: flex; + align-items: center; + gap: 0.5rem; + height: 100%; + padding: 0 0.85rem; + border: none; + background: transparent; + color: var(--hub-text-secondary); + font-family: var(--hub-font-sans); + font-size: 0.8125rem; + font-weight: 700; + letter-spacing: 0.02em; + cursor: pointer; + transition: all 120ms ease; + user-select: none; +} + +.hub-mode-tab:hover { + background-color: var(--hub-bg-card-hover); + color: var(--hub-text-primary); +} + +.hub-mode-tab.active { + background: var(--hub-accent-current); + color: #ffffff; + cursor: default; + box-shadow: 0 0 10px var(--hub-accent-glow-current); +} + +.hub-mode-divider { + width: 1px; + height: 60%; + background-color: var(--hub-border-medium); + flex: none; +} + +@keyframes panelIn { + from { + opacity: 0; + transform: translateY(4px); + } + to { + opacity: 1; + transform: none; + } +} + +.hub-canvas-anim { + animation: panelIn 140ms ease both; +} diff --git a/static/js/hub.js b/static/js/hub.js index 9989864..70e5e59 100644 --- a/static/js/hub.js +++ b/static/js/hub.js @@ -1,15 +1,21 @@ /** * Hermes Hub — Client-side Workspace Switcher & Context Manager * Garantit l'isolation stricte du state front-end entre univers + * Intègre le sélecteur toggle DSH (Session vs Explorateur de fichiers) */ (function () { let activeUniverse = "tt"; + let activeMode = "session"; // "session" ou "files" (pour l'univers nabil) + const iframe = document.getElementById("workspace-iframe"); const loader = document.getElementById("hub-loader"); const activeTitle = document.getElementById("active-universe-name"); const activeScope = document.getElementById("active-universe-scope"); const activeTagline = document.getElementById("active-universe-tagline"); + const nabilModeToggle = document.getElementById("nabil-mode-toggle"); + const tabSession = document.getElementById("tab-session"); + const tabFiles = document.getElementById("tab-files"); // Accent mappings const ACCENT_MAP = { @@ -33,21 +39,51 @@ document.documentElement.style.setProperty("--hub-accent-glow-current", glow); } - function switchUniverse(universeId) { - if (!universeId || (activeUniverse === universeId && iframe.src !== "about:blank")) { - return; + function getTargetUrl(universeId, mode) { + if (universeId === "nabil" && mode === "files") { + return "/u/nabil/files/"; } + return `/u/${universeId}/`; + } + + function setMode(mode) { + activeMode = mode; + if (tabSession && tabFiles) { + if (mode === "session") { + tabSession.classList.add("active"); + tabSession.setAttribute("aria-selected", "true"); + tabFiles.classList.remove("active"); + tabFiles.setAttribute("aria-selected", "false"); + } else { + tabFiles.classList.add("active"); + tabFiles.setAttribute("aria-selected", "true"); + tabSession.classList.remove("active"); + tabSession.setAttribute("aria-selected", "false"); + } + } + + // Recharger le canvas avec le mode choisi + loader.classList.remove("hidden"); + iframe.src = "about:blank"; + setTimeout(() => { + iframe.src = getTargetUrl(activeUniverse, activeMode); + }, 50); + } + + function switchUniverse(universeId) { + if (!universeId) return; const btn = document.querySelector(`.hub-universe-btn[data-universe="${universeId}"]`); if (!btn) return; // 1. ISOLATION TOTALE DU STATE CLIENT - // Forcer le déchargement de l'iframe précédente pour éliminer tout résidu JS en mémoire + // Décharger immédiatement l'iframe précédente pour purger la mémoire JS loader.classList.remove("hidden"); iframe.src = "about:blank"; // 2. Mettre à jour l'univers actif activeUniverse = universeId; + activeMode = "session"; // Réinitialiser le mode à session par défaut // 3. Mettre à jour la classe active sur la sidebar document.querySelectorAll(".hub-universe-btn").forEach((el) => el.classList.remove("active")); @@ -60,15 +96,31 @@ const name = btn.dataset.name || universeId.toUpperCase(); const scope = btn.dataset.scope || universeId; const tagline = btn.dataset.tagline || ""; + const supportsFiles = btn.dataset.supportsFiles === "true"; if (activeTitle) activeTitle.textContent = name; if (activeScope) activeScope.textContent = `Scope: ${scope}`; if (activeTagline) activeTagline.textContent = tagline; - // 6. Charger le nouvel univers via le proxy dédié - const targetProxyUrl = `/u/${universeId}/`; + // 6. Afficher ou masquer le sélecteur toggle DSH (Claude Design) + if (nabilModeToggle) { + if (supportsFiles) { + nabilModeToggle.classList.add("visible"); + // Reset tabs visual state + if (tabSession && tabFiles) { + tabSession.classList.add("active"); + tabSession.setAttribute("aria-selected", "true"); + tabFiles.classList.remove("active"); + tabFiles.setAttribute("aria-selected", "false"); + } + } else { + nabilModeToggle.classList.remove("visible"); + } + } + + // 7. Charger le nouvel univers via le proxy dédié setTimeout(() => { - iframe.src = targetProxyUrl; + iframe.src = getTargetUrl(activeUniverse, activeMode); }, 50); } @@ -105,7 +157,7 @@ } } - // Modal Personnalité + // Modales Personnalité & Clonage const personaModal = document.getElementById("persona-modal"); const cloneModal = document.getElementById("clone-modal"); @@ -190,6 +242,10 @@ }); }); + // Toggle Mode DSH (Session vs Files) + tabSession?.addEventListener("click", () => setMode("session")); + tabFiles?.addEventListener("click", () => setMode("files")); + // Outils Footer document.getElementById("btn-edit-persona")?.addEventListener("click", openPersonaModal); document.getElementById("btn-clone-universe")?.addEventListener("click", openCloneModal);