From f052b10e6af0d7b968bc54fd7cda3521302b5341 Mon Sep 17 00:00:00 2001 From: Pierre Houdyer Date: Fri, 10 Apr 2026 10:33:41 +0200 Subject: [PATCH 1/3] fix: disable hidden required inputs in connector create modal and page The Elasticsearch config_index input had a required attribute that silently blocked form validation when other connector types were selected, because the hidden panel's inputs still participated in browser validation. Disable required fields in hidden config panels and add data-turbo="false" to prevent Turbo from intercepting form submissions. --- .../connectors/_connector_cards.html | 34 ++++++++-- connectors/templates/connectors/create.html | 62 ++++++++++++++++--- 2 files changed, 83 insertions(+), 13 deletions(-) diff --git a/connectors/templates/connectors/_connector_cards.html b/connectors/templates/connectors/_connector_cards.html index 244ae54..68d24e9 100644 --- a/connectors/templates/connectors/_connector_cards.html +++ b/connectors/templates/connectors/_connector_cards.html @@ -78,6 +78,8 @@

{% trans "Connecteurs" %}

{% elif conn.connector_type == 'elasticsearch' %} + {% elif conn.connector_type == 'zipupload' %} + {% else %} {% endif %} @@ -166,6 +168,8 @@

{% trans "Connecteurs" %}

{% elif val == 'elasticsearch' %} + {% elif val == 'zipupload' %} + {% else %} {% endif %} @@ -173,7 +177,7 @@

{% trans "Connecteurs" %}

{{ label }}
- {% if val == 'sharepoint' %}{% trans "Bibliothèques SharePoint Online" %}{% elif val == 'confluence' %}{% trans "Espaces Confluence Cloud" %}{% elif val == 'elasticsearch' %}{% trans "Index Elasticsearch" %}{% else %}{% trans "Fichiers locaux ou URL" %}{% endif %} + {% if val == 'sharepoint' %}{% trans "Bibliothèques SharePoint Online" %}{% elif val == 'confluence' %}{% trans "Espaces Confluence Cloud" %}{% elif val == 'elasticsearch' %}{% trans "Index Elasticsearch" %}{% elif val == 'zipupload' %}{% trans "Archive ZIP de documents" %}{% else %}{% trans "Fichiers locaux ou URL" %}{% endif %}
@@ -212,7 +216,7 @@

{% trans "Connecteurs" %}

{% trans "Connecter SharePoint à SCORE" %}
{% trans "Configurez les paramètres de connexion pour indexer automatiquement vos documents dans SCORE." %}
-
+ {% csrf_token %} @@ -319,6 +323,15 @@

{% trans "Connecteurs" %}

+ + +
@@ -393,13 +406,15 @@

{% trans "Connecteurs" %}

sharepoint: '', confluence: '', elasticsearch: '', - generic: '' + generic: '', + zipupload: '' }; var titles = { sharepoint: 'Connecter SharePoint \u00e0 SCORE', confluence: 'Connecter Confluence \u00e0 SCORE', elasticsearch: 'Connecter Elasticsearch \u00e0 SCORE', - generic: 'Connecter une source \u00e0 SCORE' + generic: 'Connecter une source \u00e0 SCORE', + zipupload: 'Importer un fichier ZIP dans SCORE' }; var select = document.getElementById('cf-connector-type'); @@ -410,9 +425,16 @@

{% trans "Connecteurs" %}

