fix(kb): prevent double-commit race + collapse admin sections by default

Two unrelated UX issues in the Knowledge Base manage tab:

1. Double-commit 400 error. The polling tick at index.js:1217 was
   blindly overwriting a locally-set 'committing' status with whatever
   the server reported. If the GET /batch poll raced ahead of the POST
   commitJob reaching the DB, the card flipped back to 'awaiting_review',
   the "אשר הכל" button re-enabled, and a second click triggered a duplicate
   commit which the backend rejected with HTTP 400 ("job N is in status
   'done', expected 'awaiting_review'"). Fix: never downgrade
   'committing' → 'awaiting_review' from the polling response.

2. Admin tab scrolling. The "מקורות בנושא", "תוויות תת-נושא" and
   "משימות אחרונות" panels were always expanded on tab entry, pushing
   the actual upload form far down the page. Wrapped each in a
   collapsible body with a chevron toggle, default collapsed. Data
   lazy-loads on first expand.

Refs Task Master #21

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 11:08:29 +00:00
parent 1b8972c264
commit 95c48c77d9
4 changed files with 85 additions and 26 deletions
+14 -2
View File
@@ -254,12 +254,24 @@
"priority": "medium", "priority": "medium",
"subtasks": [], "subtasks": [],
"updatedAt": "2026-04-25T20:19:47.523Z" "updatedAt": "2026-04-25T20:19:47.523Z"
},
{
"id": "21",
"title": "fix(kb): prevent double-commit + collapse admin sections",
"description": "Fix UI race in commitAllBatch + collapse Sources/Labels/Recent Jobs panels",
"details": "Fix 1: in polling tick (index.js ~1217), don't downgrade local 'committing' status back to 'awaiting_review' from server. Fix 2: wrap Sources, Labels and Recent Jobs panel bodies in collapsible div with chevron toggle, default collapsed; lazy-load on first expand.",
"testStrategy": "",
"status": "in-progress",
"dependencies": [],
"priority": "high",
"subtasks": [],
"updatedAt": "2026-04-26T11:01:34.264Z"
} }
], ],
"metadata": { "metadata": {
"version": "1.0.0", "version": "1.0.0",
"lastModified": "2026-04-25T20:19:47.523Z", "lastModified": "2026-04-26T11:01:34.265Z",
"taskCount": 20, "taskCount": 21,
"completedCount": 14, "completedCount": 14,
"tags": [ "tags": [
"master" "master"
@@ -90,9 +90,12 @@
</div> </div>
</div> </div>
<div class="panel panel-default kb-sources-panel" style="padding:12px;"> <div class="panel panel-default kb-sources-panel kb-collapsible kb-collapsed" style="padding:12px;">
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:8px;"> <div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:8px;">
<h4 style="margin:0;">מקורות בנושא<span class="kb-topic-name-suffix"></span></h4> <h4 style="margin:0;cursor:pointer;user-select:none;" data-action="toggleSection" title="הרחב/כווץ">
<span class="kb-collapse-chevron glyphicon glyphicon-chevron-left" style="font-size:12px;margin-left:6px;"></span>
מקורות בנושא<span class="kb-topic-name-suffix"></span>
</h4>
<div style="display:flex;gap:8px;align-items:center;"> <div style="display:flex;gap:8px;align-items:center;">
<select class="form-control kb-sources-kind-filter" style="width:auto;"> <select class="form-control kb-sources-kind-filter" style="width:auto;">
<option value="">כל הסוגים</option> <option value="">כל הסוגים</option>
@@ -107,22 +110,29 @@
</button> </button>
</div> </div>
</div> </div>
<div class="kb-sources-table"> <div class="kb-collapsible-body" style="display:none;">
<div class="text-muted">טוען מקורות…</div> <div class="kb-sources-table">
<div class="text-muted">טוען מקורות…</div>
</div>
</div> </div>
</div> </div>
<div class="panel panel-default kb-labels-panel" style="padding:12px;"> <div class="panel panel-default kb-labels-panel kb-collapsible kb-collapsed" style="padding:12px;">
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:8px;"> <div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:8px;">
<h4 style="margin:0;">תוויות תת-נושא</h4> <h4 style="margin:0;cursor:pointer;user-select:none;" data-action="toggleSection" title="הרחב/כווץ">
<span class="kb-collapse-chevron glyphicon glyphicon-chevron-left" style="font-size:12px;margin-left:6px;"></span>
תוויות תת-נושא
</h4>
<button type="button" class="btn btn-default btn-sm" data-action="refreshLabels" title="רענן"> <button type="button" class="btn btn-default btn-sm" data-action="refreshLabels" title="רענן">
<span class="glyphicon glyphicon-refresh"></span> <span class="glyphicon glyphicon-refresh"></span>
</button> </button>
</div> </div>
<div class="kb-labels-table"> <div class="kb-collapsible-body" style="display:none;">
<div class="text-muted">טוען תוויות…</div> <div class="kb-labels-table">
</div> <div class="text-muted">טוען תוויות…</div>
<div class="text-muted small" style="margin-top:6px;"> </div>
תוויות נוצרות אוטומטית כשמעלים מסמך חדש ושירה מציעה תת-נושא. כאן ניתן למחוק תוויות שאינן בשימוש. <div class="text-muted small" style="margin-top:6px;">
תוויות נוצרות אוטומטית כשמעלים מסמך חדש ושירה מציעה תת-נושא. כאן ניתן למחוק תוויות שאינן בשימוש.
</div>
</div> </div>
</div> </div>
@@ -173,10 +183,15 @@
</div> </div>
</div> </div>
<div class="panel panel-default" style="padding:12px;"> <div class="panel panel-default kb-jobs-panel kb-collapsible kb-collapsed" style="padding:12px;">
<h4 style="margin-top:0;">משימות אחרונות</h4> <h4 style="margin-top:0;cursor:pointer;user-select:none;" data-action="toggleSection" title="הרחב/כווץ">
<div class="kb-jobs-list"> <span class="kb-collapse-chevron glyphicon glyphicon-chevron-left" style="font-size:12px;margin-left:6px;"></span>
<div class="text-muted">טוען…</div> משימות אחרונות
</h4>
<div class="kb-collapsible-body" style="display:none;">
<div class="kb-jobs-list">
<div class="text-muted">טוען…</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -236,6 +236,10 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
const topicId = (topicAttr === '' || topicAttr == null) ? null : parseInt(topicAttr, 10); const topicId = (topicAttr === '' || topicAttr == null) ? null : parseInt(topicAttr, 10);
if (batchId) this._openBatch(String(batchId), topicId); if (batchId) this._openBatch(String(batchId), topicId);
}, },
'click [data-action="toggleSection"]': function (e) {
e.preventDefault();
this._toggleSection($(e.currentTarget).closest('.kb-collapsible'));
},
}, },
// Cancel any in-flight ask/search, drop the cached last-result for // Cancel any in-flight ask/search, drop the cached last-result for
@@ -325,14 +329,11 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
// here. The polling continues silently in the background. // here. The polling continues silently in the background.
} }
// Always refresh the recent-jobs list on mount. // Sources / Labels / Recent-Jobs panels are collapsed by default
this._loadJobsList(); // (they're long lists the user usually doesn't need on mount).
// Phase 3: load the sources table when the manage tab is open. // Their data loads lazily on first expand via _toggleSection.
this._loadAdminSources();
// Phase 4: load the topics admin table (firm-wide, not topic-scoped). // Phase 4: load the topics admin table (firm-wide, not topic-scoped).
this._loadAdminTopics(); this._loadAdminTopics();
// Phase 6: load the labels admin table.
this._loadAdminLabels();
// Phase 6: load batches that still need review (RAM-only // Phase 6: load batches that still need review (RAM-only
// _activeBatch is wiped by hard refresh; this lets the user // _activeBatch is wiped by hard refresh; this lets the user
// resume any pending batch from the panel). // resume any pending batch from the panel).
@@ -344,6 +345,29 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
} }
}, },
_toggleSection: function ($panel) {
if (!$panel || !$panel.length) return;
const isCollapsed = $panel.hasClass('kb-collapsed');
const $body = $panel.find('.kb-collapsible-body').first();
const $chev = $panel.find('.kb-collapse-chevron').first();
if (isCollapsed) {
$panel.removeClass('kb-collapsed');
$chev.removeClass('glyphicon-chevron-left').addClass('glyphicon-chevron-down');
$body.slideDown(150);
// Lazy-load on first expand. The individual loaders are
// idempotent — they early-out if the list is in the DOM but
// already populated. Pass force=true so a stale "טוען…"
// placeholder gets replaced even on second expand.
if ($panel.hasClass('kb-sources-panel')) this._loadAdminSources(true);
else if ($panel.hasClass('kb-labels-panel')) this._loadAdminLabels(true);
else if ($panel.hasClass('kb-jobs-panel')) this._loadJobsList();
} else {
$panel.addClass('kb-collapsed');
$chev.removeClass('glyphicon-chevron-down').addClass('glyphicon-chevron-left');
$body.slideUp(150);
}
},
_loadJobsList: function () { _loadJobsList: function () {
const self = this; const self = this;
const $list = this.$el.find('.kb-jobs-list'); const $list = this.$el.find('.kb-jobs-list');
@@ -1214,7 +1238,15 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
let s = it.status; let s = it.status;
if (s === 'processing' && it.processing_stage === 'classifying') s = 'classifying'; if (s === 'processing' && it.processing_stage === 'classifying') s = 'classifying';
else if (s === 'processing' && it.processing_stage === 'embedding') s = 'committing'; else if (s === 'processing' && it.processing_stage === 'embedding') s = 'committing';
card.status = s; // Don't downgrade a locally-set 'committing' back to
// 'awaiting_review' just because the POST commit
// hasn't reached the DB yet — that re-enables the
// button and lets the user re-submit, which then
// explodes with HTTP 400 once the first commit
// races to 'done'.
if (!(card.status === 'committing' && s === 'awaiting_review')) {
card.status = s;
}
card.error = it.error_message || null; card.error = it.error_message || null;
// Parse ai_suggestions if string // Parse ai_suggestions if string
let ai = it.ai_suggestions; let ai = it.ai_suggestions;
+2 -2
View File
@@ -1,14 +1,14 @@
{ {
"name": "KnowledgeBase", "name": "KnowledgeBase",
"module": "KnowledgeBase", "module": "KnowledgeBase",
"version": "0.8.0", "version": "0.8.1",
"acceptableVersions": [ "acceptableVersions": [
">=8.0.0" ">=8.0.0"
], ],
"php": [ "php": [
">=8.1" ">=8.1"
], ],
"releaseDate": "2026-04-25", "releaseDate": "2026-04-26",
"author": "klear", "author": "klear",
"description": "Knowledge Base — Israeli National Insurance law, regulations, and circulars. Hybrid search + ask-shira, powered by shira-hermes KB.", "description": "Knowledge Base — Israeli National Insurance law, regulations, and circulars. Hybrid search + ask-shira, powered by shira-hermes KB.",
"displayLabel": "מאגר ידע" "displayLabel": "מאגר ידע"