diff --git a/README.md b/README.md index 2ad4780..1c77d56 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,9 @@ enquête réelle pendant les validations de développement. Le déploiement personnel local de `http://invest.labfy` repose sur une unité systemd utilisateur et un proxy nginx limités à `127.0.0.1`. Sur cette machine, le service démarre sans inspecter la bibliothèque configurée ; l'utilisateur -charge sa liste, puis ouvre une enquête par deux actions explicites. Labfy +charge sa liste, peut [découvrir et enregistrer un dossier existant](docs/architecture/EXISTING_INVESTIGATION_LIBRARY.md) +sous `LABFY_LIBRARY` sans copie, puis ouvre l'enquête. « Fermer l’enquête » vide +le contexte actif avant une autre ouverture. Labfy démarre le Qwen local configuré dans XDG pour le prompt Agent existant. Les étapes d’installation et de retrait sont dans le [guide du poste Web](docs/ui/WEB_WORKBENCH.md). diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 5270679..d14a920 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -8,6 +8,11 @@ Le contrôle navigateur local sur espace exclusivement synthétique est décrit dans [WEB_WORKSPACE_CONTROL.md](architecture/WEB_WORKSPACE_CONTROL.md). Cette surface mutationnelle J6 est distincte des démonstrations read-only J2–J5. +La [bibliothèque d'enquêtes existantes](architecture/EXISTING_INVESTIGATION_LIBRARY.md) +est `CURRENT local` sur `SPECIMEN` : discovery explicite limitée aux enfants +directs, registre v2 rétrocompatible, fermeture et changement d'enquête avec +contexte Agent isolé. Elle n'ajoute aucun accès filesystem ou SQLite au modèle. + La [recherche OSINT assistée V1](architecture/OSINT_ASSISTED_RESEARCH.md) est `CURRENT` dans le worktree uniquement pour son parcours de laboratoire : le JobStore V4 est additif à V3, les grants sont explicites et aucun fournisseur diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index d98499e..649afb2 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -23,6 +23,13 @@ > du prompt Agent avec Qwen réel (`investigation.search`, final `COMPLETED`). > Aucune enquête réelle n'a été ouverte pendant cette validation locale. +> Ouverture d'enquêtes existantes : `CURRENT local` sur deux dossiers +> `SPECIMEN` après validation A → fermeture → B avec Qwen réel. Le registre +> v2 conserve les entrées managed et existing ; la discovery reste explicite +> et confinée à `LABFY_LIBRARY`. Le sélecteur de chemin arbitraire, la +> migration SQLite automatique et plusieurs enquêtes actives restent `TARGET` +> ou hors périmètre. Voir [bibliothèque existante](architecture/EXISTING_INVESTIGATION_LIBRARY.md). + ## 1. Ordre et portes Labfy évolue sans réécriture générale. Les contrats du cœur précèdent les diff --git a/docs/architecture/EXISTING_INVESTIGATION_LIBRARY.md b/docs/architecture/EXISTING_INVESTIGATION_LIBRARY.md new file mode 100644 index 0000000..d777edb --- /dev/null +++ b/docs/architecture/EXISTING_INVESTIGATION_LIBRARY.md @@ -0,0 +1,77 @@ +# Enquêtes existantes dans la bibliothèque locale — CURRENT local + +Ce parcours a été validé uniquement avec deux enquêtes `SPECIMEN` temporaires. +Il ne donne ni accès à un sélecteur de chemin arbitraire, ni autorisation de +traiter une enquête réelle pendant le développement. + +## Parcours humain + +Le démarrage `--lazy-library` ne lit pas la bibliothèque. Depuis le poste Web, +« Charger les enquêtes » lit le registre, puis « Rechercher les dossiers +existants » déclenche une discovery distincte. Celle-ci examine uniquement les +enfants directs de `LABFY_LIBRARY`, sans descendre dans les sous-dossiers. +L'utilisateur enregistre un candidat, l'ouvre, puis peut choisir « Fermer +l’enquête » avant d'en ouvrir une autre. « Accueil » ne ferme pas l'enquête. +Une seule enquête est active par instance. + +Les routes `POST /api/v1/library/discover-existing`, +`POST /api/v1/library/register-existing` et `POST /api/v1/library/close` +appliquent les contrôles de session, Host, Origin, CSRF, type et taille de corps +du serveur Web. Le navigateur ne transmet qu'un `candidate_id` opaque, une +génération et une clé d'idempotence ; il ne peut fournir un chemin serveur. +La réponse de discovery expose un nom d'affichage, un titre, un identifiant +d'enquête éventuel, un état et une raison bornée, jamais un chemin absolu. + +## Registre et validation + +`labfy.web_library.registry.v2` conserve des entrées `MANAGED` sous +`workspaces/` et des entrées `EXISTING` référencées par un nom d'enfant +direct. La référence reste privée et relative à la bibliothèque. Le lecteur +accepte aussi `registry.v1` ; la conversion en v2 n'a lieu qu'à la première +mutation humaine explicite. L'écriture utilise un temporaire privé, `fsync`, +remplacement atomique et `fsync` du répertoire. Des identifiants, références ou +clés d'idempotence dupliqués sont refusés. Une entrée enregistrée devenue +absente ou invalide apparaît `MISSING` ou `INVALID` sans bloquer les autres. + +La discovery est en lecture seule. Elle ignore les fichiers, dossiers cachés, +éléments internes et sous-dossiers imbriqués ; elle borne sa réponse à 256 +candidats. Elle refuse les liens symboliques du dossier, des parents de +métadonnées, de la base, du JobStore et du manifeste. Le bridge C +`validate-workspace-json` vérifie en lecture seule le schéma runtime courant, +l'identité, le titre et le JobStore de la même enquête. Il ne publie aucun +snapshot et ne migre aucune base. + +Les états des candidats sont `READY_TO_REGISTER`, `ALREADY_REGISTERED`, +`NEEDS_METADATA_MIGRATION`, `UNSUPPORTED_LEGACY_LAYOUT` et `INVALID`. +Un dossier prêt est inscrit par une mise à jour du registre uniquement : ni +copie, ni déplacement, ni renommage, ni modification de fichiers métier. +Lorsqu'une base et un JobStore sont déjà compatibles mais que seul +`workspace.json` manque, une action humaine séparée peut créer ce manifeste +après prévisualisation. La base reste inchangée. Un manifeste présent mais +invalide n'est jamais écrasé. Aucune migration SQLite n'est effectuée. + +L'inscription revalide le candidat et sa signature au moment de la mutation. +Si le dossier a changé, l'utilisateur doit relancer la discovery. Une même +intention et une même clé se rejouent sans second enregistrement ; une clé liée +à une autre intention est refusée. L'ouverture revalide le registre, le +mapping de stockage, le manifeste et les données via le bridge C avant de +publier l'identité active. Un échec d'export ne laisse pas de demi-ouverture. + +## Fermeture et contexte Qwen + +La fermeture annule les turns Agent non terminaux du workspace et attend de +façon bornée les appels déjà en cours avant d'effacer l'identité active. Elle +terminalise la mission active. Les propositions et demandes historiques gardent +leur provenance, mais ne deviennent pas actives dans l'enquête suivante. +L'interface abandonne ses requêtes en cours et réinitialise graphe, sélection, +panneaux Agent, mission, propositions, toolbox et code change. + +Sans enquête active, le démarrage d'un turn est refusé et l'interface affiche +« Ouvrez une enquête pour utiliser Qwen. ». Après A → fermeture → B, +`InvestigationContext` est reconstruit depuis B. Les références objet et les +continuations de A ne sont pas acceptées sous B. Qwen utilise les capabilities +existantes ; aucun accès direct au filesystem ou à SQLite ne lui est ajouté. + +Un sélecteur de chemin extérieur à `LABFY_LIBRARY`, la discovery récursive, +une migration automatique de base et plusieurs contextes actifs simultanés +restent hors de ce contrat. diff --git a/docs/architecture/OPERATIONAL_AGENT.md b/docs/architecture/OPERATIONAL_AGENT.md index 99dcdc2..5f62c0f 100644 --- a/docs/architecture/OPERATIONAL_AGENT.md +++ b/docs/architecture/OPERATIONAL_AGENT.md @@ -14,6 +14,13 @@ compteurs, types, sélection et activité récente bornée. Il ne reçoit ni SQL ni chemins, ni corpus de preuves. Les références sont réévaluées dans le workspace actif. +Le [parcours d'enquête existante](EXISTING_INVESTIGATION_LIBRARY.md) ferme +explicitement A avant l'ouverture de B. Les turns non terminaux de A sont +annulés avant le changement, et un nouveau turn reçoit un +`InvestigationContext` reconstruit depuis B seulement. Sans workspace actif, +le backend refuse de démarrer Qwen ; les continuations, références et demandes +de tooling de A ne sont pas admises dans B. + Les corrélations sont des candidats déterministes d'égalité normalisée (email, domaine, téléphone, username, URL, IP, IBAN/BIC, hash, identifiant structuré, provenance ou observation). Elles gardent les sources et `confidence_kind`; elles diff --git a/docs/architecture/WEB_WORKSPACE_CONTROL.md b/docs/architecture/WEB_WORKSPACE_CONTROL.md index d736c79..048da6b 100644 --- a/docs/architecture/WEB_WORKSPACE_CONTROL.md +++ b/docs/architecture/WEB_WORKSPACE_CONTROL.md @@ -1,7 +1,7 @@ # Poste de travail Web contrôlé — J6 local > État : `CURRENT` pour le poste Web local contrôlé et sa bibliothèque -> explicitement choisie. Runtime métier : V20 inchangé. JobStore : V3 inchangé. +> explicitement choisie. Runtime métier : V20 inchangé. JobStore : V4 inchangé. > V21 reste indépendante. ## Parcours @@ -17,8 +17,8 @@ L'instance écoute sur `127.0.0.1:8081` dans cet exemple. La navigation racine établit automatiquement une session locale HttpOnly et ouvre directement la bibliothèque, sans formulaire ni code de session. La bibliothèque permet de créer une enquête locale par le bridge C ou d'ouvrir une enquête enregistrée. -Une seule enquête peut être active par instance ; une -autre ouverture exige le redémarrage du poste local. L'utilisateur sélectionne +Une seule enquête peut être active par instance ; une autre ouverture exige sa +fermeture explicite, puis l'ouverture suivante. L'utilisateur sélectionne ensuite l’EML ou l’image dans le graphe et choisit « Analyser les en-têtes » ou « Examiner les métadonnées ». L’intention est persistée avant la réponse HTTP `202`. Le worker C exécute le service J3/J4 correspondant, puis C republie @@ -46,19 +46,24 @@ le démarrage conserve seulement le chemin configuré : il ne crée ni ne lit la bibliothèque et n'ouvre aucune enquête. La première page établit la session automatique sans appeler l'API de liste. L'action visible « Charger les enquêtes » déclenche seule l'initialisation et la lecture du registre ; -l'ouverture d'une enquête exige ensuite un clic distinct. Le workspace +« Rechercher les dossiers existants » lance ensuite la discovery explicite +des enfants directs. L'enregistrement et l'ouverture exigent chacun un clic +distinct. Le workspace inactif du serveur réside dans son espace XDG privé, hors de la bibliothèque. Le parcours `make web` sans cette option conserve son chargement initial. ## Bibliothèque, état XDG et limites La bibliothèque est privée (`0700`), refuse les liens symboliques et conserve -un registre atomique borné à 256 enquêtes. Chaque enquête est un enfant direct -identifié par UUID ; le bridge C existant est le seul créateur de son runtime -V20 et de son JobStore V3. La création est idempotente pour une même clé et le -même titre ; réemployer la clé avec un autre titre est refusé. L'ouverture exige +un registre atomique borné à 256 enquêtes. Les entrées `MANAGED` vivent sous +`workspaces/` ; les entrées `EXISTING` sont des enfants directs référencés +sans chemin absolu exposé. Le bridge C existant reste le seul créateur du runtime +V20 et du JobStore lors d'une création managed. La création est idempotente +pour une même clé et le même titre ; réemployer la clé avec un autre titre est +refusé. L'ouverture exige la génération courante du registre afin de ne pas utiliser une projection -périmée. +périmée. La discovery, l'adoption metadata only et l'isolation après fermeture +sont détaillées dans [le contrat de bibliothèque](EXISTING_INVESTIGATION_LIBRARY.md). L'instance place ses verrous, identité et code d'amorçage dans `$XDG_RUNTIME_DIR/labfy-investigation-web/`, ou dans un repli privé par UID diff --git a/docs/ui/WEB_WORKBENCH.md b/docs/ui/WEB_WORKBENCH.md index 031d808..b491c1d 100644 --- a/docs/ui/WEB_WORKBENCH.md +++ b/docs/ui/WEB_WORKBENCH.md @@ -10,9 +10,13 @@ À l'ouverture, le lanceur `tools/labfy` démarre ou rejoint l’instance loopback possédée, ouvre le navigateur et établit une session HttpOnly locale sans code manuel. La bibliothèque affiche les enquêtes locales enregistrées et -permet d'en créer une avec un titre explicite. Une enquête ouverte reste la +permet d'en créer une avec un titre explicite. Elle peut aussi découvrir les +dossiers existants sous `LABFY_LIBRARY` après « Rechercher les dossiers +existants », puis les enregistrer sans copie. Une enquête ouverte reste la seule enquête active de cette instance ; le poste n'en restaure pas une autre -automatiquement. Le titre et la génération de la bibliothèque identifient le +automatiquement. « Fermer l’enquête » permet ensuite d'en ouvrir une autre ; +« Accueil » ne ferme pas l'enquête. Le titre et la génération de la bibliothèque +identifient le contexte affiché, afin qu'une réponse tardive d'une enquête précédente ne le repeuple pas. @@ -138,7 +142,11 @@ absolu explicitement choisi) et `LABFY_WEB_PORT=8091`, avec mode `0600`. `agent.json` suit le contrat de configuration de `LocalModelSupervisor`. À l'arrivée, la page n'inspecte pas la bibliothèque et n'ouvre aucune enquête. L'utilisateur choisit « Charger les enquêtes », puis ouvre explicitement -l'enquête souhaitée. Le lancement du service seul ne lit pas leurs données. +l'enquête souhaitée ou lance la discovery des enfants directs et enregistre un +dossier détecté. La fiche présente nom, titre, état et raison sans chemin +absolu ; une adoption de métadonnées demande une confirmation explicite. +Le lancement du service seul ne lit pas leurs données. Sans enquête ouverte, +le prompt indique « Ouvrez une enquête pour utiliser Qwen. ». Le bloc nginx doit être placé dans `http {}` de `/etc/nginx/nginx.conf`, puis `127.0.0.1 invest.labfy` doit être ajouté une fois à `/etc/hosts`. diff --git a/prototypes/web-graph/agent_runtime.py b/prototypes/web-graph/agent_runtime.py index 6642588..72a2111 100644 --- a/prototypes/web-graph/agent_runtime.py +++ b/prototypes/web-graph/agent_runtime.py @@ -232,6 +232,36 @@ class AgentRuntime: self._set_state(turn, "CANCELLED", "Annulation demandée") return self._snapshot(turn) + def cancel_scope(self, scope, timeout=2.0): + """Cancel every non-terminal turn in one workspace scope, then wait bounded.""" + if (not isinstance(scope, str) or not scope or + not isinstance(timeout, (int, float)) or isinstance(timeout, bool) or + timeout < 0): + raise AgentRuntimeError("Annulation de scope invalide") + deadline = time.monotonic() + timeout + with self._changed: + scoped = [turn for (turn_scope, _turn_id), turn in self._turns.items() + if turn_scope == scope] + for turn in scoped: + if turn["state"] in self.TERMINAL_STATES: + continue + turn["cancel_requested"] = True + if turn["state"] == "QUEUED" or turn["state"] in self.WAITING_STATES: + self._set_state(turn, "CANCELLED", "Workspace fermé") + + # WHY: a model request or gateway call already owned by this runtime + # cannot be interrupted safely. Closing waits until its worker observes + # cancel_requested, which prevents a later tool call or cross-workspace use. + while any(turn["state"] not in self.TERMINAL_STATES for turn in scoped): + remaining = deadline - time.monotonic() + if remaining <= 0: + raise AgentRuntimeError( + "Annulation Agent encore en cours", + code="agent_runtime_cancel_timeout", status=409, + ) + self._changed.wait(remaining) + return [self._snapshot(turn) for turn in scoped] + def status(self, scope, turn_id): with self._lock: return self._snapshot(self._get_turn(scope, turn_id)) diff --git a/prototypes/web-graph/public/app.js b/prototypes/web-graph/public/app.js index ef5ebdd..cdda9aa 100644 --- a/prototypes/web-graph/public/app.js +++ b/prototypes/web-graph/public/app.js @@ -150,6 +150,7 @@ let evidenceOpenSequence = 0; let openedEvidenceId = null; let libraryGeneration = null; let pendingCreateIntent = null; +const pendingRegisterIntents = new Map(); let workspaceContext = null; let workspaceAbortController = null; let workspaceReadTail = Promise.resolve(); @@ -168,6 +169,7 @@ function invalidateWorkspaceContext() { workspaceAbortController?.abort(); workspaceAbortController = null; workspaceContext = null; + workspaceReadTail = Promise.resolve(); eventSource?.close(); eventSource = null; for (const timer of refreshTimers) clearInterval(timer); @@ -191,6 +193,24 @@ function invalidateWorkspaceContext() { byId("agent-proposal-list")?.replaceChildren(); byId("agent-toolbox-list")?.replaceChildren(); byId("agent-code-changes-list")?.replaceChildren(); + agentCalling = false; + byId("agent-send").disabled = true; + byId("agent-resume").disabled = true; + byId("agent-cancel").disabled = true; + byId("agent-status").textContent = "Ouvrez une enquête pour utiliser Qwen."; + byId("agent-runtime-detail").textContent = "Aucun contexte d’enquête actif"; + byId("agent-mission-state").textContent = "Aucune mission active."; + byId("agent-mission-goal").textContent = "—"; + byId("agent-mission-scope").textContent = "—"; + byId("agent-mission-risk").textContent = "—"; + byId("agent-mission-profile").textContent = "—"; + byId("agent-mission-contacts").textContent = "—"; + byId("agent-mission-tools").textContent = "—"; + byId("agent-mission-elapsed").textContent = "—"; + byId("agent-mission-start").disabled = true; + byId("agent-mission-cancel").disabled = true; + byId("agent-toolbox-state").textContent = "Aucun workspace actif."; + byId("agent-code-changes-state").textContent = "Aucun workspace actif."; } function beginWorkspaceContext(workspaceId, generation) { @@ -2640,7 +2660,7 @@ function renderLibrary(value) { const active = value.active_workspace_id === entry.workspace_id; const anotherActive = value.active_workspace_id !== null && !active; state.textContent = active ? "Ouverte dans cette session" : anotherActive - ? "Disponible après redémarrage du poste local" + ? "Fermez d’abord l’enquête courante" : entry.state === "READY" ? "Prête à ouvrir" : `État : ${entry.state}`; const open = document.createElement("button"); open.type = "button"; @@ -2656,12 +2676,90 @@ function renderLibrary(value) { `${value.entries.length} enquête(s) · génération ${value.generation}`; } +function renderExistingCandidates(value) { + if (value.contract !== "labfy.web_library.discovery.v1" || + !Array.isArray(value.candidates) || !Number.isInteger(value.generation)) + throw new Error("Contrat de découverte inattendu"); + libraryGeneration = value.generation; + pendingRegisterIntents.clear(); + const list = byId("existing-list"); + list.replaceChildren(); + for (const candidate of value.candidates) { + const item = document.createElement("li"); + item.className = "workspace-card existing-candidate"; + const heading = document.createElement("h3"); + heading.textContent = candidate.display_name; + const title = document.createElement("p"); + title.textContent = candidate.title ? `Titre détecté : ${candidate.title}` : + "Titre détecté : indisponible"; + const state = document.createElement("p"); + state.className = "candidate-state"; + state.textContent = candidate.state; + const reason = document.createElement("p"); + reason.textContent = candidate.reason; + item.append(heading, title, state, reason); + if (["READY_TO_REGISTER", "NEEDS_METADATA_MIGRATION"].includes(candidate.state)) { + const register = document.createElement("button"); + register.type = "button"; + register.textContent = candidate.state === "NEEDS_METADATA_MIGRATION" + ? "Préparer les métadonnées" : "Enregistrer"; + register.dataset.candidateId = candidate.candidate_id; + register.addEventListener("click", () => void registerExisting(candidate, register)); + item.append(register); + } + list.append(item); + } + byId("library-discovery-status").textContent = value.candidates.length === 0 + ? "Aucun dossier existant détecté dans la bibliothèque configurée." + : `${value.candidates.length} dossier(s) examiné(s).`; +} + +async function discoverExisting() { + const value = await libraryRequest("/api/v1/library/discover-existing", { + method: "POST", + headers: { "Content-Type": "application/json", "X-Labfy-CSRF": csrfToken }, + body: "{}", + }); + renderExistingCandidates(value); +} + +async function registerExisting(candidate, button) { + if (candidate.state === "NEEDS_METADATA_MIGRATION" && !window.confirm( + "Préparer les métadonnées Labfy ?\n\n" + + "La base est compatible. Le manifeste sera créé atomiquement et la base restera inchangée.", + )) return; + button.disabled = true; + let intent = pendingRegisterIntents.get(candidate.candidate_id); + if (!intent) { + intent = newUuid(); + pendingRegisterIntents.set(candidate.candidate_id, intent); + } + try { + const registered = await libraryRequest("/api/v1/library/register-existing", { + method: "POST", + headers: { "Content-Type": "application/json", "X-Labfy-CSRF": csrfToken }, + body: JSON.stringify({ candidate_id: candidate.candidate_id, + expected_generation: libraryGeneration, idempotency_key: intent, + human_confirmed: true }), + }); + if (registered.contract !== "labfy.web_library.workspace.v1") + throw new Error("Contrat d’enregistrement inattendu"); + libraryGeneration = registered.generation; + await refreshLibrary(); + await discoverExisting(); + } catch (error) { + byId("library-discovery-status").textContent = error.message; + button.disabled = false; + } +} + async function refreshLibrary() { const value = await libraryRequest("/api/v1/library", { cache: "no-store" }); renderLibrary(value); byId("library-load").hidden = true; byId("workspace-create").hidden = false; byId("library-list-section").hidden = false; + byId("existing-discovery").hidden = false; return value; } @@ -2674,6 +2772,7 @@ async function showLibrary({ focus = false, load = true } = {}) { byId("library-home").hidden = false; byId("workspace-create").hidden = !load; byId("library-list-section").hidden = !load; + byId("existing-discovery").hidden = !load; byId("library-load").hidden = load; byId("library-title").textContent = "Bibliothèque d’enquêtes"; byId("library-connection").textContent = load @@ -2713,6 +2812,8 @@ async function activateWorkspace(opened) { byId("library-home").hidden = true; byId("workbench-shell").hidden = false; byId("home-button").hidden = false; + byId("close-workspace").hidden = false; + byId("close-workspace").disabled = false; byId("investigation-title").textContent = opened.title; byId("mode-badge").textContent = "Espace local · contrôles locaux"; byId("open-import").disabled = false; @@ -2763,6 +2864,7 @@ async function activateLegacyWorkspace(session) { byId("library-home").hidden = true; byId("workbench-shell").hidden = false; byId("home-button").hidden = true; + byId("close-workspace").hidden = true; byId("investigation-title").textContent = session?.title ?? "Enquête locale"; byId("open-import").disabled = !operationalMode || !workspaceReady; reportDraftKey = `labfy-report-draft:${workspaceId}:${generation}`; @@ -2844,6 +2946,31 @@ async function openWorkspace(workspaceId, button) { } } +async function closeWorkspace(button) { + if (!workspaceContext) return; + button.disabled = true; + byId("connection").textContent = "Fermeture…"; + try { + const closed = await libraryRequest("/api/v1/library/close", { + method: "POST", + headers: { "Content-Type": "application/json", "X-Labfy-CSRF": csrfToken }, + body: JSON.stringify({ workspace_id: workspaceContext.workspaceId, + expected_generation: libraryGeneration, idempotency_key: newUuid(), + human_confirmed: true }), + }); + if (closed.contract !== "labfy.web_library.close.v1" || + closed.active_workspace_id !== null) + throw new Error("Contrat de fermeture inattendu"); + libraryGeneration = closed.generation; + invalidateWorkspaceContext(); + resetWorkspacePresentation(); + await showLibrary({ focus: true, load: true }); + } catch (error) { + byId("connection").textContent = error.message; + button.disabled = false; + } +} + function configureApplication() { configureControls(); configureResizableLayout(); @@ -2890,6 +3017,8 @@ function configureApplication() { } }); byId("home-button").addEventListener("click", () => void showLibrary({ focus: true })); + byId("close-workspace").addEventListener("click", (event) => + void closeWorkspace(event.currentTarget)); byId("library-load").addEventListener("click", async (event) => { const button = event.currentTarget; button.disabled = true; @@ -2902,6 +3031,18 @@ function configureApplication() { button.disabled = false; } }); + byId("library-discover").addEventListener("click", async (event) => { + const button = event.currentTarget; + button.disabled = true; + byId("library-discovery-status").textContent = "Recherche…"; + try { + await discoverExisting(); + } catch (error) { + byId("library-discovery-status").textContent = error.message; + } finally { + button.disabled = false; + } + }); byId("workspace-create-form").addEventListener("submit", async (event) => { event.preventDefault(); const button = event.currentTarget.querySelector("button"); diff --git a/prototypes/web-graph/public/index.html b/prototypes/web-graph/public/index.html index 4a9d5f8..16a40be 100644 --- a/prototypes/web-graph/public/index.html +++ b/prototypes/web-graph/public/index.html @@ -24,10 +24,16 @@
-

Enquêtes disponibles

+

Enquêtes enregistrées

    +