feat(library): import curated collections + upload your own documents#20
Merged
FzzySwtr merged 2 commits intoJul 17, 2026
Merged
Conversation
Adds a way for users to grow the field library at runtime, from one curated
place, without a rebuild or restart. Content lands in the sources volume where
the ingestion service already auto-indexes it.
Catalog (sources.yaml):
- Becomes the single source of truth: a top-level `collections:` block groups
the 32 public-domain / CC documents into 8 named bundles (Core Survival,
Austere Medicine, Food & Water, Power & Engineering, Comms & Radio,
Education, Civics & Culture, Software), and every item carries a
`collection:` tag alongside its category. License values containing '#'
are quoted so they aren't parsed as YAML comments.
CLI (helper-scripts/fetch-source-data.sh):
- Parses the new field; adds `--collection <id>` and `--list-collections`;
--validate now checks each item references a declared collection.
Server (src/catalog.h new, src/server.cpp):
- GET /api/catalog — collections + per-item "installed" state.
- POST /api/import — { "collection": id }; validates the id against the
catalog (a closed set) AND as a strict slug, single-flights via an atomic
flag, and downloads by posix_spawn-ing the bundled fetcher with an explicit
argv (never a shell). Only the fixed, image-baked manifest URLs are ever
contacted — no arbitrary-URL / SSRF surface.
- POST /api/upload — multipart PDF/TXT; filename reduced to a safe basename,
category constrained to NNN_Name, %PDF magic + size checks, a free-space
guard, and an atomic link()-based finalise that fails closed (EEXIST → 409)
so a check-then-rename race can't clobber an existing file.
- Small JSON endpoints (/query, /api/import) reject bodies over
MAX_REQUEST_BODY; the global body cap is bounded (MAX_UPLOAD_BYTES = 32 MB).
UI (public/): a "+ Add" action on the Field Library panel opens a modal that
lists the collections (with install progress) and a PDF/TXT upload drop zone;
CSP-safe (no inline handlers), design-canon clean.
Air-gapped deployments can disable network import with JIC_DISABLE_NETWORK_IMPORT
(uploading local files stays available).
Verified: end-to-end (catalog lists 8 collections; a real collection downloads
and indexes; an uploaded doc is indexed) and security (path-traversal, command
injection, SSRF, single-flight, disk/DoS) — 17/17; plus degraded test-server.sh
21/21, test-config.sh 38/38, lint-canon, unit tests, clean image build. The
change passed an adversarial security review with a SHIP verdict.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a user-facing “grow the library” workflow to JustInCase by introducing a curated content catalog (importable by collection) plus a direct PDF/TXT upload path that writes into the existing sources volume for ingestion to auto-index.
Changes:
- Add server endpoints for catalog listing, collection import (via
posix_spawn), and multipart document upload with validation/guards. - Define curated “collections” in
sources.yamland extend the fetch helper to filter/list by collection and validate collection membership. - Add a UI modal (+ CSS/JS wiring) to install curated collections and upload local documents.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/server.cpp |
Adds /api/catalog, /api/import, /api/upload, and payload-size gating; implements import worker spawning and upload validation/storage logic. |
src/config.h |
Introduces MAX_UPLOAD_BYTES and config/env helpers for catalog path, fetcher path, and network-import enablement. |
src/catalog.h |
New minimal manifest parser for collections + items used by /api/catalog and /api/import validation. |
sources.yaml |
Adds top-level collections: and tags each source item with a collection:. |
public/index.html |
Adds “+ Add” action and modal markup for curated import + upload UI. |
public/app.js |
Implements modal open/close, catalog rendering, import triggering + polling, and upload flow/drag-drop. |
public/style.css |
Styles the new “add content” modal and catalog/upload UI elements. |
helper-scripts/fetch-source-data.sh |
Adds --collection, --list-collections, and validation that each item references a declared collection. |
…ling Four Copilot review findings on the content-import feature: - /api/upload: reject a missing "file" form field with a clean 400 (has_file check) instead of risking a throw/500 on a malformed multipart body. - /api/upload: validate the extension case-insensitively (e.g. FOO.PDF), and run the %PDF magic-byte check for any-case .pdf — matching ingestion, which lowercases extensions. - /api/upload: fail CLOSED on the free-space guard — if create_directories or fs::space errors, reject (500/507) rather than writing without the disk-full protection. - app.js: clear the import progress poll when the modal closes (no more background /api/catalog fetches every 3s), and resume it on reopen if an import is still running. Verified: missing-file→400, uppercase .PDF/.TXT accepted, uppercase .pdf without %PDF magic still →400; clean image build; app.js node --check + lint OK. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
There was no user-facing way to grow the field library — content could only be added via CLI/compose. This adds a way to import libraries from one curated place (public-domain, categorized) and to upload your own documents, both in-app and via CLI. New files land in the sources volume where ingestion already auto-indexes them — no rebuild, no restart.
What
Catalog —
sources.yamlis now the single source of truthcollections:block groups the 32 public-domain/CC docs into 8 named bundles (Core Survival, Austere Medicine, Food & Water, Power & Engineering, Comms & Radio, Education, Civics & Culture, Software); every item carries acollection:tag alongside its category.#are quoted (so they're not parsed as YAML comments).CLI —
fetch-source-data.sh--collection <id>,--list-collections;--validatenow checks each item references a declared collection.Server —
src/catalog.h(new) +src/server.cppGET /api/catalog— collections + per-item installed state.POST /api/import{ "collection": id }— validates the id against the catalog (closed set) and as a strict slug; single-flights via an atomic flag; downloads byposix_spawn-ing the bundled fetcher with an explicit argv (never a shell). Only the fixed, image-baked manifest URLs are ever contacted → no arbitrary-URL / SSRF surface.POST /api/upload— multipart PDF/TXT; filename reduced to a safe basename, category constrained toNNN_Name,%PDFmagic + size checks, a free-space guard, and an atomiclink()finalise that fails closed (EEXIST → 409) so a check-then-rename race can't clobber a file./queryand/api/importreject bodies overMAX_REQUEST_BODY; global body cap bounded (MAX_UPLOAD_BYTES = 32 MB).UI —
public/Air-gapped:
JIC_DISABLE_NETWORK_IMPORTdisables network import; uploading local files stays available.Verification
.txtgets indexed; installed-state flips.../../../pwned.txtsanitized to stay inside its category; injection-y import id → 404 (no shell); traversal category → 400; fake-PDF → 400; single-flight import → 409; disk/DoS guarded.test-server.sh21/21 ·test-config.sh38/38 · lint-canon · unit tests · clean image build.Reviewer notes
posix_spawnwith a validated, closed-set collection id and an explicit argv — no/bin/sh, no client-supplied URL. Works under theread_only/cap_drop: ALLhardening (curl + the fetcher +sources.yamlare image-baked; writes go to the writable sources volume).ContentReaderto drop even the transient buffer.🤖 Generated with Claude Code