Skip to content

feat(web): global indexing-progress state + cross-tab indicator (#582)#602

Merged
memtomem merged 2 commits intomainfrom
feat/582-indexing-progress-state-machine
Apr 30, 2026
Merged

feat(web): global indexing-progress state + cross-tab indicator (#582)#602
memtomem merged 2 commits intomainfrom
feat/582-indexing-progress-state-machine

Conversation

@memtomem
Copy link
Copy Markdown
Owner

@memtomem memtomem commented Apr 30, 2026

Summary

Issue #582 item 4.11. Adds a single STATE.indexing flag + cross-tab header indicator so indexing runs from any of the three trigger surfaces (Index tab streaming, Sources card per-dir, Sources "Reindex All") (a) don't trigger redundant runs that queue behind the server-side _index_lock (engine.py:476) and produce a confusing two-progress-stream UX, and (b) stay visible after the user navigates away from the launching tab.

The actual gap this closes is cross-surface trigger gating: same-button rapid-fire was already handled by btn.disabled = true (browsers don't dispatch click on disabled buttons), but Index tab → Sources card or Sources card → Index tab go through different DOM elements with independent disabled state. This PR makes them share one global guard.

What changed

web/static/app.js:

  • STATE.indexing: false added to the global state object.
  • Two helpers next to btnLoading:
    • _indexingTryStart() — early-returns with a toast if a run is already active; otherwise sets the flag and shows the header indicator.
    • _indexingEnd() — clears the flag and hides the indicator. Called from each handler's success / error / SSE-fallback paths.
  • runIndexStream gates entry on _indexingTryStart and resets via _indexingEnd on complete / onerror / SSE-malformed-3-strike paths. The new EventSource(...) setup is wrapped in try/catch — a synchronous throw there would leave STATE.indexing stuck on true and block every subsequent indexing trigger across all surfaces until page reload.

web/static/sources-memory-dirs.js:

  • mdReindexOne and mdReindexAll gate on _indexingTryStart and call _indexingEnd in their finally blocks. Cross-file references use typeof === 'function' defensive checks consistent with existing patterns in the file.

web/static/index.html:

  • New <span id="indexing-indicator" class="stat-pill indexing-pill" hidden ... aria-live="polite"> in header-info-bar, alongside the existing chunks/sources stat-pills.
  • Static asset cache-bust: style.css?v=68→69, app.js?v=72→73, sources-memory-dirs.js?v=6→7.

web/static/style.css:

  • .indexing-pill — accent-colored variant of .stat-pill with a 1.6s pulse animation. Respects prefers-reduced-motion.

web/static/locales/{en,ko}.json:

  • index.indicator — pill text
  • toast.indexing_in_progress — early-return toast

/api/config race model — settled

The umbrella plan asked for a pre-investigation: is /api/config polled (setInterval) or push-based? Grep across app.js and settings-config.js confirms it's event-driven, not polled:

  • Initial fetch: settings-config.js:1414
  • Tab visibilitychange (Config tab only): settings-config.js:1420-1425
  • Post-action: app.js:863 (after embedding-reset)
  • Tab activation: app.js:701 (if (sectionName === 'config') loadConfig())

No setInterval on /api/config. The fetch writes STATE.serverConfig and does not touch STATE.indexing, so even if a user switches to Config mid-indexing, the indicator state survives. Single boolean flag is sufficient — no epoch/version bookkeeping needed.

Limitations (not fixed in this PR)

  • The indicator is session-bound, not server-bound. If the user reloads the page mid-indexing, STATE.indexing resets to false and the indicator hides — but the indexing run continues server-side. The user loses the visual signal until the run completes and an unrelated event re-fetches /api/stats. Closing this gap properly needs a server endpoint like GET /api/indexing/active that the page queries on load. Tracked as a follow-up in the umbrella; out of scope for this PR.

Test plan

  • uv run pytest -m "not ollama"3309 passed, 46 deselected
  • uv run pytest -k i18n — 13 passed (en/ko parity check picks up the two new keys)
  • uv run ruff check packages/memtomem/src && uv run ruff format --check packages/memtomem/src — clean
  • node --check on both modified JS files — clean
  • Manual web verify (deferred — small UI delta + no new behavior surfaces beyond the indicator + toast; node syntax check + i18n parity covers the regression-prone parts)
  • CI green

References

🤖 Generated with Claude Code

memtomem pushed a commit that referenced this pull request Apr 30, 2026
Self-review on PR #602 surfaced four items, three code-level + one
PR-body. Code changes:

1. (must-fix) ``runIndexStream`` does not wrap ``new EventSource`` in
   try/catch. ``EventSource`` can throw synchronously (malformed URL,
   browser storage policy edge cases, ``SecurityError`` in cross-origin
   contexts). Without a guard, a throw between ``_indexingTryStart``
   and the handler registration leaves ``STATE.indexing`` stuck on
   ``true`` and blocks every subsequent indexing trigger across all
   surfaces until page reload. Added a try/catch around the
   ``URLSearchParams`` + ``new EventSource`` setup that mirrors the
   existing failure-path code (toast + hide progress + ``btnLoading
   off`` + ``_indexingEnd``).

2. (smaller observation) ``var(--accent, #4a90e2)`` fallbacks in
   ``.indexing-pill`` are dead code — ``--accent`` is defined in both
   theme blocks (``style.css:12, :1927, :1949``) and the fallback
   colour ``#4a90e2`` is also from neither theme. Dropped the
   fallbacks to match the rest of the file.

3. (smaller observation) Renamed i18n key
   ``indexing.indicator`` → ``index.indicator`` so it co-locates with
   the other ``index.*`` keys in the same JSON neighbourhood.
   ``toast.indexing_in_progress`` stays in the toast group, which
   matches every other toast key.

(PR body framing — concurrency claim, double-click vs cross-surface,
reload caveat — applied via ``gh pr edit`` separately so the diff
stays focused on code.)

Verified:
- ``node --check`` on the modified JS file
- ``uv run pytest -k i18n`` — 13 passed (parity holds with renamed key)
- ``ruff check`` + ``ruff format --check`` — clean

Co-Authored-By: Claude <[email protected]>
pandas-studio and others added 2 commits April 30, 2026 15:16
Issue #582 item 4.11. Three indexing entry points (Index tab streaming
``runIndexStream``, Sources card per-dir ``mdReindexOne``, Sources
"Reindex All" ``mdReindexAll``) used local ``btnLoading`` state only,
so a user mid-index on one surface could click a second indexing
trigger on another and start a concurrent run that races on the same
DB rows. There was also no signal that indexing was still in flight
once the user navigated away from the Index tab.

Centralizes the state in a single ``STATE.indexing`` flag plus two
helpers:

- ``_indexingTryStart()`` — toast + early-return if a run is already
  active; otherwise sets the flag and shows the header indicator.
- ``_indexingEnd()`` — clears the flag and hides the indicator.
  Called from each handler's success / error / SSE-fallback paths.

All three handlers now gate on ``_indexingTryStart`` and reset via
``_indexingEnd`` in their finally / completion branches.

A new ``#indexing-indicator`` pill in the header (a third
``stat-pill`` next to chunks/sources counts) is the cross-tab signal
— styled as a pulsing variant of the existing pill (respects
``prefers-reduced-motion``). ``aria-live="polite"`` announces it to
assistive tech without interrupting other speech.

i18n: two new keys with en/ko parity (``test_i18n.py`` 13 passed):
- ``indexing.indicator``: "⏳ Indexing…" / "⏳ 인덱싱 중…"
- ``toast.indexing_in_progress``: "Indexing already in progress" /
  "이미 인덱싱이 진행 중입니다"

``/api/config`` race-safety check (per the umbrella plan): grep
confirmed the config refresh is event-driven (initial fetch +
``visibilitychange`` on Config tab + post-action ``fetchServerConfig``
calls in ``app.js:863`` / ``settings-config.js:1414, 1424``) — no
``setInterval`` polling. ``STATE.indexing`` is independent from
``STATE.serverConfig``, so a Config-tab refresh during indexing
cannot clobber the flag. A single boolean is sufficient; no
epoch/version bookkeeping needed.

Static asset cache-bust (per feedback_static_asset_cache_bust.md):
``style.css?v=68→69``, ``app.js?v=72→73``,
``sources-memory-dirs.js?v=6→7``.

Verified:
- ``node --check`` passes on both modified JS files
- ``uv run pytest -m "not ollama"`` — 3309 passed
- ``ruff check`` + ``ruff format --check`` — clean

Co-Authored-By: Claude <[email protected]>
Self-review on PR #602 surfaced four items, three code-level + one
PR-body. Code changes:

1. (must-fix) ``runIndexStream`` does not wrap ``new EventSource`` in
   try/catch. ``EventSource`` can throw synchronously (malformed URL,
   browser storage policy edge cases, ``SecurityError`` in cross-origin
   contexts). Without a guard, a throw between ``_indexingTryStart``
   and the handler registration leaves ``STATE.indexing`` stuck on
   ``true`` and blocks every subsequent indexing trigger across all
   surfaces until page reload. Added a try/catch around the
   ``URLSearchParams`` + ``new EventSource`` setup that mirrors the
   existing failure-path code (toast + hide progress + ``btnLoading
   off`` + ``_indexingEnd``).

2. (smaller observation) ``var(--accent, #4a90e2)`` fallbacks in
   ``.indexing-pill`` are dead code — ``--accent`` is defined in both
   theme blocks (``style.css:12, :1927, :1949``) and the fallback
   colour ``#4a90e2`` is also from neither theme. Dropped the
   fallbacks to match the rest of the file.

3. (smaller observation) Renamed i18n key
   ``indexing.indicator`` → ``index.indicator`` so it co-locates with
   the other ``index.*`` keys in the same JSON neighbourhood.
   ``toast.indexing_in_progress`` stays in the toast group, which
   matches every other toast key.

(PR body framing — concurrency claim, double-click vs cross-surface,
reload caveat — applied via ``gh pr edit`` separately so the diff
stays focused on code.)

Verified:
- ``node --check`` on the modified JS file
- ``uv run pytest -k i18n`` — 13 passed (parity holds with renamed key)
- ``ruff check`` + ``ruff format --check`` — clean

Co-Authored-By: Claude <[email protected]>
@memtomem memtomem force-pushed the feat/582-indexing-progress-state-machine branch from b69e009 to 4d5d6e4 Compare April 30, 2026 06:17
@memtomem memtomem merged commit d026742 into main Apr 30, 2026
7 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 30, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants