feat: facette Extensions dans les facettes de recherche #157
This commit is contained in:
@@ -58,6 +58,7 @@ jobs:
|
|||||||
node ai.test.mjs
|
node ai.test.mjs
|
||||||
node ai-sidebar.test.mjs
|
node ai-sidebar.test.mjs
|
||||||
node sidebar-filters.test.mjs
|
node sidebar-filters.test.mjs
|
||||||
|
node search-facets.test.mjs
|
||||||
node sw.test.mjs
|
node sw.test.mjs
|
||||||
node collab.test.mjs
|
node collab.test.mjs
|
||||||
node mobile-editor.test.mjs
|
node mobile-editor.test.mjs
|
||||||
@@ -78,6 +79,7 @@ jobs:
|
|||||||
node ai.test.mjs
|
node ai.test.mjs
|
||||||
node ai-sidebar.test.mjs
|
node ai-sidebar.test.mjs
|
||||||
node sidebar-filters.test.mjs
|
node sidebar-filters.test.mjs
|
||||||
|
node search-facets.test.mjs
|
||||||
node sw.test.mjs
|
node sw.test.mjs
|
||||||
node collab.test.mjs
|
node collab.test.mjs
|
||||||
node mobile-editor.test.mjs
|
node mobile-editor.test.mjs
|
||||||
|
|||||||
+13
-1
@@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/),
|
|||||||
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
> **En cours de développement** : les changements à venir sont listés dans la section
|
> **En cours de développement** : les changements à venir sont listés dans la section
|
||||||
> [Unreleased](#unreleased). La dernière version livrée est **2.45.2**.
|
> [Unreleased](#unreleased). La dernière version livrée est **2.46.0**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,6 +14,18 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [2.46.0] — 2026-10-01
|
||||||
|
|
||||||
|
### Ajouté
|
||||||
|
|
||||||
|
- **#157 — facette « Extensions » dans les facettes des résultats de recherche.** Le panneau de
|
||||||
|
facettes de la page de résultats (Vaults · Tags) gagne un troisième groupe qui liste les
|
||||||
|
extensions de fichiers présents dans les résultats (`.md`, `.xlsx`, `.pdf`, …) avec leurs
|
||||||
|
compteurs ; cliquer une extension ajoute l'opérateur `ext:` à la requête (en remplaçant un
|
||||||
|
`ext:` déjà actif). Le backend calcule `facets.extensions` dans `backend/search.py`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [2.45.2] — 2026-10-01
|
## [2.45.2] — 2026-10-01
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+3
-3
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.
|
**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
@@ -976,8 +976,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 Changelog
|
||||||
|
|
||||||
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.45.2).
|
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.46.0).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Projet : ObsiGate | Version : 2.45.2 | Dernière mise à jour : Septembre 2026*
|
*Projet : ObsiGate | Version : 2.46.0 | Dernière mise à jour : Septembre 2026*
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface.
|
**Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
@@ -1151,8 +1151,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE)
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 Changelog
|
||||||
|
|
||||||
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.45.2).
|
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.46.0).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Project: ObsiGate | Version: 2.45.2 | Last updated: September 2026*
|
*Project: ObsiGate | Version: 2.46.0 | Last updated: September 2026*
|
||||||
|
|||||||
@@ -1327,6 +1327,7 @@ def advanced_search(
|
|||||||
scored_results: list[tuple[float, dict[str, Any]]] = []
|
scored_results: list[tuple[float, dict[str, Any]]] = []
|
||||||
facet_tags: dict[str, int] = defaultdict(int)
|
facet_tags: dict[str, int] = defaultdict(int)
|
||||||
facet_vaults: dict[str, int] = defaultdict(int)
|
facet_vaults: dict[str, int] = defaultdict(int)
|
||||||
|
facet_extensions: dict[str, int] = defaultdict(int)
|
||||||
|
|
||||||
# Pre-compute prefix expansions once per term (avoid repeated binary search)
|
# Pre-compute prefix expansions once per term (avoid repeated binary search)
|
||||||
prefix_expansions: dict[str, list[str]] = {}
|
prefix_expansions: dict[str, list[str]] = {}
|
||||||
@@ -1426,6 +1427,11 @@ def advanced_search(
|
|||||||
facet_vaults[result["vault"]] = facet_vaults.get(result["vault"], 0) + 1
|
facet_vaults[result["vault"]] = facet_vaults.get(result["vault"], 0) + 1
|
||||||
for tag in result.get("tags", []):
|
for tag in result.get("tags", []):
|
||||||
facet_tags[tag] = facet_tags.get(tag, 0) + 1
|
facet_tags[tag] = facet_tags.get(tag, 0) + 1
|
||||||
|
# Extension normalised without leading dot ("md", not ".md") so it can be
|
||||||
|
# fed back directly as the `ext:` query operator.
|
||||||
|
ext = str(result.get("extension") or "").lower().lstrip(".")
|
||||||
|
if ext:
|
||||||
|
facet_extensions[ext] = facet_extensions.get(ext, 0) + 1
|
||||||
|
|
||||||
total = len(scored_results)
|
total = len(scored_results)
|
||||||
page = scored_results[offset: offset + limit]
|
page = scored_results[offset: offset + limit]
|
||||||
@@ -1439,6 +1445,7 @@ def advanced_search(
|
|||||||
"facets": {
|
"facets": {
|
||||||
"tags": dict(sorted(facet_tags.items(), key=lambda x: -x[1])[:20]),
|
"tags": dict(sorted(facet_tags.items(), key=lambda x: -x[1])[:20]),
|
||||||
"vaults": dict(sorted(facet_vaults.items(), key=lambda x: -x[1])),
|
"vaults": dict(sorted(facet_vaults.items(), key=lambda x: -x[1])),
|
||||||
|
"extensions": dict(sorted(facet_extensions.items(), key=lambda x: -x[1])),
|
||||||
},
|
},
|
||||||
"query_time_ms": elapsed_ms,
|
"query_time_ms": elapsed_ms,
|
||||||
"semantic_available": semantic_available,
|
"semantic_available": semantic_available,
|
||||||
|
|||||||
Generated
+1
-1
@@ -2626,7 +2626,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.45.2"
|
version = "2.46.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.45.2"
|
version = "2.46.0"
|
||||||
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
||||||
authors = ["Bruno Charest"]
|
authors = ["Bruno Charest"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
||||||
"productName": "ObsiGate",
|
"productName": "ObsiGate",
|
||||||
"version": "2.45.2",
|
"version": "2.46.0",
|
||||||
"identifier": "com.obsigate.desktop",
|
"identifier": "com.obsigate.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../frontend",
|
"frontendDist": "../frontend",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ avec :
|
|||||||
- **Normalisation des accents** — `resume` trouve `résumé`, `elephant` trouve `éléphant`.
|
- **Normalisation des accents** — `resume` trouve `résumé`, `elephant` trouve `éléphant`.
|
||||||
- **Stemming français** — les variantes des mots sont rapprochées.
|
- **Stemming français** — les variantes des mots sont rapprochées.
|
||||||
- **Snippets surlignés** — les termes trouvés sont mis en `<mark>` dans l'extrait.
|
- **Snippets surlignés** — les termes trouvés sont mis en `<mark>` dans l'extrait.
|
||||||
- **Facettes** — compteurs par vault et par tag sur les résultats.
|
- **Facettes** — compteurs par vault, par tag et par extension sur les résultats.
|
||||||
- **Pagination** — 50 résultats par page.
|
- **Pagination** — 50 résultats par page.
|
||||||
- **Tri** — par pertinence (TF-IDF) ou par date de modification.
|
- **Tri** — par pertinence (TF-IDF) ou par date de modification.
|
||||||
- **Chips de filtres** — les filtres actifs apparaissent sous forme de puces retirables.
|
- **Chips de filtres** — les filtres actifs apparaissent sous forme de puces retirables.
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
# ObsiGate — Roadmap
|
# ObsiGate — Roadmap
|
||||||
|
|
||||||
> **Version :** 2.45.2 | **Dernière mise à jour :** 2026-10-01
|
> **Version :** 2.46.0 | **Dernière mise à jour :** 2026-10-01
|
||||||
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
||||||
> vers les fonctionnalités livrées.
|
> vers les fonctionnalités livrées.
|
||||||
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
||||||
@@ -304,6 +304,7 @@
|
|||||||
| 115 | Viewer — barre d'outils de lecture épinglée au défilement | 2.25.0 | [features/viewer-toolbar-highlight-avatars.md](./features/viewer-toolbar-highlight-avatars.md) |
|
| 115 | Viewer — barre d'outils de lecture épinglée au défilement | 2.25.0 | [features/viewer-toolbar-highlight-avatars.md](./features/viewer-toolbar-highlight-avatars.md) |
|
||||||
| 117 | Configuration — avatars prédéfinis dans le profil utilisateur (12 images) | 2.25.0 | [features/viewer-toolbar-highlight-avatars.md](./features/viewer-toolbar-highlight-avatars.md) |
|
| 117 | Configuration — avatars prédéfinis dans le profil utilisateur (12 images) | 2.25.0 | [features/viewer-toolbar-highlight-avatars.md](./features/viewer-toolbar-highlight-avatars.md) |
|
||||||
| 85 | Refonte architecturale — découpage du monolithe (14 routers, `main.py` 4 827 → ~750 lignes), stores JSON verrouillés, rate-limit SQLite optionnel | 2.27.2→2.27.13 | [features/archi-refonte-85.md](./features/archi-refonte-85.md) |
|
| 85 | Refonte architecturale — découpage du monolithe (14 routers, `main.py` 4 827 → ~750 lignes), stores JSON verrouillés, rate-limit SQLite optionnel | 2.27.2→2.27.13 | [features/archi-refonte-85.md](./features/archi-refonte-85.md) |
|
||||||
|
| 157 | Recherche — facette « Extensions » dans les résultats (3ᵉ filtre avec Vaults et Tags) | 2.46.0 | [archive](./archive/COMPLETED_v1-v2.md) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -419,6 +419,22 @@ première modification et téléchargement du fichier d'origine.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## #157 — Recherche : facette « Extensions » ✅ TERMINÉ
|
||||||
|
|
||||||
|
Le panneau de facettes de la page de résultats de recherche affiche désormais **trois** groupes :
|
||||||
|
**Vaults**, **Tags** et **Extensions**. Le troisième liste les extensions de fichiers présents
|
||||||
|
dans le résultat courant (`.md`, `.xlsx`, `.pdf`, …) avec leur compteur ; un clic ajoute
|
||||||
|
l'opérateur `ext:<type>` à la requête (un `ext:` déjà actif est remplacé, jamais dupliqué).
|
||||||
|
|
||||||
|
| Aspect | Détail |
|
||||||
|
|---|---|
|
||||||
|
| Backend | `backend/search.py::advanced_search` — `facets.extensions` : compteurs par extension normalisée (sans point, en minuscules), triés par fréquence |
|
||||||
|
| Frontend | `frontend/js/search.js::renderAdvancedSearchResults` — groupe `[data-facet="extensions"]`, items montés en `textContent` (les suffixes viennent de noms de fichiers non fiables) |
|
||||||
|
| i18n / aide | `help.desc_facet_extensions` (FR/EN), guide `docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md` |
|
||||||
|
| Tests | `tests/test_search.py::test_facets` (clé `extensions`), `tests/frontend/search-facets.test.mjs` (4 tests JSDOM, ajoutés au CI) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Grosses fonctionnalités — fiches dédiées
|
## Grosses fonctionnalités — fiches dédiées
|
||||||
|
|
||||||
| # | Feature | Version | Fiche |
|
| # | Feature | Version | Fiche |
|
||||||
|
|||||||
@@ -3694,6 +3694,10 @@
|
|||||||
<strong>Facettes vaults</strong><span data-i18n="help.desc_facet_vaults">: Compteurs
|
<strong>Facettes vaults</strong><span data-i18n="help.desc_facet_vaults">: Compteurs
|
||||||
par vault</span>
|
par vault</span>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Facettes extensions</strong><span data-i18n="help.desc_facet_extensions">:
|
||||||
|
Compteurs par extension de fichier dans les résultats</span>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Tri par pertinence</strong><span data-i18n="help.desc_9fec424d"> : Score
|
<strong>Tri par pertinence</strong><span data-i18n="help.desc_9fec424d"> : Score
|
||||||
TF-IDF (défaut)</span></li>
|
TF-IDF (défaut)</span></li>
|
||||||
|
|||||||
+27
-1
@@ -1034,7 +1034,7 @@ export function renderAdvancedSearchResults(data, query, tagFilter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Facets panel
|
// Facets panel
|
||||||
if (data.facets && (Object.keys(data.facets.tags || {}).length > 0 || Object.keys(data.facets.vaults || {}).length > 0)) {
|
if (data.facets && (Object.keys(data.facets.tags || {}).length > 0 || Object.keys(data.facets.vaults || {}).length > 0 || Object.keys(data.facets.extensions || {}).length > 0)) {
|
||||||
const facetsDiv = el("div", { class: "search-facets" });
|
const facetsDiv = el("div", { class: "search-facets" });
|
||||||
|
|
||||||
// Vault facets
|
// Vault facets
|
||||||
@@ -1078,6 +1078,32 @@ export function renderAdvancedSearchResults(data, query, tagFilter) {
|
|||||||
facetsDiv.appendChild(group);
|
facetsDiv.appendChild(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extension facets
|
||||||
|
const extFacets = data.facets.extensions || {};
|
||||||
|
if (Object.keys(extFacets).length > 0) {
|
||||||
|
const group = el("div", { class: "search-facets__group", "data-facet": "extensions" });
|
||||||
|
const label = el("span", { class: "search-facets__label" });
|
||||||
|
label.textContent = "Extensions";
|
||||||
|
group.appendChild(label);
|
||||||
|
for (const [extName, count] of Object.entries(extFacets)) {
|
||||||
|
const item = el("span", { class: "search-facets__item" });
|
||||||
|
// textContent: extension suffixes come from filenames (untrusted), never innerHTML
|
||||||
|
item.appendChild(document.createTextNode(`.${extName} `));
|
||||||
|
const facetCount = el("span", { class: "facet-count" });
|
||||||
|
facetCount.textContent = String(count);
|
||||||
|
item.appendChild(facetCount);
|
||||||
|
item.addEventListener("click", () => {
|
||||||
|
const input = document.getElementById("search-input");
|
||||||
|
// Replace any existing ext: operator, then trigger a new search
|
||||||
|
const current = input.value.replace(/ext:\S+\s*/gi, "").trim();
|
||||||
|
input.value = current + " ext:" + extName;
|
||||||
|
_triggerAdvancedSearch(input.value);
|
||||||
|
});
|
||||||
|
group.appendChild(item);
|
||||||
|
}
|
||||||
|
facetsDiv.appendChild(group);
|
||||||
|
}
|
||||||
|
|
||||||
area.appendChild(facetsDiv);
|
area.appendChild(facetsDiv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1101,6 +1101,7 @@
|
|||||||
"help.desc_f952db17": "Refresh tokens : Cookies HttpOnly avec rotation automatique",
|
"help.desc_f952db17": "Refresh tokens : Cookies HttpOnly avec rotation automatique",
|
||||||
"help.desc_fa126609": "Converts the document structure into an Obsidian Canvas (visual cards).",
|
"help.desc_fa126609": "Converts the document structure into an Obsidian Canvas (visual cards).",
|
||||||
"help.desc_facet_vaults": ": Counters by vault",
|
"help.desc_facet_vaults": ": Counters by vault",
|
||||||
|
"help.desc_facet_extensions": ": Counters by file extension in the results",
|
||||||
"help.desc_fc9391f3": "ObsiGate sends an HTTP POST to your webhook URL with event details.",
|
"help.desc_fc9391f3": "ObsiGate sends an HTTP POST to your webhook URL with event details.",
|
||||||
"help.desc_fdeaf803": ": Each file type has its icon (markdown, code, etc.)",
|
"help.desc_fdeaf803": ": Each file type has its icon (markdown, code, etc.)",
|
||||||
"help.desc_file_suggest": ": File title suggestions",
|
"help.desc_file_suggest": ": File title suggestions",
|
||||||
|
|||||||
@@ -1101,6 +1101,7 @@
|
|||||||
"help.desc_f952db17": "Refresh tokens : Cookies\n HttpOnly avec rotation automatique",
|
"help.desc_f952db17": "Refresh tokens : Cookies\n HttpOnly avec rotation automatique",
|
||||||
"help.desc_fa126609": "🔷\n Convert to canvas :\n Convertit en diagramme Mermaid.js",
|
"help.desc_fa126609": "🔷\n Convert to canvas :\n Convertit en diagramme Mermaid.js",
|
||||||
"help.desc_facet_vaults": ": Compteurs par vault",
|
"help.desc_facet_vaults": ": Compteurs par vault",
|
||||||
|
"help.desc_facet_extensions": ": Compteurs par extension de fichier dans les résultats",
|
||||||
"help.desc_fc9391f3": "# Exemple avec curl + jq dans un script bash\nWEBHOOK_URL=\"https://discord.com/api/webhooks/...\"\nOBSIGATE_SECRET=\"mon-secret\"\n\n# Lire le payload depuis stdin\nread -r payload\nevent=$(echo \"$payload\" | jq -r '.event')\npath=$(echo \"$payload\" | jq -r '.data.path')\n\ncurl -H \"Content-Type: application/json\" -d \"{\n \\\"content\\\": \\\"📝 **$event** — $path\\\"\n}\" \"$WEBHOOK_URL\"\n\n Tester avec curl\n # Simuler ce qu'ObsiGate envoie (pour tester votre endpoint)\ncurl -X POST https://votre-serveur.com/webhook \\\n -H \"Content-Type: application/json\" \\\n -H \"X-ObsiGate-Event: file_created\" \\\n -H \"X-ObsiGate-Signature: sha256=$(echo -n '{\"event\":\"file_created\",...}' | openssl dgst -sha256 -hmac 'mon-secret' | cut -d' ' -f2)\" \\\n -d '{\"event\":\"file_created\",\"timestamp\":\"2026-06-03T14:30:00Z\",\"data\":{\"vault\":\"IT\",\"path\":\"notes/test.md\"}}'\n\n Limitations actuelles\n \n \n Timeout de 5 secondes par\n requête — les webhooks sont \"fire and\n forget\" (pas de retry)\n \n \n Les modifications détectées par le watcher\n filesystem (watchdog)\n ne déclenchent pas de\n webhooks — seules les opérations via l'API\n REST sont prises en compte\n \n \n file_modified n'est pas encore\n dispatché (réservé pour version future)\n \n Pas de retry automatique en cas d'échec\n \n Pas de filtrage par vault — un webhook\n écoute toutes les vaults\n \n \n \n \n 💾 Sauvegardes et Audits\n\n Backup automatique\n \n ObsiGate crée automatiquement des sauvegardes\n avant chaque modification destructive.",
|
"help.desc_fc9391f3": "# Exemple avec curl + jq dans un script bash\nWEBHOOK_URL=\"https://discord.com/api/webhooks/...\"\nOBSIGATE_SECRET=\"mon-secret\"\n\n# Lire le payload depuis stdin\nread -r payload\nevent=$(echo \"$payload\" | jq -r '.event')\npath=$(echo \"$payload\" | jq -r '.data.path')\n\ncurl -H \"Content-Type: application/json\" -d \"{\n \\\"content\\\": \\\"📝 **$event** — $path\\\"\n}\" \"$WEBHOOK_URL\"\n\n Tester avec curl\n # Simuler ce qu'ObsiGate envoie (pour tester votre endpoint)\ncurl -X POST https://votre-serveur.com/webhook \\\n -H \"Content-Type: application/json\" \\\n -H \"X-ObsiGate-Event: file_created\" \\\n -H \"X-ObsiGate-Signature: sha256=$(echo -n '{\"event\":\"file_created\",...}' | openssl dgst -sha256 -hmac 'mon-secret' | cut -d' ' -f2)\" \\\n -d '{\"event\":\"file_created\",\"timestamp\":\"2026-06-03T14:30:00Z\",\"data\":{\"vault\":\"IT\",\"path\":\"notes/test.md\"}}'\n\n Limitations actuelles\n \n \n Timeout de 5 secondes par\n requête — les webhooks sont \"fire and\n forget\" (pas de retry)\n \n \n Les modifications détectées par le watcher\n filesystem (watchdog)\n ne déclenchent pas de\n webhooks — seules les opérations via l'API\n REST sont prises en compte\n \n \n file_modified n'est pas encore\n dispatché (réservé pour version future)\n \n Pas de retry automatique en cas d'échec\n \n Pas de filtrage par vault — un webhook\n écoute toutes les vaults\n \n \n \n \n 💾 Sauvegardes et Audits\n\n Backup automatique\n \n ObsiGate crée automatiquement des sauvegardes\n avant chaque modification destructive.",
|
||||||
"help.desc_fdeaf803": ": Chaque type de\n fichier a son icône (markdown, code, etc.)",
|
"help.desc_fdeaf803": ": Chaque type de\n fichier a son icône (markdown, code, etc.)",
|
||||||
"help.desc_file_suggest": ": Suggestions de titres de fichiers",
|
"help.desc_file_suggest": ": Suggestions de titres de fichiers",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsigate",
|
"name": "obsigate",
|
||||||
"version": "2.45.2",
|
"version": "2.46.0",
|
||||||
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
|
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
|
||||||
"main": "patch.js",
|
"main": "patch.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* ObsiGate - JSDOM tests for the search results facets panel (#157):
|
||||||
|
* vault, tag and file-extension facets rendered by
|
||||||
|
* `renderAdvancedSearchResults()` (frontend/js/search.js).
|
||||||
|
*
|
||||||
|
* Covers:
|
||||||
|
* - the three facet groups are rendered (Vaults / Tags / Extensions)
|
||||||
|
* - the extensions facet appears even when tags and vaults are empty
|
||||||
|
* - clicking an extension replaces any `ext:` operator in the search
|
||||||
|
* input and triggers a new search
|
||||||
|
*
|
||||||
|
* Usage: node tests/frontend/search-facets.test.mjs
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { strict as assert } from "node:assert";
|
||||||
|
import { JSDOM } from "jsdom";
|
||||||
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const JS_DIR = path.resolve(__dirname, "..", "..", "frontend", "js");
|
||||||
|
|
||||||
|
const dom = new JSDOM(
|
||||||
|
`<!DOCTYPE html>
|
||||||
|
<html><body>
|
||||||
|
<input id="search-input" value="" />
|
||||||
|
<select id="vault-filter"><option value="all">all</option></select>
|
||||||
|
<div id="results-bar" style="display:none"></div>
|
||||||
|
<span id="rb-count"></span>
|
||||||
|
<div id="content-area"></div>
|
||||||
|
</body></html>`,
|
||||||
|
{ url: "http://localhost/", pretendToBeVisual: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const w = dom.window;
|
||||||
|
globalThis.window = w;
|
||||||
|
globalThis.document = w.document;
|
||||||
|
globalThis.HTMLElement = w.HTMLElement;
|
||||||
|
globalThis.Element = w.Element;
|
||||||
|
globalThis.Node = w.Node;
|
||||||
|
globalThis.Event = w.Event;
|
||||||
|
globalThis.CustomEvent = w.CustomEvent;
|
||||||
|
globalThis.KeyboardEvent = w.KeyboardEvent;
|
||||||
|
globalThis.localStorage = w.localStorage;
|
||||||
|
globalThis.sessionStorage = w.sessionStorage;
|
||||||
|
Object.defineProperty(globalThis, "navigator", {
|
||||||
|
value: w.navigator,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
globalThis.MutationObserver = w.MutationObserver;
|
||||||
|
globalThis.getComputedStyle = w.getComputedStyle.bind(w);
|
||||||
|
globalThis.requestAnimationFrame = (cb) => setTimeout(cb, 0);
|
||||||
|
globalThis.cancelAnimationFrame = (id) => clearTimeout(id);
|
||||||
|
|
||||||
|
let testCount = 0;
|
||||||
|
let passCount = 0;
|
||||||
|
|
||||||
|
async function test(name, fn) {
|
||||||
|
testCount++;
|
||||||
|
try {
|
||||||
|
await fn();
|
||||||
|
passCount++;
|
||||||
|
console.log(` ✓ ${name}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(` ✗ ${name}\n ${err.message}`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backend payload shape for /api/search/advanced (facets only matter here).
|
||||||
|
const PAYLOAD = {
|
||||||
|
results: [],
|
||||||
|
total: 0,
|
||||||
|
offset: 0,
|
||||||
|
limit: 50,
|
||||||
|
query_time_ms: 1,
|
||||||
|
facets: {
|
||||||
|
vaults: { IT: 4, Recettes: 2 },
|
||||||
|
tags: { linux: 3 },
|
||||||
|
extensions: { md: 5, pdf: 2, xlsx: 1 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
globalThis.fetch = async () => ({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => PAYLOAD,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { renderAdvancedSearchResults } = await import(
|
||||||
|
pathToFileURL(path.join(JS_DIR, "search.js")).href
|
||||||
|
);
|
||||||
|
|
||||||
|
const facetGroups = () =>
|
||||||
|
Array.from(document.querySelectorAll(".search-facets__group"));
|
||||||
|
const extGroup = () =>
|
||||||
|
document.querySelector('.search-facets__group[data-facet="extensions"]');
|
||||||
|
const extItems = () =>
|
||||||
|
Array.from(extGroup()?.querySelectorAll(".search-facets__item") || []);
|
||||||
|
|
||||||
|
console.log("Search facets (#157) — Vaults / Tags / Extensions");
|
||||||
|
|
||||||
|
await test("renders the three facet groups", async () => {
|
||||||
|
renderAdvancedSearchResults(PAYLOAD, "python", null);
|
||||||
|
assert.equal(facetGroups().length, 3, "vault, tag and extension groups");
|
||||||
|
const labels = facetGroups().map((g) =>
|
||||||
|
g.querySelector(".search-facets__label").textContent,
|
||||||
|
);
|
||||||
|
assert.deepEqual(labels, ["Vaults", "Tags", "Extensions"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test("lists extensions with their counts", async () => {
|
||||||
|
const texts = extItems().map((i) => i.textContent.trim());
|
||||||
|
assert.deepEqual(texts, [".md 5", ".pdf 2", ".xlsx 1"]);
|
||||||
|
assert.equal(
|
||||||
|
extItems()[0].querySelector(".facet-count").textContent,
|
||||||
|
"5",
|
||||||
|
"count lives in .facet-count",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test("extension facet shows even with empty tags and vaults", async () => {
|
||||||
|
renderAdvancedSearchResults(
|
||||||
|
{ ...PAYLOAD, facets: { vaults: {}, tags: {}, extensions: { md: 1 } } },
|
||||||
|
"",
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
assert.ok(extGroup(), "extensions group rendered alone");
|
||||||
|
assert.equal(extItems().length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test("clicking an extension adds the ext: operator", async () => {
|
||||||
|
document.getElementById("search-input").value = "python ext:md";
|
||||||
|
renderAdvancedSearchResults(PAYLOAD, "python", null);
|
||||||
|
extItems()[1].click(); // .pdf
|
||||||
|
assert.equal(
|
||||||
|
document.getElementById("search-input").value,
|
||||||
|
"python ext:pdf",
|
||||||
|
"existing ext: operator replaced, not duplicated",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Summary ──
|
||||||
|
console.log(`\n${passCount}/${testCount} tests passed`);
|
||||||
|
if (passCount !== testCount) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
@@ -225,6 +225,14 @@ class TestAdvancedSearchAPI:
|
|||||||
resp = client.get("/api/advanced-search?q=python&vault=all")
|
resp = client.get("/api/advanced-search?q=python&vault=all")
|
||||||
data = self._check(resp)
|
data = self._check(resp)
|
||||||
assert "facets" in data
|
assert "facets" in data
|
||||||
|
# #157 — extension facet: keys are query-ready (no leading dot) and,
|
||||||
|
# when there are results, at least one extension is counted.
|
||||||
|
exts = data["facets"]["extensions"]
|
||||||
|
assert isinstance(exts, dict)
|
||||||
|
assert all(not e.startswith(".") for e in exts)
|
||||||
|
if data["total"] > 0:
|
||||||
|
assert len(exts) >= 1
|
||||||
|
assert sum(exts.values()) <= data["total"]
|
||||||
|
|
||||||
def test_empty_query(self, client):
|
def test_empty_query(self, client):
|
||||||
resp = client.get("/api/advanced-search?q=")
|
resp = client.get("/api/advanced-search?q=")
|
||||||
|
|||||||
Reference in New Issue
Block a user