feat(rag): add explicit opt-in for ZIM files in the knowledge base - #1120
feat(rag): add explicit opt-in for ZIM files in the knowledge base#1120just-jbc wants to merge 67 commits into
Conversation
…utions#926) The map_markers backend has accepted a `notes` column since PR Crosstalk-Solutions#770 and the popup display path was wired up to render it (commit 6328256), but the placement UI never got an input. Result: notes are stored, displayed when present, and impossible to actually enter via the UI. Add a notes textarea below the name input in the placement popup, thread the value through `addMarker` and `createMapMarker`, and trim + null-coalesce on save. Notes display in the marker popup on click is unchanged and now actually reachable. - admin/inertia/lib/api.ts: extend createMapMarker request type with optional notes - admin/inertia/hooks/useMapMarkers.ts: addMarker accepts and forwards notes (response already populated notes into local state, so no display-side change needed) - admin/inertia/components/maps/MapComponent.tsx: markerNotes state, textarea after name input, threaded into handleSaveMarker Edit-mode for existing markers (so users can backfill notes on already-placed pins) is intentionally out of scope here - selected-marker popup is still read-only. That's a follow-up PR if there's demand.
…ion (Crosstalk-Solutions#1044) Closes Crosstalk-Solutions#902. Two gaps in the structured ZIM extraction path: 1. NON_CONTENT_HEADING_PATTERNS was only used by the structure heuristic to count meaningful sections, never at section-emit time. Sections under "See also" / "References" / "External links" / etc. were still chunked and embedded. They're now flagged when the heading opens and dropped. 2. <table> elements were run through cheerio's `.text()`, concatenating every cell with no separators ("AgeDoseAdult500mg") into unsearchable word salad. New tableToText() joins cells with " | " and rows with newlines so row/column structure survives into the chunk. Refactor: moved extractStructuredContent out of ZIMExtractionService into a pure, cheerio-only util (app/utils/zim_html.ts) so it can be unit-tested without the native @openzim/libzim binding. Service delegates to it; behavior is otherwise unchanged. Adds tests/unit/zim_html.spec.ts (6 tests). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…utions#1049) Meshtastic Daemon was pulled from DEFAULT_SERVICES because it can't work without hands-on setup (radio MAC address, etc.). The seeder never deletes, so every early-access deployment keeps an orphaned nomad_meshtasticd row and still shows the broken card. Migration mirrors the legacy-Kolibri sunset: drop the row where installed=0, flag is_deprecated where installed=1 (keeps it manageable, hides from catalog). Runs automatically on each box's next update. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d downloads (Crosstalk-Solutions#1059) Previously, failed downloads showed only an alert icon and a dismiss (X) button — the user had no way to retry or reach the resource page without manually re-adding the download and finding the source URL elsewhere. This commit adds: - POST /api/downloads/jobs/:jobId/retry endpoint (controller + service) - retryDownloadJob() API method on the frontend - Failed-state UI in ActiveDownloads.tsx now shows: - Retry button (re-dispatches the original download job) - 'Download page' external link (when the download URL is an HTTP(S) URL) - Loading state on the retry button while the request is in-flight - Screenshots documenting before/after UI Co-authored-by: eizus <hello@cdr.xyz>
) * feat: expandable rows in Kiwix Library browser (rebased onto dev) Click a row in the Content Explorer's 'Browse the Kiwix Library' table to expand it and reveal the full description (no longer truncated) along with additional metadata: author, publisher, language, category, article count, media count, issue date, file size, tags, and file name. Changes: - Extend RemoteZimFileEntry type with optional metadata fields (language, publisher, category, tags, article_count, media_count, issued) - Update zim_service.ts listRemote() to map these fields from the raw Kiwix API response in the paginated accumulator loop - Remove @tanstack/react-virtual virtualization from remote-explorer.tsx (12 items per page — virtualization not needed and incompatible with variable-height expanded rows) - Use StyledTable's built-in expandable prop with expandedRowRender to show full details when a row is clicked - Preserve upstream's custom libraries source selector and directory browser * fix(RemoteExplorer): guard against invalid dates being rendered --------- Co-authored-by: eizus <hello@cdr.xyz> Co-authored-by: jakeaturner <jturner@cosmistack.com>
Retire the dotted "N.O.M.A.D." styling everywhere in favor of "Project NOMAD" (no periods) ahead of the trademark filing, and remove the "Node for Offline Media, Archives, and Data" backronym from all copy except a single origin-story line in the About page (corrected to "Maps" rather than "Media"). Scope is display strings only: docs, admin UI labels/titles, install script output, Dockerfile labels, package.json description, and issue templates. Code identifiers, container/service names, env vars, CSS tokens, URLs, and the project-nomad slug are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wordmark application filed (USPTO serial 99912179). Mark the persistent Command Center footer wordmark (both the AppLayout footer and the settings/docs sidebar footer) and the first mention on the Legal Notices page. Browser-tab titles and body copy left unmarked. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Crosstalk-Solutions#1074) Ollama's scheduler drops integrated GPUs unless OLLAMA_IGPU_ENABLE=1 is set. NOMAD sets HSA_OVERRIDE_GFX_VERSION for AMD but never this flag, so AMD APUs (780M/890M/8060S) silently fell back to CPU-only inference despite correct /dev/kfd and /dev/dri passthrough. Set OLLAMA_IGPU_ENABLE=1 whenever AMD acceleration is configured, on both the install and update provisioning paths. The flag is a no-op on discrete AMD cards, so it's safe to set unconditionally within the AMD branch. On the update path we also strip any prior value so containers provisioned before this change pick up the flag on their next update. Verified on NOMAD2 (Ryzen AI 9 HX 370 / Radeon 890M): before, Ollama logged "dropping integrated GPU" and ran on CPU; after, it reports the 890M as an iGPU ROCm inference device and models load at 100% GPU. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…GPUs (Crosstalk-Solutions#1076) The current ollama:rocm image bundles ROCm 7.2, which natively supports the RDNA 3/3.5 mobile parts (780M gfx1103, 890M gfx1150, Strix Halo gfx1151). Forcing HSA_OVERRIDE_GFX_VERSION=11.0.0 coerces them onto gfx1100's kernels: unnecessary on the 890M and a source of faults on the 780M, which lacks the gfx1100 WMMA instructions those kernels use. - Map gfx1103/gfx1150/gfx1151 to no override (was 11.0.0) so ROCm discovers them natively. - Change the no-marker default from 11.0.0 to null. A hardcoded default gets more wrong as ROCm adds native targets; native discovery is the safer forward-looking default. Hardware that still needs coercion (e.g. RDNA 2 iGPUs on an install without the gfx marker) can force a value via the ai.amdHsaOverride KV. - Keep gfx1031..gfx1036 (RDNA 2 iGPUs like the 680M) on 10.3.0 — still not natively supported. Verified on NOMAD2 (Radeon 890M): after a Force Reinstall the container has no HSA_OVERRIDE, Ollama discovers the GPU as its native gfx1150 with the rocm_v7_2 libraries, and a model loads at 100% GPU with no faults. Depends on Crosstalk-Solutions#1056 context; see companion issue for the 780M follow-up. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…looping (Crosstalk-Solutions#1077) Homebox >= 0.26 (ghcr.io/sysadminsmedia/homebox:0.26.2) panics at boot unless HBOX_AUTH_API_KEY_PEPPER is set to a >= 32-byte value. The seeder ships no Env block, so every clean install crash-loops immediately. Generate a per-install pepper, persist it in the KV store (apps.homebox.apiKeyPepper), and inject it as HBOX_AUTH_API_KEY_PEPPER at every container-create path. The pepper is generated once and reused: rotating it would invalidate every API key a user has issued from Homebox. Inject at all three create paths so no lifecycle action drops it: - _createContainer (install / force-reinstall) - the service update path (heals a pre-fix container on update) - recreateCustomAppContainer (the Edit / reconfigure rebuild — the in-app docs tell users to add HBOX_OPTIONS_ALLOW_REGISTRATION=false via Manage > Edit, which rebuilds Env from container_config alone and would otherwise drop the pepper and re-trigger the crash loop) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…isconnect (Crosstalk-Solutions#1078) Two defects made chat hang forever with thinking-capable models on the OpenAI-compat (/v1) path, which NOMAD uses for both local and remote Ollama: 1. Field mismatch. chatStream()/chat() read `delta.thinking` / `message.thinking`, but Ollama's /v1 endpoint emits thinking tokens as `reasoning`. All thinking output was silently dropped, so the SSE stream was nothing but empty content+thinking chunks and never reached done. Now read `thinking ?? reasoning` in both paths (the inline <think>-tag parser for other backends is unchanged). 2. No abort on client disconnect. When the user gave up and closed the chat, the upstream generation kept decoding server-side. With Ollama's default OLLAMA_NUM_PARALLEL=1 that abandoned request occupied the only slot, so every later chat/RAG request queued behind it and the whole assistant appeared dead. The controller now wires an AbortController to the response 'close' event and threads the signal into the OpenAI SDK request, so a disconnect aborts the upstream generation. Verified on NOMAD2 (qwen3:0.6b, which reports the `thinking` capability and emits `reasoning` on /v1): before, the stream was endless empty chunks; after, thinking streams visibly and reaches done. On disconnect, Ollama's decode counter freezes and the server logs `cancel task` / `slot release`, freeing the slot for the next request. Note: thinking is still force-on for capable models here; a user-facing per-model thinking toggle (default off) is a planned follow-up. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
) Capture the frontend conventions so new UI (including community PRs) stays visually and behaviorally uniform with the rest of the GUI: reuse the shared component library (Switch, InfoTooltip, StyledModal, Input), match sibling label punctuation and typography tokens, use theme tokens for dark mode, keep tooltips from clipping, and test UI changes in a real browser before submitting. Notes when a raw control (checkbox, radio, native select) is still the right call. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ZIM/Kiwix files were previously auto-discovered and embedded into the AI knowledge base as soon as they existed on disk, with no way to exclude one without deleting it entirely. This made browsing content in Kiwix and including it in AI search the same decision, which isn't what users expect or consented to. - Stop auto-discovering ZIM_STORAGE_PATH during KB sync; ZIM files must be explicitly added via the Content Manager's new 'Add to Knowledge Base' action (embedSingleFile validates directly against disk instead). - Add a per-file active/inactive toggle (kb_ingest_state.active) so a file already indexed can be excluded from/re-included in search instantly, without re-embedding. - searchSimilarDocuments excludes inactive sources via a Qdrant filter. - Hard-deleting an indexed ZIM now also cleans up its kb_ingest_state row and Qdrant points, instead of leaving an orphaned KB entry behind. - Batch the kb_ingest_state lookup in ZimService.list() into a single query instead of one per file.
…scripts (Crosstalk-Solutions#1098) The uninstall and update scripts called header_red() and referenced color variables (GREEN/RED/RESET) that were never defined, so running them printed "header_red: command not found" instead of the section banners. Also corrects the update script's success message (it said "installation completed" and pointed at an undefined ${nomad_dir} path) and fixes two spelling typos. - uninstall_nomad.sh: add Color Codes block + header_red() - update_nomad.sh: add header_red(); fix success wording and start_nomad.sh path - CONTRIBUTING.md, admin/constants/ollama.ts: spelling fixes Reported by @fix2015 in Crosstalk-Solutions#1058; reimplemented in-house. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rosstalk-Solutions#1101) * fix(updater): prune superseded images after update to reclaim disk (Crosstalk-Solutions#858) The sidecar updater pulled new image versions on every update but never removed the old ones, so /var/lib/containerd grew unbounded across releases (50+ GB of orphaned layers observed on long-running installs). After a confirmed-successful recreate, prune (1) dangling layers left by re-pulled moving tags and (2) superseded tags of the core services this updater manages (the images in compose.yml), keeping the refs now in use. Deliberately avoids `docker system/image prune -a`: that would delete images for installed-but-stopped Supply Depot / curated services and force a re-pull that fails on an offline box. Scoped strictly to compose-managed repositories; optional/offline images are never touched. Uses `docker rmi` without -f so anything still referenced by a container is refused rather than force-removed. Best-effort; never fails the update. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(updater): scope image pruning to those directly used by NOMAD --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: jakeaturner <jturner@cosmistack.com>
…agnostics (Crosstalk-Solutions#1102) The Debug Info bundle carried no storage-path info, which stalled diagnosis of relocation issues like Crosstalk-Solutions#1050 (moved data not seen by the admin). Add the fields support actually asks for, all best-effort so one failure never blanks the bundle: - Storage: resolved host storage root (Crosstalk-Solutions#938), container path, whether NOMAD_STORAGE_PATH is set, and the Kiwix library book count (0 books is the tell for an empty/wrong-path library). - Docker Engine version (reporters currently paste it by hand; needed for container/updater issues). - GPU passthrough health (gpuHealth.status + detected gpu.type) for the passthrough-lost-after-update class (Crosstalk-Solutions#755/Crosstalk-Solutions#878). - Auto-update status for core/apps/content plus any auto-disabled reason, since the auto-update trilogy shipped. Adds a public DockerService.getHostStorageRoot() wrapper over the existing Crosstalk-Solutions#938 resolver. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nt (Crosstalk-Solutions#1103) The two compose comments contradicted each other on NOMAD_STORAGE_PATH (one called it optional/"explicit", the other "MUST match") and neither warned about the failure modes that actually break relocation. A user hit exactly this on Crosstalk-Solutions#1050: a case-mismatched path silently produced an empty Kiwix library. - Frame the admin /app/storage host path as the single source of truth; the admin auto-detects it (Crosstalk-Solutions#938) and child apps follow, so no per-service edits. - NOMAD_STORAGE_PATH is a fallback that should be kept in sync, not a hard requirement (reconciles the contradiction). - Add the real gotchas: move existing data first (keep zim/models subfolders), paths are case-sensitive, and update the disk-collector volume too or host disk stats point at the wrong place. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…osstalk-Solutions#1110) Step 1: remove the "Additional Tools" (Notes/Data Tools) section from onboarding and point users to Supply Depot, the browsable app catalog, for everything beyond the three core capabilities. Step 2: add a note that individual countries and a full global map can be installed any time from the Maps Manager. Step 4: default the KB auto-index policy to "Ask me first" (Manual) instead of "Yes, always" — auto-indexing has cost/resource implications a non-technical user won't anticipate from the toggle alone. ollama_service: the recommended-models fallback only fired on a null result, so a successful-but-empty upstream response (models: []) showed "No recommended AI models available" and poisoned the 24h cache. Now the fallback fires on empty too, empty results are never cached, empty caches are ignored, and the upstream request has a 10s timeout. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…talk-Solutions#1112) Surface post-upgrade release highlights on the Command Center: a dismissable banner keyed to the running build's release line (major.minor). It appears when a user upgrades into the line and every patch within it, then disappears on the next minor. Dismissal is remembered per-line in localStorage, so dismissing v1.34 won't suppress a future v1.35 note. Reuses the existing dismissable Alert component and the dashboard's top banner slot; reads the running version from the appVersion shared prop. No schema or migration needed — to surface a new release's highlights, bump WHATS_NEW.version and replace its highlights. Initial content: Creator Packs and the offline Medication Reference. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n't 403 (Crosstalk-Solutions#1114) download.kiwix.org routes the large Wikimedia-family ZIMs (Wikipedia, Wikiversity, Wikibooks — including the flagship full Wikipedia) to dumps.wikimedia.org, which enforces a User-Agent policy and returns HTTP 403 for requests with a missing or generic (axios/x) User-Agent. Because doResumableDownload sent no User-Agent, every Wikimedia-hosted ZIM failed to download while Kiwix-mirror-hosted ZIMs succeeded — so a curated set or Easy Setup run would silently stall on exactly the highest-value content. Add a descriptive User-Agent to the HEAD and GET requests. Verified against the live mirror: default/empty UA -> 403, ProjectNOMAD UA -> 200/206. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ne ghost entries (Crosstalk-Solutions#1099) The Content Explorer remote list already filters out installed ZIMs client-side (flatData excludes localNames), but the installed-files query has refetchOnWindowFocus disabled and was never invalidated on download completion. So when a ZIM finished downloading it dropped off the active-downloads list (isDownloading -> false) while localFiles stayed stale (isPresent -> false), letting the just-installed ZIM reappear as a ghost entry until the page remounted. Add an effect that invalidates the ['zim-files'] query whenever a job drops off the polled downloads list, so the completed install is picked up and pruned. Fix originally diagnosed by @johno10661 in Crosstalk-Solutions#771; reimplemented focused onto current dev (the accumulated-page/filter groundwork already landed via Crosstalk-Solutions#731). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tions#1082) (Crosstalk-Solutions#1084) Phase 1 of the live benchmark-run experience. Replaces the opaque (and in sync mode, simulated) progress bar with a real-time run view driven by actual host telemetry. - Async run path: the UI now dispatches to the queue worker and keys off SSE instead of faking stage progress with client-side timers. - New BenchmarkTelemetrySampler broadcasts per-core CPU load, CPU temp (best-effort, hidden when unavailable), and disk MB/s at 1 Hz over a new benchmark-telemetry SSE channel. Runs in the orchestration process, never the sysbench container, so it cannot affect scores. - BenchmarkProgress carries the ordered stage plan + index so the frontend renders a live stage rail. - AI benchmark streams /api/generate for live tokens/sec and true TTFT; the scored numbers still come from Ollama's authoritative final eval fields. - Frontend: useBenchmarkRun hook owns both subscriptions; self-contained SVG components (StageRail, CoreGrid, Sparkline, LiveReadout) + BenchmarkRunView, styled in the desert palette. No chart library added. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ip (Crosstalk-Solutions#1085) Phase 2 of the live benchmark run experience. Streams sysbench --report-interval=1 interim lines over a Docker attach so the run view shows authoritative in-test CPU events/sec and disk read/write MiB/s (overlaying the Phase 1 host-proxy disk numbers), and fills a "results so far" strip as each stage completes. - _runSysbenchCommandStreaming: attaches to the container output for live onLine callbacks, but returns the authoritative output via container.logs() after exit -- byte-identical to _runSysbenchCommand, so the SCORED numbers are unchanged (attach 'data' can flush after container.wait() resolves, which would truncate the final report). - CPU + disk stages stream interim eps: / reads:/writes: MiB/s into setStageMetric; memory stays non-streaming (too fast to sample). - _emitPartialResult broadcasts each finished stage's raw result on the progress channel; useBenchmarkRun accumulates them; ResultsSoFar renders the chip strip. - Frontend: live CPU ev/s readout+sparkline, disk hero switches to "Benchmark throughput" when in-test numbers arrive. Browser-validated on NOMAD3 (System-Only): CPU 6536 ev/s live, disk 10399 MB/s benchmark throughput, results strip, final score 68.3. Part of Crosstalk-Solutions#1082 (tracker stays open for Phase 3). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rosstalk-Solutions#1087) Phase 3 of the live benchmark run experience. - ScoreReveal: replaces the abrupt run-view unmount with a deliberate end-of-run "REPORT" card -- animated NOMAD score gauge + odometer count-up number, sub-score gauges cascading in, Continue button + 5s auto-dismiss. Takes score scale as a prop so it survives Score v2. - GPU-util overlay (NVIDIA): during the AI stage, a ~1Hz nvidia-smi poll inside the Ollama container feeds live GPU utilization + VRAM into the telemetry frames; shown in the AI hero, hidden when absent (AMD/none). Poller is side-effect-only and cleared in a finally -- scored numbers unchanged. - Disk polish: reset in-test buffers on stage transition so the write stage no longer briefly shows the carried disk-read value. Browser-validated on NOMAD3 (RTX 5060): GPU overlay live (1% util, 0.2/8.0 GB VRAM during model load); reveal cascade + count-up; AI-only score 39.8 (scored path unchanged). Part of Crosstalk-Solutions#1082 (tracker stays open). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…malize) (Crosstalk-Solutions#1088) A System-Only or AI-Only run is a partial result, not the NOMAD Score (which is the full-benchmark composite). Two problems addressed: 1. Scoring bug: AI-only runs were NOT renormalized. _calculateNomadScore always added the system weights (0.60) to the denominator even for an AI-only run (default-zero system scores), so an excellent AI-only run scored ~39.8 -- scaled against the full NOMAD 100 where AI is only 40% -- while system-only already renormalized correctly. Fix: pass systemScores only when the system benchmarks actually ran, so AI-only renormalizes to its own 0-100 (39.8 -> ~99.7). Full and System-only scores are unchanged. 2. Presentation: partial runs were shown with the full "NOMAD Score" label + big gauge, outweighing the small "Partial" notice. Now partial runs are relabelled "System Score" / "AI Score" with a PARTIAL badge, a muted (neutral) gauge + number, and a "run a Full Benchmark for your NOMAD Score" CTA -- applied to both the persistent score section and the Phase 3 ScoreReveal via a shared getScoreDisplay() helper. Adds a `muted` prop to CircularGauge. Browser-validated on NOMAD3: AI-only now shows "AI Score" + PARTIAL, muted gauge, 99.7 (was 39.8); Full still shows "NOMAD Score" green. Implements the display-layer fix from the Score v2 red-team's W2; the AI-ceiling saturation (W1) remains v2 work. Part of Crosstalk-Solutions#1082. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ord provenance (Crosstalk-Solutions#1089) Score v2 Phase 1. Safe under v1 — no scoring, weight, reference, or submission-payload changes; only the benchmark's failure behavior and forensic metadata. - Fail-on-parse-miss (W3): the four SCORED sysbench metrics (CPU events/sec, memory ops/sec, disk read/write MiB/s) now THROW when the regex misses or the value is <= 0, instead of silently returning 0. A parse failure (e.g. an upstream image output-format change) now fails the run with a clear error via the existing _runBenchmark try/catch, rather than submitting a phantom zero sub-score. Secondary/ informational fields keep their existing defaults. - Pin sysbench by digest (W3): severalnines/sysbench@sha256:64cd003b... (was :latest), so a latest-tag format change can't break the parsers fleet-wide. Digest validated on the NOMAD6 reference build. - Record provenance (W7): sysbench_digest + ollama_version (from Ollama /api/version, null-tolerant) stored on each result. New nullable columns + additive migration. Verified: pinned digest pulls/runs/parses on NOMAD3; migration applies (columns present); typecheck clean. Part of the NOMAD Score v2 effort. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sstalk-Solutions#1063) * feat(rag): add subject/collection organization to knowledge base - Add nullable collection field to KbIngestState, propagated through the embed job, RAG service, and Qdrant point payloads (indexed for filtering) - Add upload-time category selection and per-file collection reassignment in the Knowledge Base modal, with a filterable Stored Files table - Add a 'Search in' collection filter to the chat interface, threaded through to searchSimilarDocuments as an optional Qdrant filter - Fix .docx extraction: previously routed through raw-text extraction (garbage output for a ZIP-based XML format); adds a proper mammoth-based extractor and a dedicated 'docx' file-type case * feat(rag): support dynamic KB collection creation, rename, and removal Extends collection organization with a Manage Collections UI: collections are created on the fly when a file is assigned to a new name, can be renamed (bulk-updates every tagged file and Qdrant point), and can be removed (reassigns tagged files back to Uncategorized rather than deleting anything). * fix(rag): use dynamic collections query in chat search filter chat/index.tsx still imported the static KB_COLLECTIONS constant for its 'Search in' dropdown, inconsistent with KnowledgeBaseModal.tsx which already uses the live getKnowledgeCollections() query. Renamed/added collections via the new Manage Collections UI weren't reflected in the chat filter. * feat(rag): broaden preset tags and add creatable collection combobox Replaces the survival-specific preset list with general-purpose starter tags (recipes, diy, health, technology, finance, travel, hobbies, reference, survival, energy) so the Knowledge Base reads well for home-lab/reference use, not just prepping. Adds sanitizeCollectionName() (trim, lowercase, length cap) applied on every write path server-side, and a dependency-free CollectionCombobox component replacing the plain <select> + window.prompt pattern for tagging — autocompletes against presets + tags already in use, with a '+ Create' option for anything new. * chore(rag): remove .docx fix from this branch, split into Crosstalk-Solutions#1100 Per review feedback, the .docx extraction fix is unrelated to the collections feature and can merge independently. Moved to a standalone PR (Crosstalk-Solutions#1100) off dev. * chore: remove unrelated diff noise (lockfile, comments, indentation) --------- Co-authored-by: John Cortright <jcortright@zscaler.com>
…utions#1100) * fix(rag): add proper .docx text extraction via mammoth .docx files were classified as plain text and routed through raw-text extraction (extractTXTText). Since .docx is a ZIP archive containing XML, this produced garbage content in the Knowledge Base — XML tags and binary noise instead of the actual document text. Adds a dedicated 'docx' file type (split out of the generic 'text' bucket in determineFileType) and a mammoth-based extractor that parses the document XML properly. * fix: clean up package-lock.json diff * chore(deps): pin mammoth version --------- Co-authored-by: John Cortright <jcortright@zscaler.com> Co-authored-by: jakeaturner <jturner@cosmistack.com>
* feat(AI): nomad.md for custom instructions * fix(UI): broken HTML tag close
… curated data (Crosstalk-Solutions#1130) Generate app/data/{conditions,natural_remedies,home_remedies}.ts from the repo-root collections/*.json via `npm run gen:curated-data`, so the JSON is the only file edited by hand. The generated modules keep the data compiled into the image (no runtime file read, no path fragility, which is why the data was a TS constant), and curated_data_sync.standalone.ts fails CI if a generated module ever drifts from its JSON. This removes the burden of hand-keeping the .json mirror and the .ts constant in sync. Follow-up to the review discussion on Crosstalk-Solutions#1040. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… GPU (Crosstalk-Solutions#1134) PR Crosstalk-Solutions#1076 stopped forcing HSA_OVERRIDE_GFX_VERSION=11.0.0 on "natively-supported" AMD iGPUs. That was correct for gfx1150/gfx1151 (Strix 890M / Strix Halo, which are in the bundled rocblas allowlist) but wrong for gfx1103 (Phoenix/Hawk Point 780M/760M), which is NOT in that list. Without the override, ollama drops the 780M with "no rocblas support for gfx target" and falls back to CPU on a fresh AI provision. Extract the gfx→HSA mapping into a pure, unit-tested util and map gfx1103 → 11.0.0 (gfx1100 kernels), the value that worked on v1.33.0 and that restores full GPU offload in the field. gfx1150/1151 stay native. Also harden the installer's 780M detection (Hawk Point / "Radeon 780M/ 760M" strings) so the gfx marker isn't silently deleted, and upgrade the no-marker fallback log from info to warn since it can mask CPU fallback.
…Crosstalk-Solutions#1135) _ensureCollection() runs once per document on the embed path, but only createCollection sat behind the collectionExists guard — the getCollections probe and the three createPayloadIndex calls fired unconditionally every time. On large ZIM ingestions those redundant requests consumed roughly 45% of per-document Qdrant time, making jobs look stalled while they were slowly progressing. Memoize ensured collections in a per-instance Set, recorded only after every step succeeds so partial failures retry. The cache is cleared when the Qdrant health check resets the client (server may have been recreated), and the entry is dropped before resetAndRebuild() recreates the collection it just deleted. Memoizing instead of moving the index calls inside the guard keeps missing indexes healing on collections that predate the current payload schema. Closes Crosstalk-Solutions#1129 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ct situations, and a disclaimer gate (Crosstalk-Solutions#1137) * feat(drug-reference): compact header + single dashboard tile Phase 1 of the drug-reference redesign: - AppLayout gains an opt-in `compact` prop (small inline logo+title) so tool pages reclaim the ~230px the full branding block costs; drug-reference/index opts in. - Consolidate the two dashboard tiles (Drug Reference + When to use what) into a single Drug Reference tile with a broadened description (/conditions already redirects to /drug-reference). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(drug-reference): tabbed redesign — search-by-drug + by-situation + FDA data - Split /drug-reference into three tabs (Headless UI TabGroup): 'Search by drug', 'By situation', 'FDA data'. Each tab runs only its own direction, which removes the two-overlapping-sections confusion. - Search by drug: results grouped by active ingredient (IngredientGroup, single-ingredient groups first, combos after), drug-first result rows, and a collapsible de-jargoned filter drawer (Over-the-counter / Prescription, Form, Sort) that auto-collapses once results land. - By situation: multi-select symptom chips → an 'Treats all N selected' intersection section pinned on top (computed client-side from each situation's result set) + one union group per situation. - FDA data: the download/ingest control + status moved behind its own tab (the pre-ingest empty state stays the prominent download prompt). - DrugResultRow now leads with the active ingredient (drug-first) by default, or the brand when rendered inside an ingredient group. - Rename 'Compare interactions' → 'Compare label warnings' to match what it does. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(drug-reference): rank situation matches single-ingredient-first The raw FDA indication match floods a situation (e.g. Headache) with many-ingredient homeopathic products, burying real OTC drugs and leaving the cross-situation intersection empty. Pull a wider result set (200) and sort by active-ingredient count ascending — not a medical judgement, the same 'single-ingredient first' principle as the drug-search grouping. Now real drugs (acetaminophen, ibuprofen) surface on top and the 'Treats all N selected' intersection actually finds the shared OTC options. Per-situation cards cap the display to the top 25 (ranked), intersection uses the full set. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(drug-reference): first-open disclaimer gate + first-group-expanded - Add a required disclaimer modal on first open of the Drug Reference (Jake's mechanism): comprehensive not-medical-advice notice the user must acknowledge; acceptance is saved to the browser's localStorage (versioned key) so it isn't shown again on that browser, while new browsers/devices see it on first open. Non-dismissible (no backdrop/Escape) — only the acknowledge button closes it. - Search-by-drug: expand the first ingredient group by default, collapse all subsequent groups (IngredientGroup gains an explicit defaultOpen prop, replacing the size heuristic). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(drug-reference): intersection-first multi-select Multiple situations now lead with the 'Treats all N selected' intersection and only break out per-situation sections when the intersection is empty (nothing treats all) — with a 'No single option treats all N of these' explainer. Keeps the view combined when there's a shared answer, and only fragments as a fallback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(drug-reference): compact header on detail/interactions/conditions pages Apply the compact AppLayout header to the drug detail, interactions, and condition pages so they match the redesigned index instead of the full-height branding block. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(drug-reference): rename interactions heading to 'Compare label warnings' Match the page heading + title to the button label, so the name reflects what the view does (each drug's own FDA-labeled warnings side by side, not a cross-drug interaction checker). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ils (Crosstalk-Solutions#1138) The submit flow discarded the real failure reason and always returned the generic "Failed to submit benchmark results." to the UI. The most common cause is the leaderboard's one-per-hour rate limit (HTTP 429), which left users with no idea why their submission failed or that retrying shortly would also fail. - benchmark_controller: return a clear, actionable message. Name the rate limit explicitly on 429; otherwise pass through the underlying detail (repository error or a service validation message like "already submitted"), falling back to the generic only when we have nothing. - benchmark_service: attach the raw upstream `detail` to the thrown error so the controller can surface it. The frontend already renders the server `error` string, so no client change is needed.
…scores (Crosstalk-Solutions#1140) The AI benchmark evicted resident models (forcing the benchmark model cold) and then went straight into the timed median-of-N loop with no warm-up. On a cold box the model-load + GPU spin-up cost landed inside the timed runs (observed: 173s TTFT / 5.83 tok/s vs a ~80 tok/s warm steady-state), and consecutive runs weren't isolated (a prior run left the model warm). Because the AI channel is uncapped and ~30% of the composite, the same machine could post a ~2x-different NOMAD Score depending on warm/cold state (888 vs 1958 observed back-to-back). Add one discarded warm-up inference after eviction and before the timed loop so every timed run measures warm, steady-state throughput. Cold and warm invocations now converge on the same score. Best-effort: a warm-up hiccup never fails the run. Closes Crosstalk-Solutions#1139
…ions#1141) Validated Project NOMAD on Ubuntu 26.04 LTS across two builds (an AMD/CPU box and an NVIDIA RTX 5060 box), so recommend 26.04 as the default going forward. 24.04 LTS and Debian 12 remain supported. - bug_report.yml: add Ubuntu 26.04 (Resolute Raccoon) to the OS dropdown - README / CONTRIBUTING / FAQ: name Ubuntu 26.04 LTS as the recommended version - getting-started.md: correct the GPU note. NOMAD's installer sets up the NVIDIA Container Toolkit and Docker runtime automatically; the user only needs the NVIDIA driver (via "Install third-party drivers" at OS setup)
Mirror of the main-branch catalog addition so dev carries the Modern Rogue pack and it isn't dropped on the next dev->main release merge. ZIM is uploaded to R2 and verified serveable via the entitlement Worker. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Solutions#1142) The MeshCore Web app doc listed meshcore.co.uk as the official site. The canonical MeshCore project (github.com/meshcore-dev/MeshCore, the firmware + protocol) declares its homepage as meshcore.io, so update the two references in the MeshCore Web section to point there. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-Solutions#1147) The modern-rogue pack entry landed in collections/creator-packs.json (Crosstalk-Solutions#1145/Crosstalk-Solutions#1146) but its banner image was never committed, so the app fell back to /creator-packs/modern-rogue.webp which 404s and the card rendered without a header. Adds the 1060x175 banner (RGB webp), matching the existing project-nomad.webp and crosstalk-solutions.webp, generated from the pack's source banner.png built on the pack-build workspace. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… host (Crosstalk-Solutions#1157) DockerService.getServiceURL() resolves ai.remoteOllamaUrl ahead of the local container, so when a remote AI host is configured the AI channel measures THAT machine while every other channel measures this one. The submission then reports someone else's tok/s under this hardware's CPU, RAM and disk. Under v2 this matters more than it did under v1: ai_tokens_per_second carries 0.30 of the weight and the score is uncapped, so a remote GPU's throughput is no longer limited by a clamp. Adds a guard alongside the existing submit-time checks. Uses the same truthiness predicate as getServiceURL, so the guard fires exactly when the remote routing it is guarding against would occur. KVStore.clearValue() nulls the value and getValue() returns null for that, so a cleared key correctly does not trip it. Blocks submission only. Running the benchmark locally is still useful to the operator — it just isn't a result about this box, so it shouldn't go on a board that ranks hardware. Known limitation: the check reads the KV at submit time, not at measurement time, so benchmarking with a remote host and then clearing the setting before submitting would still get through. That is a deliberate workaround rather than an accident, and closing it properly needs a column on benchmark_results to record how inference was reached. Worth doing if it ever shows up in practice; not worth a migration on the evidence available. Refs Crosstalk-Solutions#1151
…orm metadata (Crosstalk-Solutions#1158) Three changes that together let ARM hardware appear on the leaderboard honestly. Shipping them separately would leave ARM half-supported either way: without the image a Pi cannot submit at all, and without the architecture field it submits but is indistinguishable from x86. 1. PIN THE OFFICIAL MULTI-ARCH SYSBENCH IMAGE severalnines/sysbench publishes amd64 only, so ARM hosts could not run the System Benchmark at all — not a graceful failure, the container simply cannot execute. Apple Silicon could only run it under Rosetta emulation, which distorts the measurement it is taking, and that is what drove a community macOS fork to substitute a different benchmark and submit incomparable numbers. Swaps to ghcr.io/crosstalk-solutions/nomad-sysbench (Debian 12 + sysbench 1.0.20+ds-5, built for linux/amd64 + linux/arm64). One digest covers both architectures; verified that pulling the pinned manifest-list digest resolves to arm64 on a Raspberry Pi 5 and amd64 on x86, and that RepoDigests reports the same manifest-list digest on both — so a single allowlist entry serves both. No rescoring: 1.0.17 -> 1.0.20 measured 1.25% apart on identical hardware with identical flags (7170.18 vs 7259.56 events/sec), inside run-to-run noise and ~0.3% on a composite. Both digests are allowlisted server-side, so the fleet can cross over gradually. 2. REPORT THE DIGEST ACTUALLY RESOLVED The submission previously sent SYSBENCH_DIGEST, the constant the client was compiled with. The leaderboard validates that field, but a constant attests to how a client was BUILT rather than what it RAN, so any build inherits a valid value simply by carrying the same source. Now reads it back from the image. Uses RepoDigests (the manifest digest we pulled by), never Id — Id is the config digest, differs per architecture, and would never match the allowlist. Falls back to the constant if inspection yields nothing usable, so a benchmark never fails over provenance metadata. Still forgeable, and always will be with an open-source client. It moves the bar from "no effort" to "deliberate", which is the distinction that matters when judging whether a submission is a mistake or a choice. 3. RECORD CPU ARCHITECTURE AND OS The leaderboard is a single board across instruction sets by design, with disclosure as the fairness mechanism. Without an architecture field an ARM result sits unlabelled beside x86 — exactly what the disclosure exists to prevent. All three fields come from the Docker daemon, reusing the docker.info() call _detectRunEnvironment already makes. That is deliberate: inside the admin container os.arch() and si.osInfo() describe the CONTAINER, not the host being benchmarked. cpu_architecture Architecture x86_64 -> amd64, aarch64 -> arm64 os_version OSVersion '24.04' (already structured, no parsing) os_name OperatingSystem 'Ubuntu 24.04.4 LTS' minus the version run_environment is kept rather than replaced: "which distro" and "is this virtualised" are different questions, and WSL2 is a real performance factor. String handling lives in app/utils/platform_metadata.ts with unit tests, matching the amd_hsa_override convention, so it is testable without a Docker daemon. Unknown architectures pass through verbatim rather than being guessed at, and os_name falls back to the full description whenever the version is missing or absent from it — an over-long name is harmless, a wrong one is not. Columns are nullable and the submission fields optional, so results recorded before this shipped remain submittable. Closes Crosstalk-Solutions#1156 Refs Crosstalk-Solutions#1151
|
Resolved a merge conflict with Rebuilt and re-tested locally after the merge — still working as described above. Ready for review whenever you get a chance. |
|
Merged latest dev (b4e4c64) to pull in the collections feature and OpenAPI generator changes, resolving conflicts in rag_service.ts (Qdrant filter logic) and routes.ts (route registration). After merging: npm run typecheck passes with 0 errors |
Closes #1119
Summary
ZIM/Kiwix files were previously auto-discovered and embedded into the AI knowledge base as soon as they existed on disk, with no way to exclude one without deleting it entirely. This decoupled the two decisions "browse this in Kiwix" and "let the AI search this," which weren't the same thing.
ZIM_STORAGE_PATHduring KB sync — ZIM files must now be explicitly added via the Content Manager's new "Add to Knowledge Base" action.kb_ingest_state.active) so an already-indexed file can be excluded from/re-included in search instantly, with no re-embedding.searchSimilarDocuments()excludes inactive sources via a Qdrantmust_notfilter.kb_ingest_staterow and Qdrant points, instead of leaving an orphaned KB entry behind.kb_ingest_statelookup inZimService.list()into a single query instead of one per file.Testing
Tested manually against a running NOMAD instance:
kb_ingest_staterow (verified via direct DB query).npm run typecheckpasses.UI
Content Manager's ZIM listing gets a new "Knowledge Base" column: an "Add to Knowledge Base" button for not-yet-added files, and the shared
Switchcomponent for the toggle once added — per the UI Consistency guidelines in CONTRIBUTING.md.