fix(web): render orphan-indexed sources in Sources tab#639
Conversation
The ``/api/sources`` response carries rows with ``memory_dir=null,
kind=null`` for two paths:
1. Index-tab uploads land in ``~/.memtomem/uploads/`` (see
``system.py:upload_files``), which isn't a configured
``memory_dir``.
2. A configured dir was removed from ``memory_dirs`` after its files
were indexed; chunks survive but no longer have an owning dir.
Server-side intent — pinned by
``test_orphan_source_kind_is_null`` /
``test_kind_general_filter_includes_orphans`` — is that orphans "ride
along with general" so users can find and prune them. The Sources tab
client renderer (``_renderMemorySourceTree``) violated that intent by
keying grouping with ``s.memory_dir || ''`` and then filtering the
empty key out via ``if (k) allDirs.add(k)``, leaving orphan rows
unassigned to any vendor group and therefore unrendered.
User-facing failure mode: a file uploaded via the Index tab appeared
in ``mem_search`` and the namespaces tab, but the source link from
namespaces landed on a Sources tab that didn't contain it. Reported
via ``mm web`` Index-tab upload → namespaces source-link cross-link.
Fix: collect orphan rows into a separate ``orphanItems`` list,
attribute them to the ``user`` vendor (both feed paths are
user-driven), and append a collapsed "Other (unregistered)"
``<details>`` block at the end of that vendor's active panel reusing
the existing ``_renderMemorySourceItem`` card shape so file-click →
chunks drill-in stays identical to indexed dirs. The user vendor's
sub-tab badge and the empty-state guard count orphans too, so a user
with only Index-tab uploads sees a populated ``user`` tab instead of
"user memory not found".
Server contract is unchanged; the fix is client-only. Index.html
``app.js?v=`` bumped 79 → 80 to bust disk cache. New i18n key
``sources.orphan_label`` added to en/ko (parity test passes).
Co-Authored-By: Claude <[email protected]>
Two review nits on the orphan-render commit, both surfaced by
self-review:
1. ``renderOrphanBlock`` was normalising chunk-bar widths against
``localMax`` (max chunk_count *within the orphan list*), while
``_renderMemoryDirGroup`` uses the outer ``maxChunks`` (tree-wide
max). That meant a 5-chunk file in the orphan section drew a
longer bar than a 5-chunk file in an indexed dir above —
visually inconsistent without a meaningful reason. Pass the
outer ``maxChunks`` so identical chunk_counts get identical
bars regardless of section.
2. The multi-product render branch always appended an empty
``.source-vendor-products`` wrapper when ``visibleCats`` had no
non-empty entries (the orphan-only case for ``user`` vendor).
The empty div still ships its CSS margin, leaving a phantom
gap before the orphan block. Guarded with
``products.children.length`` so the wrapper only mounts when
it has actual content.
Cache-bust ``app.js?v=`` 80 → 81.
Co-Authored-By: Claude <[email protected]>
Playwright verification (isolated env)Ran both manual scenarios from the test plan against an isolated worktree ( Scenario 1 — Index tab upload
{
orphanBlock: true,
orphanLabel: "기타 (등록되지 않은 위치)",
orphanCount: "1",
userBadgeCount: "1",
userBadgeHidden: false,
activeTab: "user",
sourceItems: 1
}Scenario 2 — registered dir removed mid-flight
{
orphanBlock: true,
orphanCount: "2",
userBadgeCount: "2",
orphanItems: ["upload-fixture.md", "note-in-registered-dir.md"],
indexedDirsLabel: ["…/mm-orphan-test/dummy-memories"],
emptyProductsWrapper: "absent (good - skipped)", // review nit 2 effective
placeholderShown: false,
sourcesListChildrenTags: [
"details.source-group.source-group-memory",
"details.source-vendor-orphan"
]
}Notes on review nit 1 (bar scale)Both files in this run had What this leaves uncoveredThis is one-shot manual verification, not a regression guard. Tracked in #641 (JS unit-test infrastructure) so the next time this code path moves, a 10-line jsdom assertion catches the regression on CI instead of waiting for a user report. |
Summary
The
/api/sourcesresponse carries rows withmemory_dir=null, kind=nullfor two paths:~/.memtomem/uploads/(system.py:upload_files) — not a configuredmemory_dir.memory_dirsbut no longer have an owning dir.Server-side intent (pinned by
test_orphan_source_kind_is_null/test_kind_general_filter_includes_orphans) is that orphans "ride along with general" so users can find and prune them. The Sources tab client renderer violated that intent by:…leaving orphan rows unassigned to any vendor group and therefore unrendered.
User-facing failure mode
File uploaded via the Index tab appears in
mem_searchand the namespaces tab, but the source link from namespaces lands on a Sources tab that doesn't contain it. Reported viamm web: Index-tab upload → namespaces source-link cross-link → "있는데 없음" state.Fix
Client-only change in
_renderMemorySourceTree:!s.memory_dirinto a separateorphanItemslist.uservendor (both feed paths are user-driven).<details>block at the end of that vendor's active panel._renderMemorySourceItemcard shape so file-click → chunks drill-in works identically to indexed dirs.usertab instead of "user memory not found".Server contract is unchanged.
index.htmlapp.js?v=bumped 79 → 81 (two-commit body changes) to bust disk cache. New i18n keysources.orphan_labeladded toen.json/ko.json.Follow-up commit (
55c091b) addresses two self-review nits:renderOrphanBlocknow passes the outer tree-widemaxChunksto_renderMemorySourceIteminstead of a local max, so chunk-bar widths stay visually comparable across indexed groups and the orphan section.products.children.lengthbefore appending, so an orphan-onlyuservendor doesn't ship an empty.source-vendor-productswrapper before the orphan block.Test plan
test_i18n.pyparity (en/ko key set + placeholder shape) — 12 tests pass-k sourceslice (75 tests) — pass against the worktreetest_web_routes.py); this PR doesn't loosen itmm web→ Index tab upload a file → switch to Sources tab → confirm file appears under "Other (unregistered)" inside the User sub-tab — verified via Playwright in an isolatedHOME=/tmp/mm-orphan-testenvmemory_dirthat has indexed chunks → confirm those chunks now show under "Other (unregistered)" instead of disappearing — verified via Playwright; both upload and ex-registered-dir files end up in the same orphan section, count=2, User badge=2See the verification comment below for full Playwright assertion output.
Out of scope
_active_runspolling-loop suspicion → filed as Investigation:/api/indexing/activeendpoint reports active indefinitely after watcher / SSE indexing #640mm webstatic modules #641🤖 Generated with Claude Code