Skip to content

web/upload: cancel-after-partial-success leaves UI stale on Upload + Search drop sites #804

@memtomem

Description

@memtomem

Background

PR #802 wired the SPA confirm-and-retry UX for the redaction guard. For
/api/upload, when the user cancels the bypass dialog, the two
upload entry points behave inconsistently and both leave the UI in a
stale state if any files were already persisted on the first pass.

This issue covers both sites in one fix because they share the same
state-refresh and selection-cleanup logic.

Affected sites

A. Upload tab (packages/memtomem/src/memtomem/web/static/app.js ~line 4082)

const upload = await uploadFilesWithRedactionRetry(form);
const data = upload.data;
show(result);
result.innerHTML = '';
data.files.forEach(r => { /* render rows */ });
if (upload.cancelled) {
  showToast(t('toast.upload_redaction_cancelled', { count: upload.blockedFileCount }), 'error');
  return;            // ← early return skips _markDataStale() / refresh / clear
}

On cancel, per-file rows render, the cancel toast renders, but:

  • _markDataStale() is never called → header counts / source stats stay
    cached from before the upload.
  • selectedFiles is not cleared → the same files remain in the picker.
    If the user retries with a different surface (e.g. paste into Add),
    the upload picker still lists them.

B. Search-tab drag-drop (app.js ~line 5594)

const upload = await uploadFilesWithRedactionRetry(fd);
if (upload.cancelled) return;        // ← silent: no toast, no rendering
const data = upload.data;

On cancel, the drop-zone shows nothing — no toast, no per-file error
display. The earlier toast.indexing_files toast is still on screen,
which actively misleads (suggests success). Same _markDataStale() gap
as site A for any files that were written on the first pass.

Reproduction

  1. mm web with at least one indexed source already populated (so the
    stats panel has a non-zero baseline to compare against).
  2. Site A: Index → Upload → drop a mixed batch (1 clean + 1 secret).
  3. First pass: clean file persists, secret file returns blocked.
  4. Cancel the bypass dialog.
  5. Observe: header / source stats unchanged despite a new file having
    been written; selectedFiles still populated.
  6. Repeat for site B by drag-dropping onto the Search-tab drop-zone.
    Note: no cancel toast at all.

Proposed fix

In both sites, on upload.cancelled:

  1. If upload.data.files contains any successful writes (indexed_chunks > 0
    or no error), call _markDataStale() and clear those entries from
    selectedFiles (site A) before returning.
  2. Show a uniform cancel toast at site B (currently only site A has one).
  3. Keep the blocked entries visible / re-attachable so the user can
    modify and retry without re-selecting.

A small helper — say _finalizeUploadCancel({data, blockedCount})
would let both sites share the cleanup logic and avoid drift.

Test plan

  • Playwright spec extension: cancel path on mixed batch asserts
    _markDataStale() was called (data-stale class on header, or
    follow-up /api/stats refetch fires).
  • Site B spec: cancel after mixed-batch drop on Search drop-zone fires a
    cancel toast.
  • Manual mm web smoke: refresh-on-cancel makes the new clean-file
    count visible without a manual reload.

Out of scope

Refs PR #802, PR #784, issue #785.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions