12 KiB
Code Context — ObsiGate Bug Investigation
Files Retrieved
frontend/app.js(lines 5585–5665) —showWelcome()rebuilds dashboard HTML with only bookmarks + recent sectionsfrontend/index.html(lines 360–406) — Initial dashboard DOM has all 4 sections: stats, bookmarks, conflicts, recentfrontend/app.js(lines 7640–7672) —TabManager.activate()hides dashboard;_showDashboard()restores itfrontend/app.js(lines 7497–7597) —TabManager.openPreview()/openPersistent()definitionsfrontend/app.js(lines 7778–7830) —TabManager._renderTabs()— missingpreviewCSS classfrontend/style.css(lines 5450–5457) — CSS rules for.tab-item.preview(italic)frontend/app.js(lines 2340–2376) — Tree click handlers in_renderDirectoryInContainerfrontend/app.js(lines 3144–3310) —renderFile()overwritescontent-area.innerHTML, destroying dashboard DOMfrontend/app.js(lines 3347–3380) —DashboardStatsWidgetrelies ondashboard-stats-gridelementfrontend/app.js(lines 3381–3434) —DashboardConflictsWidgetrelies ondashboard-conflicts-sectionelementbackend/search.py(lines 239–425) —InvertedIndexclass,is_stale(),rebuild(),get_inverted_index()backend/indexer.py(lines 28, 330–601, 634–667, 770–839) —_index_generationcounter and all increment sitesbackend/watcher.py(lines 191–226) — Debounce: dispatches batched events afterdebounce_seconds(2s default)backend/main.py(lines 376–420) —_on_vault_change()callsupdate_single_fileper event → each increments_index_generationbackend/search.py(lines 670–700, 844–875) —advanced_search()andsuggest_titles()callget_inverted_index()
Issue 1: Dashboard Layout — Sections Disappearing
Root Cause: showWelcome() Rebuild Kills Stats & Conflicts Sections
showWelcome() (frontend/app.js, lines 5585–5665):
- It checks if
dashboard-homeordashboard-bookmarks-sectiondon't exist. - If either is missing, it replaces
content-area.innerHTMLwith only two sections: bookmarks and recent. - Stats and Conflicts sections are NOT included in this rebuilt HTML.
// line 5595 — only rebuilds if home or bookmarksSection is missing
if (area && (!home || !bookmarksSection)) {
area.innerHTML = `
<div id="dashboard-home" ...>
<div id="dashboard-bookmarks-section" class="dashboard-section">...</div>
<div id="dashboard-recent-section" class="dashboard-section">...</div>
</div>`;
// NOTE: No #dashboard-stats-section or #dashboard-conflicts-section!
}
The original dashboard (frontend/index.html, lines 360–406) has 4 sections:
#dashboard-stats-section#dashboard-bookmarks-section#dashboard-conflicts-section#dashboard-recent-section
When does the dashboard get destroyed?
-
renderFile()at line 3302:area.innerHTML = "";— this wipescontent-areaincluding#dashboard-home(which is a child ofcontent-area). -
After the file renders,
activate()at line 7642 hides the dashboard:dashboard.style.display = "none"— but at this point the dashboard DOM elements have been removed from the DOM. -
Later, when
_showDashboard()(line 7764) is called:_showDashboard() { const area = document.getElementById("content-area"); area.innerHTML = ""; // clear file content const dashboard = document.getElementById("dashboard-home"); if (dashboard) { dashboard.style.display = ""; // show it area.appendChild(dashboard); // move back into DOM } }This works only if
dashboard-homestill exists in the document. ButrenderFile()calledarea.innerHTML = ""which destroyed it. -
Next time
showWelcome()is called (e.g., aftergoHome()), it detects!home→ rebuilds with only 2 sections.
Why "Recently opened" stays visible when stats/bookmarks disappear:
- The rebuilt HTML in
showWelcome()includes#dashboard-recent-sectionand#dashboard-bookmarks-section, but not#dashboard-stats-sectionor#dashboard-conflicts-section. DashboardStatsWidget.load()(line 3348) looks fordocument.getElementById("dashboard-stats-grid")— if not found, it silently returns.DashboardConflictsWidget.load()(line 3383) looks fordocument.getElementById("dashboard-conflicts-section")— if not found, silently returns.- So stats and conflicts sections vanish, while bookmarks and recent survive the rebuild.
How the Dashboard Visibility Toggle Works
TabManager.activate()(line 7642):dashboard.style.display = "none"— hides dashboard when a file tab is shown.TabManager._showDashboard()(line 7764):dashboard.style.display = ""+ appends tocontent-area— shows dashboard when all tabs close.
CSS for Dashboard Sections
frontend/style.css (lines 4739–4742):
.dashboard-section {
display: flex;
flex-direction: column;
}
All .dashboard-section elements are just flex columns — there's no show/hide logic in CSS beyond what JavaScript sets via inline style.display.
Issue 2: Click/Double-Click Handling
Tree Click Handlers — Correctly Wired
In _renderDirectoryInContainer (frontend/app.js, lines 2354–2360):
fileItem.addEventListener("click", () => {
scrollTreeItemIntoView(fileItem, false);
TabManager.openPreview(vaultName, item.path); // single click → preview
closeMobileSidebar();
});
fileItem.addEventListener("dblclick", (e) => {
e.preventDefault();
TabManager.openPersistent(vaultName, item.path); // double click → persistent
});
Same pattern at lines 3132–3133 (sidebar recent), 4909–4910 and 5090–5091 (search results). Wiring is correct.
openPreview() and openPersistent() — Properly Defined
openPreview()(line 7497): Creates tab withpreview: true, closes existing preview, activates.openPersistent()(line 7533): If a preview tab exists for this file, converts it to persistent (previewTab.preview = false). If already persistent, just activates. Otherwise creates new persistent tab viathis.open().
Both are correctly implemented.
_renderTabs() — BUG: Missing preview CSS Class
_renderTabs() at line 7792:
el.className = "tab-item" + (tab.id === this._activeTabId ? " active" : "");
The preview class is NEVER added. The CSS has rules for .tab-item.preview:
/* style.css line 5450 */
.tab-item.preview .tab-name { font-style: italic; opacity: 0.75; }
.tab-item.preview { background: var(--bg-hover); }
But since the JS never adds el.classList.add("preview") when tab.preview is true, preview tabs do not render in italic.
The fix should be:
el.className = "tab-item" + (tab.id === this._activeTabId ? " active" : "") + (tab.preview ? " preview" : "");
Additionally, the click handler on tabs at line 7824 calls this.activate(tab.id) — activating a preview tab should work fine since activate() already properly handles loading file content.
Issue 3: Search Performance — "Rebuilding Inverted Index..."
InvertedIndex Architecture
backend/search.py (lines 239–425):
InvertedIndexis a singleton stored in module variable_inverted_index(line 415).is_stale()(line 270): returnsTruewhen_indexer._index_generation != self._source_generation.rebuild()(line 278): logs"Rebuilding inverted index...", iterates over the entireindexdict (all vaults, all files), rebuilds all internal structures from scratch. Setsself._source_generation = _indexer._index_generationat line 345.get_inverted_index()(line 418): check-then-rebuild every call:def get_inverted_index() -> InvertedIndex: if _inverted_index.is_stale(): _inverted_index.rebuild() return _inverted_index
What triggers _index_generation increments?
backend/indexer.py — the counter increments at 7 sites, every time the index mutates:
| Line | Trigger |
|---|---|
| 336 | rebuild_index() — full index rebuild on startup or manual reindex |
| 375 | Per-vault processing within rebuild_index() (once per vault) |
| 468 | reindex_vault() — single vault reindex |
| 601 | _remove_file_from_structures() — single file deletion |
| 667 | _add_file_to_structures() — single file addition |
| 794 | remove_vault_from_index() — vault removal |
| 839 | add_vault_to_index() — vault addition |
Frequent Rebuild Trigger: File Watcher Hot-Reload
backend/main.py (lines 376–420) — _on_vault_change():
- Each file watcher event (create/modify/delete/move) calls
update_single_file(),remove_single_file(), orhandle_file_move(). - These call
_remove_file_from_structures()+_add_file_to_structures(), each incrementing_index_generation. - The watcher debounces for 2 seconds (
backend/watcher.pyline 100), then dispatches all batched events at once (line 207–210). - Each event in the batch still increments
_index_generationindependently.
Result: If you save 5 files simultaneously, _index_generation jumps by up to 5. The next search/autocomplete call triggers a full inverted index rebuild. On a vault with thousands of files, each rebuild is expensive.
Which API calls trigger the rebuild?
get_inverted_index() is called from:
advanced_search()atsearch.pyline 684 (the TF-IDF advanced search)suggest_titles()atsearch.pyline 862 (autocomplete)main.pyline 2518 (stats/diagnostic endpoint)
The basic search() function (line 430+) does NOT use get_inverted_index() — it scans the raw index dict directly. But suggest_titles() is called on every keystroke in the search bar.
Why frequent saves cause repeated rebuilds
- User saves file → watcher fires
modifiedevent →_on_vault_change→update_single_file→_index_generation += 1(or +2 for add+remove). - User types in search bar →
suggest_titles()→get_inverted_index()→is_stale()returnsTrue→ full rebuild. - User saves again → generation increments → next keystroke triggers another full rebuild.
This is the core performance issue: single-file index mutations invalidate the entire inverted index, and it gets rebuilt from scratch on the very next query.
Architecture Summary
Data Flow for Dashboard
index.html (full dashboard DOM)
→ renderFile() destroys dashboard-home via innerHTML
→ activate() hides remaining dashboard ref
→ _showDashboard() tries to re-append dashboard-home
→ showWelcome() rebuilds from scratch (only 2/4 sections)
→ Stats/Conflicts widgets silently no-op (target elements missing)
Data Flow for Search Performance
File change → watcher debounce (2s) → _on_vault_change()
→ update_single_file() → _remove + _add → _index_generation += 2
→ OR remove_single_file() → _index_generation += 1
User types → suggest_titles() → get_inverted_index()
→ is_stale() checks generation counter
→ if counter changed: full rebuild of InvertedIndex (O(N) over all files)
Start Here
Open frontend/app.js at line 5585 (showWelcome()) — this is the dashboard rebuild function and the primary cause of Issue 1. The fix needs to:
- Include all 4 dashboard sections in
showWelcome()'s rebuild HTML - OR: store the dashboard DOM before
renderFile()wipes it, and restore it in_showDashboard()
For Issue 2, open frontend/app.js at line 7792 (_renderTabs) — add the missing preview class.
For Issue 3, open backend/search.py at line 418 (get_inverted_index) and backend/indexer.py lines 600–667 — consider whether single-file incremental updates to the inverted index are feasible, or whether the generation counter should be debounced/coalesced for file watcher batches.