Commit Graph

38 Commits

Author SHA1 Message Date
caweis 77c36b3fef
Add offline FDA drug reference (labels, interaction view, conditions, remedies) (#1040)
* feat(drug-reference): offline FDA drug labels, conditions, and remedies

Adds an offline medical-reference feature with three coupled layers:

- Drug Reference: full-text search over openFDA drug-label indications,
  a detail page per label, and a side-by-side single-drug comparison view.
  A two-phase background pipeline downloads the openFDA label parts to the
  storage volume (resumable) and ingests them into the search table.
- Conditions ("When to use what"): a curated spine of first-aid situations
  that maps each situation to matching OTC drugs, each linking back to its
  Drug Reference detail page.
- Curated remedies: hand-authored natural and home-remedy entries drawn from
  US-government public-domain sources (NCCIH, CDC, MedlinePlus, FDA), shown
  with their source links and the same safety disclaimers as the rest of the
  feature.

The drug-reference and conditions layers are intentionally coupled: the drug
detail page shows the situations a drug treats, and the conditions controller
reads the same drug_labels table.

Safety surfaces ship as written. The amber SafetyBanner ("informational only,
not medical advice, not an FDA endorsement, not a drug-interaction checker, in
an emergency call emergency services") renders on the condition pages and the
search page; the detail and comparison pages carry their own "not a cross-drug
interaction checker" callout; and every page carries the openFDA CC0 source
citation and no-FDA-affiliation footer.

Wiring on this branch:
- start/routes.ts: the /drug-reference and /conditions page GETs plus their
  /api/* groups.
- commands/queue/work.ts: the drug-download and drug-ingest queues, both at
  concurrency 1. The two drug queues get a per-queue stall override
  (lockDuration 1_800_000, maxStalledCount 3) because each part is one long
  stream; every other queue keeps the existing 300000 default.
- inertia/pages/home.tsx: Drug Reference and "When to use what" tiles. The
  icon and display_order are a starting point, open to change.
- types/kv_store.ts: the two drugReference.* keys the pipeline reads and writes.
- package.json: yauzl and stream-json (plus their @types), used by the ingest
  job to stream the label JSON out of the downloaded zips.

The app reads the conditions and remedy data from the compiled TS constants in
app/data/; the repo-root collections/*.json files are the browseable mirrors.
The natural-remedies standalone test reads collections/natural_remedies.json to
assert the two stay in sync, so that file is also a test fixture.

The four standalone tests pass (drug_interactions, drug_ingest_status,
conditions, natural_remedies). tsc reports no errors in the feature code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(drug-reference): drop a fork-internal comment reference

* fix(drug-reference): address review — defer FULLTEXT, fallback remedies prop, strip fork refs

- migration: wrap the FULLTEXT ALTER in this.defer so it runs after the
  deferred createTable (was silently swallowed, index never created)
- controller: add remedies:[] to the index() error fallback (required prop)
- strip fork-internal issue/spec references from ported comments
- tsconfig: exclude tests/standalone (node --experimental-strip-types only)
- correct the varchar(768) byte-math comment; extend remedy-spine test

* fix(drug-reference): make the interaction comparison readable at five drugs

The comparison view laid its columns out on an equal-fraction CSS grid
(repeat(N, minmax(0, 1fr))), so each added drug shrank every column; at the
five-drug maximum the FDA interaction text was squeezed into unreadable slivers.

Lay the columns out with flex instead: full-width and stacked on phones, then
fixed-width columns that scroll sideways from the sm: breakpoint up, so they
never shrink below a readable width. Theme the columns with the same palette as
the rest of the page (they were on stock gray), and give the headers a fixed
min-height so columns line up when drug names wrap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(collections): route a 'dataset' tier resource to the drug pipeline

Add an optional `type` discriminator to SpecResource ('zim' | 'dataset',
absent == 'zim'), so the tier installer can carry a DB-ingested resource
alongside ZIM files. ZimService.downloadCategoryTier branches on it: a
'dataset' resource dispatches the existing FDA download+ingest pipeline
instead of RunDownloadJob, guarded against duplicate dispatch by the drug
ingest status. Every existing manifest entry has no `type` and keeps the
exact ZIM path.

Widen InstalledResource.resource_type to include 'dataset' and exclude
dataset rows from the ZIM/map catalog-update scan (datasets aren't
filename-versioned; their freshness path is separate). No dataset rows are
written yet: the InstalledResource 'dataset' row on ingest-ready, the
manifest entry, install-gating, and the downloads-aggregator integration
are follow-up commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(drug-reference): rework into an opt-in medicine-standard tier dataset

Reshapes the offline FDA drug reference from a built-in feature into curated
content installed by selecting the Medicine / Standard tier, per maintainer
direction.

- install-state: the ingest writes an installed_resources 'dataset' row on
  ready (version = the openFDA export_date), threaded installer to download to
  ingest; the tier-status math and the home-tile gate read it. Manual ingests
  write no row, so install-state stays tied to the curated path.
- manifest: declare the dataset in the medicine-standard tier (runtime fetches
  the remote manifest, so this also needs to land upstream).
- install-gating: the drug-reference home tiles render only when installed.
- uninstall: DrugReferenceService.uninstall() stops the two drug queues, deletes
  the on-disk parts, truncates drug_labels (schema kept), clears the KV markers,
  and drops the install row. Best-effort, logged, scoped to drug data only.
- downloads: the download phase reports the canonical {percent, downloadedBytes,
  totalBytes} shape as one drug-data card in the Active Downloads aggregator with
  cancel/remove; the heavy ingest stays in the IngestStatus surface with an
  Indexing handoff on the card.
- auto-update: a daily DrugAutoUpdateJob compares the manifest export_date and
  re-downloads when newer, gated on installed + no active job.

typecheck clean; the drug standalone suites pass. Three points are flagged in
code for the maintainer: the InstalledResource 'dataset' approach, the tier
home, and the export_date string format.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(downloads): keep the drug download card live across all parts

The Active Downloads card filtered to the deterministic jobId, but the
download's continuations run under auto-generated jobIds (only part 0 uses the
deterministic one). So the card tracked part 0 and then vanished while parts
2..N kept downloading. The queue is concurrency 1, so collapse to whichever
single part is in flight and report the deterministic jobId: one card tracks
aggregate progress through the whole download and cancel/remove still routes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(drug-reference): co-locate a persistent safety note with affirmative remedy guidance

Add RemedySafetyNote at the head of every natural-remedy section — the two on
Drug Reference and the one on "When to use what" — so the "informational only,
not medical advice, seek real medical care in an emergency" framing appears with
the guidance itself, not only in the page-top banner. Replaces the terse
per-section caveat with the same amber alert language as SafetyBanner.

Addresses the upstream #1040 review request that the disclaimer be unmistakable
and present wherever affirmative self-care guidance appears, not a one-time banner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(drug-reference): fold dataset freshness into the content-auto-update path

The drug dataset auto-updated on its own daily cron that ignored the
contentAutoUpdate.* master switch, so it would refresh even with content
auto-update turned off, and it didn't ride the content-update path the way
ZIMs and maps do.

Move the export_date freshness/apply orchestration onto
DrugReferenceService.attemptAutoUpdate(), add
ContentAutoUpdateService.attemptDrugDataset() gated on the same enabled +
window config, and have the hourly ContentAutoUpdateJob drive both. Retire
the standalone DrugAutoUpdateJob. The ZIM/map attempt() path is unchanged.

Addresses the upstream #1040 request to wire the openFDA export_date check
into the content updater so the dataset updates alongside ZIMs and maps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(drug-reference): strengthen the remedy note to consult a clinician before combining with meds

Widen the affirmative-remedy safety note from "talk to a clinician before use"
to explicitly cover using a remedy AND combining one with a medication the user
already takes — the interaction case is the higher-risk path for an off-grid
user self-treating.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(drug-reference): gate affirmative remedy content behind an off-by-default flag

Add drugReference.remediesEnabled (default off), independent of the tier
install. When off, the server emits no remedy data at any boundary — the
drug-reference page prop, conditions show, and the /api/conditions/drugs
situation search — and the "Natural" filter is hidden, so installing the
medicine-standard tier lights up the verbatim FDA label search and the
condition-to-OTC matching but not the hand-authored self-care and herbal
sections. No user-facing toggle: it is flipped on after a clinician content-pass.

Implements the upstream #1040 split-by-risk request: the regulated label content
ships with the tier; the authored remedy guidance stays gated until sign-off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Jake Turner <52841588+jakeaturner@users.noreply.github.com>
2026-07-21 11:06:18 -07:00
Chris Sherwood 398ca018cd feat(benchmark): Score v2 app client — raws, uncapped score, v2 payload + UI
Phase 4 of the NOMAD Score v2 effort (stacked on #1089). Captures the raw
channel values the leaderboard scores from, computes the uncapped v2 score
in-app (byte-matching the leaderboard's server-side recompute), submits the
v2 payload, and surfaces it in the benchmark UI.

- Capture raws: per-channel sysbench values + O_DIRECT disk (W4), single/multi
  thread CPU + memory, W6 consistency companions, and run-environment metadata
  (run_environment, storage_path_type, gpu_compute_detected, #1016).
- AI hardening: reference model llama3.1:8b, num_predict=256, VRAM eviction
  before the run + unload own model after, pre-flight disk check (~6.5GB, only
  when the model is uncached), median-of-3 run (W7).
- Score: _calculateNomadScoreV2 (uncapped, reference->1000) byte-matches the
  leaderboard recompute. nomad_score_v2 stays null unless a full run with AI.
- Payload: submitToRepository sends the v2 raws + score alongside legacy v1.
- UI: v2 headline score, legacy v1 as secondary, raws/env in details.
- Migration: 13 nullable columns; double (not knex float(8,2)) so millions-scale
  raws keep full precision and don't break the leaderboard byte-match.

Reference constant REFERENCE_SCORES_V2.ai_tokens_per_second is still the stale
40.5 placeholder (TODO in source); locking it to the measured 13.2 is a
follow-up gated on the AMD iGPU fix (#1074). Live prod v2 submit is separately
blocked by the leaderboard TTFT floor (tracked in the leaderboard follow-up).

Verified on NOMAD3 (RTX 5060) dev env: migration applies, raws recorded,
v2 score computed, backend + benchmark.tsx typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 09:36:14 -07:00
just-jbc 4f4cc5bc65
feat(rag): add subject/collection organization to knowledge base (#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 #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/project-nomad#1100) off dev.

* chore: remove unrelated diff noise (lockfile, comments, indentation)

---------

Co-authored-by: John Cortright <jcortright@zscaler.com>
2026-07-20 15:09:11 -07:00
chriscrosstalk 00b5977953
feat(benchmark): harness hardening — fail loudly + pin sysbench + record provenance (#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>
2026-07-20 13:48:09 -07:00
Jake Turner 8982d93a31
feat: replace legacy Kolibri image default with latest v19 image (#1019)
* feat: replace legacy Kolibri image default with latest v19 image
* feat(supply-depot): add content migration instructions for Edu Platform Gen 1 to 2
2026-06-23 04:47:07 +00:00
jakeaturner 6a2d4c2bf6
feat(content): opt-in automatic updates for installed ZIM & map content 2026-06-23 04:47:01 +00:00
jakeaturner 026ab6df8f
feat(supply-depot): add custom launch URLs for apps
Let users override an app's "Open" link with a reverse-proxy or
local-DNS address (e.g. https://jellyfin.myhomelab.net). Falls back to
the default host+port when unset. Metadata-only — no container changes.
2026-06-23 04:46:56 +00:00
Chris Sherwood 3977c723c2
fix(KB): stop partial_stall warning firing on atypical ZIMs (link-out/PDF-heavy)
The Stored Files "partial stall" warning compares chunks in Qdrant against an
expected count from the ratio registry. The registry has an empty-pattern
catch-all (100 chunks/MB) that matches any filename, so a ZIM that matches no
specific pattern still gets a size-based estimate. For archives that are mostly
PDFs, images, or link-out stubs (e.g. irp.fas.org military-medicine), byte size
wildly over-predicts embeddable text: a 75 MB ZIM estimates ~7,236 chunks but
produces ~1, tripping a false "ingestion may have stalled" warning that re-embed
can't clear.

The catch-all is fine for rough aggregate disk-cost estimates, but it should not
drive a per-file stall signal. Add an `ignoreCatchAll` option to the ratio
lookup that excludes the empty-pattern row (returning null when only the
fallback would match), and use it in the warnings path so partial_stall only
fires when the registry has a *specific* expectation for the file. Files that
match a real pattern (wikipedia_, devdocs_, ifixit_, ...) are unaffected;
disk-cost/batch estimates keep using the fallback.

Closes #913

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 04:46:54 +00:00
jakeaturner cf8db6218d
feat(supply-depot): opt-in automatic updates for installed apps 2026-06-23 04:46:52 +00:00
Chris Sherwood 02c33b277e
feat: edit curated apps + fix dropdown clip + stale _old rollback 2026-06-23 04:46:46 +00:00
Jake Turner be434d755a
feat: supply depot 2026-06-23 04:46:45 +00:00
Jake Turner 82f67debc1 fix(models): correct inverted belongsTo keys on ChatMessage.session (#921)
foreignKey/localKey were swapped on the ChatMessage → ChatSession
relation. Per Lucid's belongsTo contract, foreignKey is the column
on the child model and localKey is on the parent — so this must be
{ foreignKey: 'session_id', localKey: 'id' }, mirroring the inverse
hasMany on ChatSession.

The relation is not currently preloaded anywhere in the codebase, so
no runtime behavior changes today; this closes a latent bug that
would have broken any future preload('session') call.
2026-05-20 10:16:00 -07:00
Chris Sherwood e68c753e39 feat(KB): surface embedding-disk estimate in curated tier-change modal (RFC #883 §1)
When a user picks a tier in TierSelectionModal, show how much additional
disk space the AI Assistant will need if the new ZIMs are indexed, plus
a policy-aware footer explaining whether they'll auto-index (Always) or
wait for opt-in (Manual). Estimates consume #891's KbRatioRegistry via a
new POST /api/rag/estimate-batch endpoint.

Backend
- New POST /api/rag/estimate-batch route + RagController.estimateBatch
- VineJS schema accepting array of {filename, sizeBytes}, capped at 500
- KbRatioRegistry.estimateBatch aggregates via the existing prefix-match
  lookup, returns {totalChunks, totalBytes, hasUnknown}
- New BYTES_PER_CHUNK_ON_DISK constant (~8 KB: 3 KB vector + ~3 KB chunk
  text + ~2 KB payload/index overhead). Tunable; will be replaced by
  Phase 4 self-calibration once we have real measurements.
- Controller normalizes incoming filenames via path.basename so callers
  that send full paths or URLs still match registry prefixes correctly.

Frontend
- api.estimateEmbeddingBatch() client method
- TierSelectionModal: when localSelectedSlug is set, resolve the tier's
  resources (incl. inherited tiers), POST to /estimate-batch, and render
  a new info block with the +~X GB figure + ingest-policy copy. Also
  fetches rag.defaultIngestPolicy so the same block surfaces whether
  indexing will fire automatically or wait for the user.
- resourceFilename() helper extracts the basename from the resource URL
  so the registry lookup hits the right prefix regardless of mirror.

Tests
- 4 new cases in tests/unit/kb_ratio_lookup.spec.ts covering the
  estimateBatch aggregator: standard sum, unknown-flagging, video-only
  ZIM (0 chunks but known, hasUnknown stays false), empty input.

Stacks on feat/kb-ratio-registry (#891) — consumes the registry table
seeded by that PR. Once #891 merges to rc, this PR auto-rebases.

Out of scope for this PR (deferred to follow-ups):
- Per-batch opt-in checkbox (RFC §1's '☑ Also index these for AI') needs
  a per-batch policy override path and is a separate PR
- Guardrail modal at 50 GB / 10% free / 6 hr thresholds (RFC §7) is also
  separate; this PR is informational, not gating
- Time-to-embed estimate awaits a chunks-per-second metric per host
2026-05-20 10:16:00 -07:00
Chris Sherwood 159d57b2af feat(KB): ratio registry for disk + time estimates (Phase 1B of RFC #883)
Foundation for the cost estimates and partial-stall detection that
Phase 2 will surface. No consumers yet — this PR just lays the table,
the seed rows, and the lookup helper so subsequent UI work has
estimates available without a per-ZIM benchmark.

## What lands

- New table `kb_ratio_registry` (pattern, chunks_per_mb, sample_count,
  notes). Migration creates and seeds heuristic defaults from the RFC
  appendix: devdocs (1100/MB), Wikipedia variants (270/MB), iFixit
  (50/MB), Stack Exchange Q&A (200/MB), video-only ZIMs (0), plus a
  catch-all fallback at 100/MB.
- `KbRatioRegistry` model with static `lookup()` and `estimateChunks()`.
- Pure helper `kb_ratio_lookup.ts` doing longest-prefix-match — a
  specific entry (`wikipedia_en_simple_`) overrides a broader one
  (`wikipedia_en_`). 9 unit tests covering the lookup boundary.
- `sample_count` starts at 0 (heuristic seed) and is reserved for
  Phase 4 self-calibration to increment as observed ZIMs update each row.

## Not in scope

- Self-calibration on successful ingestion (Phase 4)
- UI consumers — Warning B (partial-embed stall) and the storage budget
  meter / time estimates land in Phase 2.

## Tested

- Type-check clean
- 9 unit tests pass for `findChunksPerMb` and `estimateChunkCount`
- Migration applied on NOMAD3 via hot-patch; 9 seed rows verified in DB
2026-05-20 10:16:00 -07:00
chriscrosstalk 743549ca74 feat(KB): per-file ingest state machine (Phase 1 of RFC #883) (#888)
Adds a persistent state machine for AI knowledge-base ingestion so the
scanner can distinguish "fully indexed", "user opted out", "failed", and
"stalled" from each other — none of which were derivable from the prior
binary "any chunks in Qdrant ⇒ embedded" check.

## What lands

- New table `kb_ingest_state` keyed by `file_path` with enum state column
  (`pending_decision | indexed | browse_only | failed | stalled`).
  Independent of `installed_resources` so it covers both curated downloads
  and manually-uploaded KB files.
- New KV key `rag.defaultIngestPolicy` (string: `Always | Manual`).
  Registered now but not consumed yet — JIT prompt + wizard step land in
  Phase 3 of the RFC.
- `EmbedFileJob.handle` writes state on terminal outcomes:
  - Success (final batch) → `indexed` + chunks count
  - `UnrecoverableError` → `failed` + error message
  - Retryable errors are left to BullMQ's existing retry path
- `scanAndSyncStorage` swaps the binary qdrant check for a state-aware
  decision tree (see `decideScanAction`). Existing installs auto-backfill
  on first scan: files with chunks in Qdrant but no state row become
  `indexed`; new files start as `pending_decision`.
- `deleteFileBySource` drops the state row last, so removed files
  disappear entirely instead of leaving an orphan that the next scan
  would re-dispatch into nothing.

## What does NOT land here

- Ratio registry (separate PR) — needed for partial-stall detection and
  cost estimates, but a separable concern.
- #880 follow-up initial-progress anchor (separate tiny PR).
- Phase 2 UI (status pill, per-card actions, conditional warnings).
- Phase 3 policy surfaces (wizard step, JIT prompt, guardrail modal).
- PR #886's bulk-action hookup — `_deletePointsBySource` / Re-embed All
  / Reset & Rebuild would also want to set state, but #886 isn't merged
  yet; that wiring goes in a follow-up once #886 lands.

## Target

This is forward work for v1.40.0 (RFC #883). Branching off `rc` because
that's the current latest base and post-GA Jake will sync rc→dev; a
retarget at PR-open time is a fast-forward if requested.

## Tests

- 9 new unit tests for `decideScanAction` covering all five states plus
  the no-row / chunks-present / chunks-missing combinations
- Type-check clean
- Smoke-tested end-to-end on NOMAD3 via hot-patch:
  - Backfill: 5 ZIMs + 2 KB uploads with existing chunks in Qdrant all
    came back `indexed` on first scan
  - Pending dispatch: a video-only ZIM with no chunks (`lrnselfreliance`)
    came back `pending_decision` and was correctly re-dispatched (Bull
    deduped to its historical `:completed` jobId — bgauger's #886 fix
    drains that)
  - Delete hook: deleting a KB upload via `DELETE /api/rag/files`
    removed both the disk file and the state row

Co-authored-by: Jake Turner <52841588+jakeaturner@users.noreply.github.com>
2026-05-20 10:16:00 -07:00
chriscrosstalk 62e75fdb54 feat(Content): custom ZIM library sources with pre-seeded mirrors (#593)
* feat(content): add custom ZIM library sources with pre-seeded mirrors

Users reported slow download speeds from the default Kiwix CDN. This adds
the ability to browse and download ZIM files from alternative Kiwix mirrors
or self-hosted repositories, all through the GUI.

- Add "Custom Libraries" button next to "Browse the Kiwix Library"
- Source dropdown to switch between Default (Kiwix) and custom libraries
- Browsable directory structure with breadcrumb navigation
- 5 pre-seeded official Kiwix mirrors (US, DE, DK, UK, Global CDN)
- Built-in mirrors protected from deletion
- Downloads use existing pipeline (progress, cancel, Kiwix restart)
- Source selection persists across page loads via localStorage
- Scrollable directory browser (600px max) with sticky header
- SSRF protection on all custom library URLs

Closes #576

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(content): recognize Wikipedia downloads from mirror sources

When Wikipedia is downloaded via a custom mirror instead of the default
Kiwix server, the completion callback now matches by filename instead
of exact URL. This ensures the Wikipedia selector correctly shows
"Installed" status and triggers old-version cleanup regardless of
which mirror was used.

Also handles the case where no Wikipedia selection exists yet (file
downloaded before visiting the selector), creating the record
automatically.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(ZIM): use cheerio for custom mirror directory parsing

* fix(ZIM): use URL constructor for more robust joining

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Jake Turner <jturner@cosmistack.com>
2026-05-20 10:16:00 -07:00
chriscrosstalk 0183b42d71 feat(maps): add scale bar and location markers (#636)
Add distance scale bar and user-placed location pins to the offline maps viewer.

- Scale bar (bottom-left) shows distance reference that updates with zoom level
- Click anywhere on map to place a named pin with color selection (6 colors)
- Collapsible "Saved Locations" panel lists all pins with fly-to navigation
- Full dark mode support for popups and panel via CSS overrides
- New `map_markers` table with future-proofed columns for routing (marker_type,
  route_id, route_order, notes) to avoid a migration when routes are added later
- CRUD endpoints: GET/POST /api/maps/markers, PATCH/DELETE /api/maps/markers/:id
- VineJS validation on create/update
- MapMarker Lucid model

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 14:26:50 -07:00
Jake Turner 58b106f388 feat: support for updating services 2026-03-11 14:08:09 -07:00
Jake Turner 96beab7e69 feat(AI Assistant): custom name option for AI Assistant 2026-03-04 20:05:14 -08:00
Jake Turner 6817e2e47e fix: improve type-safety for KVStore values 2026-03-03 20:51:38 -08:00
Jake Turner 32d206cfd7
feat: curated content system overhaul 2026-02-11 15:44:46 -08:00
Jake Turner 36b6d8ed7a fix: rework content tier system to dynamically determine install status
Removes the InstalledTier model and instead checks presence of files on-the-fly. Avoid broken state by handling on the server-side vs. marking as installed by client-side API call
2026-02-04 22:58:21 -08:00
Jake Turner 6b17e6ff68 fix(Curated Collections): ensure resources are not duplicated on fetch-latest 2026-02-04 21:35:18 -08:00
Chris Sherwood 2c4fc59428 feat(ContentManager): Display friendly names instead of filenames
Content Manager now shows Title and Summary columns from Kiwix metadata
instead of just raw filenames. Metadata is captured when files are
downloaded from Content Explorer and stored in a new zim_file_metadata
table. Existing files without metadata gracefully fall back to showing
the filename.

Changes:
- Add zim_file_metadata table and model for storing title, summary, author
- Update download flow to capture and store metadata from Kiwix library
- Update Content Manager UI to display Title and Summary columns
- Clean up metadata when ZIM files are deleted

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 23:14:28 -08:00
Jake Turner 1923cd4cde feat(AI): chat suggestions and assistant settings 2026-02-01 07:24:21 +00:00
Chris Sherwood 68f374e3a8 feat: Add dedicated Wikipedia Selector with smart package management
Adds a standalone Wikipedia selection section that appears prominently in both
the Easy Setup Wizard and Content Explorer. Features include:

- Six Wikipedia package options ranging from Quick Reference (313MB) to Complete
  Wikipedia with Full Media (99.6GB)
- Card-based radio selection UI with clear size indicators
- Smart replacement: downloads new package before deleting old one
- Status tracking: shows Installed, Selected, or Downloading badges
- "No Wikipedia" option for users who want to skip or remove Wikipedia

Technical changes:
- New wikipedia_selections database table and model
- New /api/zim/wikipedia and /api/zim/wikipedia/select endpoints
- WikipediaSelector component with consistent styling
- Integration with existing download queue system
- Callback updates status to 'installed' on successful download
- Wikipedia removed from tiered category system to avoid duplication

UI improvements:
- Added section dividers and icons (AI Models, Wikipedia, Additional Content)
- Consistent spacing between major sections in Easy Setup Wizard
- Content Explorer gets matching Wikipedia section with submit button

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 21:00:51 -08:00
Jake Turner 243f749090 feat: [wip] native AI chat interface 2026-01-31 20:39:49 -08:00
Jake Turner 50174d2edb feat(RAG): [wip] RAG capabilities 2026-01-31 20:39:49 -08:00
chriscrosstalk 7a5a254dd5
feat(benchmark): Require full benchmark with AI for community sharing (#99)
* feat(benchmark): Require full benchmark with AI for community sharing

Only allow users to share benchmark results with the community leaderboard
when they have completed a full benchmark that includes AI performance data.

Frontend changes:
- Add AI Assistant installation check via service API query
- Show pre-flight warning when clicking Full Benchmark without AI installed
- Disable AI Only button when AI Assistant not installed
- Show "Partial Benchmark" info alert for non-shareable results
- Only display "Share with Community" for full benchmarks with AI data
- Add note about AI installation requirement with link to Apps page

Backend changes:
- Validate benchmark_type is 'full' before allowing submission
- Require ai_tokens_per_second > 0 for community submission
- Return clear error messages explaining requirements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(benchmark): UI improvements and GPU detection fix

- Fix GPU detection to properly identify AMD discrete GPUs
- Fix gauge colors (high scores now green, low scores red)
- Fix gauge centering (SVG size matches container)
- Add info tooltips for Tokens/sec and Time to First Token

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(benchmark): Extract iGPU from AMD APU CPU name as fallback

When systeminformation doesn't detect graphics controllers (common on
headless Linux), extract the integrated GPU name from AMD APU CPU model
strings like "AMD Ryzen AI 9 HX 370 w/ Radeon 890M".

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(benchmark): Add Builder Tag system for community leaderboard

- Add builder_tag column to benchmark_results table
- Create BuilderTagSelector component with word dropdowns + randomize
- Add 50 adjectives and 50 nouns for NOMAD-themed tags (e.g., Tactical-Llama-1234)
- Add anonymous sharing option checkbox
- Add builder tag display in Benchmark Details section
- Add Benchmark History section showing all past benchmarks
- Update submission API to accept anonymous flag
- Add /api/benchmark/builder-tag endpoint to update tags

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(benchmark): Add HMAC signing for leaderboard submissions

Sign benchmark submissions with HMAC-SHA256 to prevent casual API abuse.
Includes X-NOMAD-Timestamp and X-NOMAD-Signature headers.

Note: Since NOMAD is open source, a determined attacker could extract
the secret. This provides protection against casual abuse only.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 00:24:31 -08:00
Chris Sherwood 5afc3a270a feat: Improve curated collections UX with persistent tier selection
- Add installed_tiers table to persist user's tier selection per category
- Change tier selection behavior: clicking a tier now highlights it locally,
  user must click "Submit" to confirm (previously clicked = immediate download)
- Remove "Recommended" badge and asterisk (*) from tier displays
- Highlight installed tier instead of recommended tier in CategoryCard
- Add "Click to choose" hint when no tier is installed
- Save installed tier when downloading from Content Explorer or Easy Setup
- Pass installed tier to modal as default selection

Database:
- New migration: create installed_tiers table (category_slug unique, tier_slug)
- New model: InstalledTier

Backend:
- ZimService.listCuratedCategories() now includes installedTierSlug
- New ZimService.saveInstalledTier() method
- New POST /api/zim/save-installed-tier endpoint

Frontend:
- TierSelectionModal: local selection state, "Close" → "Submit" button
- CategoryCard: highlight based on installedTierSlug, add "Click to choose"
- Content Explorer: save tier after download, refresh categories
- Easy Setup: save tiers on wizard completion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 15:33:50 -08:00
Chris Sherwood 755807f95e feat: Add system benchmark feature with NOMAD Score
Add comprehensive benchmarking capability to measure server performance:

Backend:
- BenchmarkService with CPU, memory, disk, and AI benchmarks using sysbench
- Database migrations for benchmark_results and benchmark_settings tables
- REST API endpoints for running benchmarks and retrieving results
- CLI commands: benchmark:run, benchmark:results, benchmark:submit
- BullMQ job for async benchmark execution with SSE progress updates
- Synchronous mode option (?sync=true) for simpler local dev setup

Frontend:
- Benchmark settings page with circular gauges for scores
- NOMAD Score display with weighted composite calculation
- System Performance section (CPU, Memory, Disk Read/Write)
- AI Performance section (tokens/sec, time to first token)
- Hardware Information display
- Expandable Benchmark Details section
- Progress simulation during sync benchmark execution

Easy Setup Integration:
- Added System Benchmark to Additional Tools section
- Built-in capability pattern for non-Docker features
- Click-to-navigate behavior for built-in tools

Fixes:
- Docker log multiplexing issue (Tty: true) for proper output parsing
- Consolidated disk benchmarks into single container execution

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 21:48:12 -08:00
Chris Sherwood 24f10ea3d5 feat: Use friendly app names on Dashboard with open source attribution
Updates the Dashboard to use the same user-friendly names as the Easy Setup
Wizard, giving credit to the open source projects powering each capability:

- Kiwix → Information Library (Powered by Kiwix)
- Kolibri → Education Platform (Powered by Kolibri)
- Open WebUI → AI Assistant (Powered by Open WebUI + Ollama)
- FlatNotes → Notes (Powered by FlatNotes)
- CyberChef → Data Tools (Powered by CyberChef)

Also reorders Dashboard cards to prioritize Core Capabilities first, with
Maps promoted to Core Capability status, followed by Additional Tools,
then system items (Easy Setup, Install Apps, Docs, Settings).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:43:32 -08:00
Jake Turner bb67bab9a9 feat: improved app cards and custom icons 2026-01-15 15:54:59 -08:00
Jake Turner c46b75e63d fix(admin): improve duplicate install request handling 2026-01-13 06:58:05 -08:00
Jake Turner dd4e7c2c4f feat: curated zim collections 2025-12-05 15:47:22 -08:00
Jake Turner b8eaaa7ac6
feat(Services): friendly names and descriptions 2025-11-18 14:02:22 -08:00
Jake Turner 3b81e00a69 feat: openwebui+ollama and zim management 2025-07-09 09:08:21 -07:00
Jake Turner b33a1b3e37 feat: initial commit 2025-06-29 15:51:08 -07:00