Files
880c80f52f feat(kb): per-file active toggle + collection grouping/bulk toggle (#1286)
* feat(kb): add per-file active/inactive toggle for search inclusion

Implements the per-file toggle agreed on in #1119, in place of disabling
ZIM auto-discovery by default. Any indexed file (ZIM, upload, or other)
can now be excluded from RAG search results instantly, without deleting
or re-embedding its vectors.

- Vectors stay in Qdrant permanently; each point's payload carries an
  active boolean. Toggling flips this payload in place, both directions,
  with no re-embedding cost.
- Search filter is a denylist (must_not: active == false), not an
  allowlist, so a point missing the field entirely is still found by
  default -- search correctness never depends on backfill timing.
- embedAndStoreText() looks up a file's existing active state before
  writing new points, so retries, force re-embeds, and replaced-content
  reindexing never silently reactivate a file the user explicitly
  toggled off.
- Applies uniformly to all non-admin_docs buckets (ZIM, uploads, other),
  matching how the rest of this pipeline already treats buckets, rather
  than special-casing ZIM.

New POST /api/rag/files/active endpoint and KB panel toggle switch,
following the existing updateFileCollection pattern.

Real vector deletion for genuinely deleted/replaced files remains
#1170's separate concern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(kb): group personal KB uploads by collection with bulk toggle

Groups personal KB uploads into collapsible per-collection header
rows (with an "Uncategorized" bucket for files with no collection) to
reduce clutter in the file table. Each collection header also exposes
a bulk active/inactive toggle to flip every file in that collection at
once.

Also fixes three pre-existing bugs found in the collection-assignment
dropdown while building this: the popover was clipped by table cell
truncation, clipped again by the table wrapper's overflow, and the
combobox query was stale-filtering results on open.

Refs #1193

* fix(kb): widen the modal for the Active column and coerce active to a boolean

Both found by browser-verifying on NOMAD3.

The Active column adds ~57px, which put the table back over the modal width and
reintroduced the exact defect #1198 fixed: the Delete button falls off the right
edge at every viewport. Measured on the box at 1018px of table against 961px of
usable width at max-w-5xl. Widened to 6xl.

MySQL returns tinyint(1) as 0/1 rather than a boolean, and the raw 1 reached the
DOM as aria-checked="1". That is not a valid ARIA value, so a screen reader is
told nothing useful about whether the toggle is on. Coerced in the service so
every consumer sees a real boolean, with a !! guard in Switch as well.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8Mgg1vd2eKSs8StZiuyMV

* fix(kb): validate the bulk collection toggle's input before sanitising

sanitizeCollectionName ran before the type check, so a form-encoded request
sending collection[] as an array reached .trim() on a non-string and produced
an uncaught TypeError, surfacing as a 500. The vision controller already
guards this the same way.

Also documents why requiring `active` to be a real boolean is load-bearing
rather than mere input hygiene: a cross-site HTML form can only send the string
"true", so that check is what keeps a bulk Qdrant write across every point in
the index out of reach of a form POST from another origin. Worth a comment,
because coercing it would look like a harmless usability fix.

Found in a security review of the release PRs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8Mgg1vd2eKSs8StZiuyMV

---------

Co-authored-by: John Cortright <jcortright@zscaler.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 15:54:41 -07:00
..