feat: integrate DSH Filebrowser (pinned v2.32.0, dark mode, toggle selector UI from Claude Design, /u/nabil/files/ proxy)

This commit is contained in:
bolbol
2026-08-19 23:29:25 +01:00
parent 4923aa380e
commit 098e377d27
7 changed files with 218 additions and 17 deletions
+5
View File
@@ -13,3 +13,8 @@ HERMES_TT_URL=http://100.86.197.88:3010
HERMES_NYORA_URL=http://100.86.197.88:3020 HERMES_NYORA_URL=http://100.86.197.88:3020
HERMES_PERSO_URL=http://100.86.197.88:3031 HERMES_PERSO_URL=http://100.86.197.88:3031
HERMES_NABIL_URL=http://127.0.0.1:8642 HERMES_NABIL_URL=http://127.0.0.1:8642
DSH_FILEBROWSER_URL=http://dsh-filebrowser:80
# User IDs
DSH_UID=1001
DSH_GID=1001
+13 -6
View File
@@ -1,6 +1,6 @@
import os import os
from pathlib import Path 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 import BaseModel, Field, field_validator
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
@@ -20,6 +20,8 @@ class UniverseConfig(BaseModel):
icon: str icon: str
persona_file: str persona_file: str
enabled: bool = True enabled: bool = True
supports_files: bool = False
files_url: Optional[str] = None
class Settings(BaseSettings): class Settings(BaseSettings):
app_name: str = "Hermes Hub" 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_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_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") 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 — 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") 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 # but runtime will fail fast if HUB_SECRET is absent
if "HUB_SECRET" in os.environ: if "HUB_SECRET" in os.environ:
raise e raise e
# Fallback only if running build/compile check with dummy env
settings = None settings = None
UNIVERSES: Dict[str, UniverseConfig] = { UNIVERSES: Dict[str, UniverseConfig] = {
@@ -71,7 +73,8 @@ UNIVERSES: Dict[str, UniverseConfig] = {
scope="tt", scope="tt",
accent_token="--accent-tt", accent_token="--accent-tt",
icon="briefcase", icon="briefcase",
persona_file="tt.yaml" persona_file="tt.yaml",
supports_files=False
), ),
"nyora": UniverseConfig( "nyora": UniverseConfig(
id="nyora", id="nyora",
@@ -82,7 +85,8 @@ UNIVERSES: Dict[str, UniverseConfig] = {
scope="nyora", scope="nyora",
accent_token="--accent-nyora", accent_token="--accent-nyora",
icon="sparkles", icon="sparkles",
persona_file="nyora.yaml" persona_file="nyora.yaml",
supports_files=False
), ),
"perso": UniverseConfig( "perso": UniverseConfig(
id="perso", id="perso",
@@ -93,7 +97,8 @@ UNIVERSES: Dict[str, UniverseConfig] = {
scope="perso", scope="perso",
accent_token="--accent-perso", accent_token="--accent-perso",
icon="home", icon="home",
persona_file="perso.yaml" persona_file="perso.yaml",
supports_files=False
), ),
"nabil": UniverseConfig( "nabil": UniverseConfig(
id="nabil", id="nabil",
@@ -104,7 +109,9 @@ UNIVERSES: Dict[str, UniverseConfig] = {
scope="nabil", scope="nabil",
accent_token="--accent-nabil", accent_token="--accent-nabil",
icon="terminal", 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")
) )
} }
+22 -1
View File
@@ -1,9 +1,30 @@
from fastapi import APIRouter, Request, HTTPException from fastapi import APIRouter, Request, HTTPException
from app.config import UNIVERSES from app.config import UNIVERSES, get_universe
from app.proxy import proxy_request from app.proxy import proxy_request
router = APIRouter(prefix="/u", tags=["Universe Proxy"]) 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}/", 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"]) @router.api_route("/{universe_id}/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"])
+15 -1
View File
@@ -21,6 +21,7 @@
data-name="{{ u.name }}" data-name="{{ u.name }}"
data-scope="{{ u.scope }}" data-scope="{{ u.scope }}"
data-tagline="{{ u.tagline }}" data-tagline="{{ u.tagline }}"
data-supports-files="{{ 'true' if u.supports_files else 'false' }}"
style="--item-accent: var({{ u.accent_token }});" style="--item-accent: var({{ u.accent_token }});"
> >
<div class="hub-universe-avatar"> <div class="hub-universe-avatar">
@@ -60,7 +61,20 @@
</div> </div>
<div class="hub-topbar-right"> <div class="hub-topbar-right">
<span style="font-size: 0.75rem; color: var(--hub-text-muted);">Session sécurisée VPS ↔ NAS</span> <!-- Sélecteur Toggle Mode DSH (Claude Design - Univers Nabil) -->
<div id="nabil-mode-toggle" role="tablist" aria-label="Mode du canvas" class="hub-mode-toggle">
<button id="tab-session" role="tab" aria-selected="true" class="hub-mode-tab active" type="button">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="square"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
<span>Session</span>
</button>
<div class="hub-mode-divider"></div>
<button id="tab-files" role="tab" aria-selected="false" class="hub-mode-tab" type="button">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="square"><path d="M4 20h14a2 2 0 0 0 2-2V9H12L10 6H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1z"></path></svg>
<span>Explorateur DSH</span>
</button>
</div>
<span id="hub-security-badge" style="font-size: 0.75rem; color: var(--hub-text-muted);">Session sécurisée VPS ↔ NAS</span>
</div> </div>
</header> </header>
+32 -1
View File
@@ -7,7 +7,7 @@ services:
restart: unless-stopped restart: unless-stopped
user: "1026:100" user: "1026:100"
ports: 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" - "127.0.0.1:8080:8080"
volumes: volumes:
- ./data:/app/data - ./data:/app/data
@@ -21,8 +21,39 @@ services:
- HERMES_NYORA_URL=http://100.86.197.88:3020 - HERMES_NYORA_URL=http://100.86.197.88:3020
- HERMES_PERSO_URL=http://100.86.197.88:3031 - HERMES_PERSO_URL=http://100.86.197.88:3031
- HERMES_NABIL_URL=http://127.0.0.1:8642 - HERMES_NABIL_URL=http://127.0.0.1:8642
- DSH_FILEBROWSER_URL=http://dsh-filebrowser:80
depends_on:
- dsh-filebrowser
dns: dns:
- 1.1.1.1 - 1.1.1.1
- 8.8.8.8 - 8.8.8.8
networks:
- hub-net
labels: labels:
com.centurylinklabs.watchtower.enable: "false" 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
+67
View File
@@ -443,3 +443,70 @@ html, body {
background-color: var(--hub-bg-card-hover); background-color: var(--hub-bg-card-hover);
color: var(--hub-text-primary); 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;
}
+64 -8
View File
@@ -1,15 +1,21 @@
/** /**
* Hermes Hub — Client-side Workspace Switcher & Context Manager * Hermes Hub — Client-side Workspace Switcher & Context Manager
* Garantit l'isolation stricte du state front-end entre univers * Garantit l'isolation stricte du state front-end entre univers
* Intègre le sélecteur toggle DSH (Session vs Explorateur de fichiers)
*/ */
(function () { (function () {
let activeUniverse = "tt"; let activeUniverse = "tt";
let activeMode = "session"; // "session" ou "files" (pour l'univers nabil)
const iframe = document.getElementById("workspace-iframe"); const iframe = document.getElementById("workspace-iframe");
const loader = document.getElementById("hub-loader"); const loader = document.getElementById("hub-loader");
const activeTitle = document.getElementById("active-universe-name"); const activeTitle = document.getElementById("active-universe-name");
const activeScope = document.getElementById("active-universe-scope"); const activeScope = document.getElementById("active-universe-scope");
const activeTagline = document.getElementById("active-universe-tagline"); 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 // Accent mappings
const ACCENT_MAP = { const ACCENT_MAP = {
@@ -33,21 +39,51 @@
document.documentElement.style.setProperty("--hub-accent-glow-current", glow); document.documentElement.style.setProperty("--hub-accent-glow-current", glow);
} }
function switchUniverse(universeId) { function getTargetUrl(universeId, mode) {
if (!universeId || (activeUniverse === universeId && iframe.src !== "about:blank")) { if (universeId === "nabil" && mode === "files") {
return; 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}"]`); const btn = document.querySelector(`.hub-universe-btn[data-universe="${universeId}"]`);
if (!btn) return; if (!btn) return;
// 1. ISOLATION TOTALE DU STATE CLIENT // 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"); loader.classList.remove("hidden");
iframe.src = "about:blank"; iframe.src = "about:blank";
// 2. Mettre à jour l'univers actif // 2. Mettre à jour l'univers actif
activeUniverse = universeId; activeUniverse = universeId;
activeMode = "session"; // Réinitialiser le mode à session par défaut
// 3. Mettre à jour la classe active sur la sidebar // 3. Mettre à jour la classe active sur la sidebar
document.querySelectorAll(".hub-universe-btn").forEach((el) => el.classList.remove("active")); document.querySelectorAll(".hub-universe-btn").forEach((el) => el.classList.remove("active"));
@@ -60,15 +96,31 @@
const name = btn.dataset.name || universeId.toUpperCase(); const name = btn.dataset.name || universeId.toUpperCase();
const scope = btn.dataset.scope || universeId; const scope = btn.dataset.scope || universeId;
const tagline = btn.dataset.tagline || ""; const tagline = btn.dataset.tagline || "";
const supportsFiles = btn.dataset.supportsFiles === "true";
if (activeTitle) activeTitle.textContent = name; if (activeTitle) activeTitle.textContent = name;
if (activeScope) activeScope.textContent = `Scope: ${scope}`; if (activeScope) activeScope.textContent = `Scope: ${scope}`;
if (activeTagline) activeTagline.textContent = tagline; if (activeTagline) activeTagline.textContent = tagline;
// 6. Charger le nouvel univers via le proxy dédié // 6. Afficher ou masquer le sélecteur toggle DSH (Claude Design)
const targetProxyUrl = `/u/${universeId}/`; 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(() => { setTimeout(() => {
iframe.src = targetProxyUrl; iframe.src = getTargetUrl(activeUniverse, activeMode);
}, 50); }, 50);
} }
@@ -105,7 +157,7 @@
} }
} }
// Modal Personnalité // Modales Personnalité & Clonage
const personaModal = document.getElementById("persona-modal"); const personaModal = document.getElementById("persona-modal");
const cloneModal = document.getElementById("clone-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 // Outils Footer
document.getElementById("btn-edit-persona")?.addEventListener("click", openPersonaModal); document.getElementById("btn-edit-persona")?.addEventListener("click", openPersonaModal);
document.getElementById("btn-clone-universe")?.addEventListener("click", openCloneModal); document.getElementById("btn-clone-universe")?.addEventListener("click", openCloneModal);