iconBox.className = 'cf-icon-box ' + (type || 'generic'); iconBox.innerHTML = icons[type] || icons.generic; titleEl.textContent = titles[type] || titles.generic; - modalEl.querySelectorAll('.cf-connector-config').forEach(function(el) { el.style.display = 'none'; }); + /* Hide all config panels and disable their required fields */ + modalEl.querySelectorAll('.cf-connector-config').forEach(function(el) { + el.style.display = 'none'; + el.querySelectorAll('[required]').forEach(function(inp) { inp.disabled = true; }); + }); var target = document.getElementById('cf-config-' + type); - if (target) target.style.display = 'block'; + if (target) { + target.style.display = 'block'; + target.querySelectorAll('[disabled]').forEach(function(inp) { inp.disabled = false; }); + } } select.addEventListener('change', function() { updateModal(this.value); }); diff --git a/connectors/templates/connectors/create.html b/connectors/templates/connectors/create.html index 5e96ab3..ddb1ce4 100644 --- a/connectors/templates/connectors/create.html +++ b/connectors/templates/connectors/create.html @@ -53,6 +53,7 @@ .cf-icon-box.confluence { background: rgba(13,202,240,0.12); color: #22d3ee; } .cf-icon-box.elasticsearch { background: rgba(254,210,48,0.12); color: #fed230; } .cf-icon-box.generic { background: rgba(108,113,126,0.12); color: #9ca3af; } + .cf-icon-box.zipupload { background: rgba(168,85,247,0.12); color: #a855f7; } .cf-icon-box.score-icon { background: rgba(13,110,253,0.12); color: #0d6efd; } .cf-arrow { color: var(--ds-text-muted); } .cf-arrow svg { width: 20px; height: 20px; } @@ -96,6 +97,19 @@ border-color: #0d6efd; } .cf-input::placeholder { color: var(--ds-text-muted); } + .cf-input:invalid:not(:placeholder-shown) { + border-color: #dc3545; + } + .cf-error { + font-size: 11px; + color: #dc3545; + margin-top: 4px; + display: none; + } + .cf-input:invalid:not(:placeholder-shown) + .cf-error, + .was-validated .cf-input:invalid + .cf-error { + display: block; + } .cf-hint { font-size: 11px; color: var(--ds-text-muted); @@ -246,13 +260,15 @@
{% trans "Connecter SharePoint à SCORE" %}
{% trans "Configurez les paramètres de connexion pour indexer automatiquement vos documents dans SCORE." %}
- + {% if error %}
{{ error }}
{% endif %} + {% csrf_token %}
+
{% trans "Veuillez saisir un nom pour le connecteur." %}
@@ -354,6 +370,15 @@
+ + +
@@ -409,19 +434,22 @@ sharepoint: '', confluence: '', elasticsearch: '', - generic: '' + generic: '', + zipupload: '' }; var classes = { sharepoint: 'sharepoint', confluence: 'confluence', elasticsearch: 'elasticsearch', - generic: 'generic' + generic: 'generic', + zipupload: 'zipupload' }; var titles = { sharepoint: 'Connecter SharePoint \u00e0 SCORE', confluence: 'Connecter Confluence \u00e0 SCORE', elasticsearch: 'Connecter Elasticsearch \u00e0 SCORE', - generic: 'Connecter une source \u00e0 SCORE' + generic: 'Connecter une source \u00e0 SCORE', + zipupload: 'Importer un fichier ZIP dans SCORE' }; var select = document.getElementById('connector_type'); @@ -434,14 +462,34 @@ iconBox.innerHTML = icons[type] || icons.generic; /* Title */ titleEl.textContent = titles[type] || titles.generic; - /* Config panels */ - document.querySelectorAll('.connector-config').forEach(function(el) { el.style.display = 'none'; }); + /* Config panels – hide all, disable their required fields */ + document.querySelectorAll('.connector-config').forEach(function(el) { + el.style.display = 'none'; + el.querySelectorAll('[required]').forEach(function(inp) { inp.disabled = true; }); + }); var target = document.getElementById('config-' + type); - if (target) target.style.display = 'block'; + if (target) { + target.style.display = 'block'; + target.querySelectorAll('[disabled]').forEach(function(inp) { inp.disabled = false; }); + } } select.addEventListener('change', function() { update(this.value); }); + /* Form validation feedback */ + var form = document.getElementById('create-form'); + form.addEventListener('submit', function(e) { + if (!form.checkValidity()) { + e.preventDefault(); + form.classList.add('was-validated'); + var firstInvalid = form.querySelector(':invalid:not(fieldset)'); + if (firstInvalid) { + firstInvalid.focus(); + firstInvalid.scrollIntoView({behavior: 'smooth', block: 'center'}); + } + } + }); + /* Init from query param or default */ var params = new URLSearchParams(window.location.search); var initial = params.get('type'); From 668cc223236559fc5c1c94adb74396362872d046 Mon Sep 17 00:00:00 2001 From: Pierre Houdyer Date: Fri, 10 Apr 2026 14:34:18 +0200 Subject: [PATCH 2/3] fix: record error documents instead of silently skipping on empty HTTP response HTTP connector now raises ValueError when the response body is empty (e.g. HTTP 202). The ingestion pipeline now creates documents with ERROR status when text extraction fails or an exception occurs, so users can see what went wrong instead of seeing 0 documents. --- connectors/generic.py | 5 +++++ ingestion/pipeline.py | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/connectors/generic.py b/connectors/generic.py index 534fd9f..3d30500 100644 --- a/connectors/generic.py +++ b/connectors/generic.py @@ -147,6 +147,11 @@ def _fetch_http(self, source_id: str) -> RawDocument: resp = httpx.get(source_id, timeout=60, follow_redirects=True) resp.raise_for_status() + if not resp.content: + raise ValueError( + f"Empty response from {source_id} (HTTP {resp.status_code})" + ) + content_type = resp.headers.get("content-type", "").split(";")[0].strip() etag = resp.headers.get("etag", "") version = etag or hashlib.sha256(resp.content).hexdigest()[:16] diff --git a/ingestion/pipeline.py b/ingestion/pipeline.py index 1b562ff..05edf4b 100644 --- a/ingestion/pipeline.py +++ b/ingestion/pipeline.py @@ -85,8 +85,25 @@ def run(self): try: self._process_document(doc_info) except Exception as e: - logger.error("Error processing document %s: %s", doc_info.get("source_id"), e) + source_id = doc_info.get("source_id", "") + logger.error("Error processing document %s: %s", source_id, e) self._stats["errors"] += 1 + # Record the document with ERROR status + if source_id and not Document.objects.filter( + project=self.project, + connector=self.connector_config, + source_id=source_id, + ).exists(): + Document.objects.create( + tenant=self.tenant, + project=self.project, + connector=self.connector_config, + source_id=source_id, + title=doc_info.get("title", source_id), + source_url=doc_info.get("source_url", ""), + status=Document.Status.ERROR, + error_message=str(e)[:1000], + ) # Update progress self.job.processed_documents = i + 1 + len(deleted_ids) @@ -136,6 +153,25 @@ def _process_document(self, doc_info: dict): extracted = extract_text(raw_doc.content, raw_doc.content_type) if not extracted.text: logger.warning("Empty text extraction for source_id=%s", source_id) + # Record the document with ERROR status so the user can see what happened + existing = Document.objects.filter( + project=self.project, + connector=self.connector_config, + source_id=source_id, + ).first() + if not existing: + Document.objects.create( + tenant=self.tenant, + project=self.project, + connector=self.connector_config, + source_id=source_id, + title=raw_doc.title or doc_info.get("title", ""), + source_url=raw_doc.source_url, + doc_type=raw_doc.doc_type or doc_info.get("content_type", ""), + status=Document.Status.ERROR, + error_message="Text extraction returned empty content", + ) + self._stats["errors"] += 1 return # Hash for change detection From 61a8c043c610779e211b32f9072c84ec90324363 Mon Sep 17 00:00:00 2001 From: Pierre Houdyer Date: Fri, 10 Apr 2026 14:51:32 +0200 Subject: [PATCH 3/3] style: format with ruff --- connectors/generic.py | 4 +--- ingestion/pipeline.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/connectors/generic.py b/connectors/generic.py index 3d30500..4dfaba7 100644 --- a/connectors/generic.py +++ b/connectors/generic.py @@ -148,9 +148,7 @@ def _fetch_http(self, source_id: str) -> RawDocument: resp.raise_for_status() if not resp.content: - raise ValueError( - f"Empty response from {source_id} (HTTP {resp.status_code})" - ) + raise ValueError(f"Empty response from {source_id} (HTTP {resp.status_code})") content_type = resp.headers.get("content-type", "").split(";")[0].strip() etag = resp.headers.get("etag", "") diff --git a/ingestion/pipeline.py b/ingestion/pipeline.py index 05edf4b..19ea774 100644 --- a/ingestion/pipeline.py +++ b/ingestion/pipeline.py @@ -89,11 +89,14 @@ def run(self): logger.error("Error processing document %s: %s", source_id, e) self._stats["errors"] += 1 # Record the document with ERROR status - if source_id and not Document.objects.filter( - project=self.project, - connector=self.connector_config, - source_id=source_id, - ).exists(): + if ( + source_id + and not Document.objects.filter( + project=self.project, + connector=self.connector_config, + source_id=source_id, + ).exists() + ): Document.objects.create( tenant=self.tenant, project=self.project,