The map_markers backend has accepted a `notes` column since PR #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.
Four of the five Wikipedia packages pointed at 2025-12 builds that openZIM
has since rolled forward and deleted. Every one returns 404:
Quick Reference wikipedia_en_top_mini_2025-12 404
Popular Articles wikipedia_en_top_nopic_2025-12 404
Complete Wikipedia (Compact) wikipedia_en_all_mini_2025-12 404
Complete Wikipedia (No Images) wikipedia_en_all_nopic_2025-12 404
Only Complete Wikipedia (Full) still resolved, and at 124 GB that's the
option almost nobody picks. So in practice a user choosing any sensible
Wikipedia size during Easy Setup got a failed download.
Repointed all four at the current 2026-06 builds and corrected every
size_mb from the measured Content-Length. The old figures were estimates
and had drifted by up to 8%.
Sizes are decimal MB, consistent with kiwix-categories.json.
Refs #1171
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The category was 10.8 GB of which 89% was YouTube channel archives. Nothing
in Essential or Standard could be read without watching a video, which is
the wrong shape for the situations this content is for: low power, low
bandwidth, or just looking something up quickly.
Fixes a live 404. canadian_prepper_preppingfood_en_2025-09.zim no longer
exists upstream, so anyone installing the Comprehensive tier today hits a
broken download (#1171). openZIM renamed it, and because resource_id is
derived from the on-disk filename by parseZimFilename(), the id has to
change with it or the installed file would never match its catalog entry.
Adds 5 GB of searchable text across the three tiers:
Essential water treatment, food preparation, knots (144 MB total)
Standard Ready.gov, outdoors Q&A, amateur radio Q&A
Comprehensive post-disaster library, Hundred Rabbits
Amateur radio in particular filled a gap: communications is a core
preparedness topic and the category had nothing on it.
Text share goes from 11% to 36%. Essential grows by 144 MB, which is 6%,
and stops being video-only.
Deliberately not included: gardening.stackexchange and Gutenberg Agriculture
are already curated under Agriculture & Food, and survivorlibrary.com is the
single most on-mission corpus in the Kiwix library but is 252 GB.
All URLs verified with range requests; all ids verified to match their
filename base.
Refs #1171, #1149
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CD3WD is a compilation of appropriate-technology and development
literature: food production and storage, water and sanitation,
construction, health, and village-scale manufacturing without
industrial supply chains. Requested in #1149.
The survival category was until now almost entirely YouTube channel
archives, with Project Gutenberg military science as the only text
reference. CD3WD adds a substantial practical reference at 581 MB,
which is the smallest resource in the category by a wide margin.
Verified live: HTTP 206 on a range request, 581,165,229 bytes.
Refs #1149
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the Modern Rogue pack (Brian Brushwood) to the Creator Packs catalog.
37 videos, 5177 MB. ZIM (modern-rogue_2026-07.zim) is uploaded to R2 and
verified serveable via the entitlement Worker.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebuilt on top of dev's RFC #883 state-machine UI rather than the now-defunct
StoredFile shape:
- Extend StoredFileInfo with fileName/size/uploadedAt/isUserUpload
- Populate metadata from on-disk stats in RagService.getStoredFiles
- Add fileSourceSchema validator + getFileContent/downloadFile endpoints
scoped to the uploads directory only (tighter than the original PR — matches
docs_service traversal pattern)
- KnowledgeBaseModal: sortable Size and Uploaded columns; View/Download
buttons on upload-bucket rows; new FileViewerModal for in-browser text
preview. Bucket grouping preserved — sort applies within each bucket.
- Use formatBytes from ~/lib/util rather than redefining
BullMQ instantiates a fresh ioredis client per Queue/Worker when handed a
plain {host, port} config object, and under sustained ZIM ingestion the
embed pipeline leaked ~1 client/sec until Redis maxclients was exhausted.
Pass a single shared ioredis instance (maxRetriesPerRequest: null, as
required by BullMQ) so all queues and workers reuse one client pool.
Workers still duplicate the connection once for their blocking client,
which is expected and bounded.
Closes#885
* feat: replace legacy Kolibri image default with latest v19 image
* feat(supply-depot): add content migration instructions for Edu Platform Gen 1 to 2
Adds the MeshCore web client to the Supply Depot catalog (host port 8500),
alongside the existing Meshtastic apps. Uses aXistem's prebuilt image of Liam
Cottle's MeshCore client (MeshCore is a sibling LoRa mesh project to Meshtastic).
The image is stock nginx serving a static Flutter build over HTTP, but the
client reaches radios via Web Bluetooth / Web Serial, which browsers only allow
from a secure (HTTPS) context. So we serve it over HTTPS: a new preinstall hook
generates a self-signed cert + a small SSL nginx config into storage/meshcore-web,
both bind-mounted into the container (the config over the image's default.conf),
publishing 443. Same one-time browser-warning approach as Vaultwarden, whose
openssl cert generation is refactored into a shared _ensureSelfSignedCert helper.
Also adds a NOMAD-specific docs section + Manage>Docs anchor, and registers the
IconAntenna icon. Meshtastic Web left unchanged.
Validated on NOMAD3 (v1.33.0-rc.1): the image + SSL config + self-signed cert
serves the MeshCore Flutter app over HTTPS 200 with working SPA fallback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Curated catalog apps could be installed, stopped, and force-reinstalled,
but never removed — the only path off a device was manual docker + DB
surgery. Custom apps already had delete; this adds the equivalent for
curated apps.
POST /api/system/services/uninstall stops and removes the app's
container (optionally its image, same best-effort semantics as custom
app delete) and flips the record back to not-installed so the card
returns to the available catalog. Host bind-mount data is deliberately
left on disk, so a later reinstall picks the app back up where it left
off — unlike force-reinstall, which clears volumes.
Guards: custom apps are rejected (use delete), dependency services are
rejected, and uninstalling a not-installed app is a 409.
UI: installed curated cards get an Uninstall action in the card menu,
with a confirm modal that explains data is preserved and offers the
same remove-image checkbox as custom app delete.
Bring the GitHub-facing README in line with the v1.33 feature set:
- Add Supply Depot (one-click app catalog + bring-your-own custom
Docker containers) and Automatic Updates to the "Built-in
capabilities" list and the "What's Included" table.
- Fix the Offline Maps row, which claimed "navigation" — NOMAD maps
are download/view only, with no routing. Reword to "offline viewing
and search."
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refresh the in-app Markdoc docs for the v1.33 feature set:
- Repoint dead /settings/apps links to the Supply Depot (/supply-depot)
across home, getting-started, and faq; reword "Apps page" / "Settings
-> Apps" to "Supply Depot". The old /apps route now redirects to the
Supply Depot.
- Expand supply-depot-apps.md with a "Managing your apps" section (Docs/
Edit/Logs/Stats/Update/Remove, version + update-available visibility,
custom launch URLs, per-app auto-update toggle) and a "Bringing your
own app" section for custom Docker containers.
- Add a new "Updates" doc (updates.md) covering the auto-update trilogy
(core/apps/content), manual updates, and the Early Access channel;
wire it into DOC_ORDER and cross-link from home, getting-started, faq.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Only Wikipedia had version cleanup; every other curated map and non-Wikipedia
ZIM left its prior version on disk when a newer one installed, so users silently
accumulated orphaned content (potentially hundreds of GB). (#634)
The install paths already record each resource via InstalledResource
{resource_id, resource_type, version, file_path}, so the authoritative old-file
path for a resource is known. On install of a new version we now capture the
prior row before updateOrCreate repoints it, then delete the old file — gated
behind a pure, fully unit-tested decision function with strict safety rails:
- tracked-only: requires a prior InstalledResource row for the same
resource_id, so sideloaded/untracked files are never touched
- genuine replacement: old and new file paths must differ
- new-file-verified: the new file must be confirmed on disk first
- strictly-newer: a re-install or downgrade can't wipe a newer file
- within-storage-dir: the old path must resolve under the content store
ZIM cleanup deletes the old file directly (NOT via this.delete(), which would
drop the InstalledResource row by resource_id that updateOrCreate just
repointed) and rebuilds the Kiwix library only if a file was actually removed,
so its XML never references a deleted ZIM. Maps need no library step. Wikipedia
keeps its own existing cleanup path. All deletions are best-effort and logged;
a failure never breaks the install.
Closes#634
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`getChatSuggestions` previously picked the largest installed model by file
size, on the assumption that bigger models give better suggestions. This
is unsafe: if any installed model exceeds available VRAM (e.g.
llama3.1:405b on a 96 GB GPU), Ollama spends minutes trying to load it
and the request 500s — making the chat page unusable for anyone who
happens to keep a flagship-sized model on disk.
Chat suggestions are short prompts that don't benefit from a flagship
model anyway. Prefer the user's selected `chat.lastModel` when set, and
fall back to the smallest installed model otherwise. `OllamaService.getModels()`
already excludes embedders, so the fallback always picks a chat model.
Two card tweaks for the update workflow:
- Show the installed image tag next to the powered-by name (e.g. "Kiwix ·
3.7.0"), so the running version is visible at a glance. Only rendered for
installed apps; falls back to just the version when powered_by is unset.
- Change the "Update available" pill from a muted light-green tint to a solid
desert-orange fill with white text, so an available update actually draws
the eye instead of blending into the card.
listTags() follows the registry's Link-header pagination, but the next-page
URL is relative per the OCI/Docker registry spec (e.g.
"/v2/ollama/ollama/tags/list?last=0.9.3-rc5&n=1000"). The code assigned that
raw relative path straight back to `url` and re-fetched it, so fetch() threw
"Failed to parse URL from /v2/...". Any image repo with more than 1000 tags
paginates, so the entire tag list — and therefore the update check — failed
silently for ollama/ollama and filebrowser/filebrowser.
That's the root cause of #945 ("won't update past 0.24.0"): the Ollama
update check never completed, so no newer version was ever offered.
Resolve the next-page URL against the registry origin with
new URL(next, `https://${registry}`), which also passes absolute next-URLs
through unchanged for registries that return those.
Closes#945
The maps page reset to the default US-wide view on every refresh because
initialViewState was hardcoded. Save the position and zoom to localStorage
on each move-end (key nomad:map-view, matching the existing scale-unit
pattern) and restore it at mount: saved view → default. The restore is
bounds-checked so a corrupt value falls through to the default.
Replaces #815, whose branch had drifted far out of scope (56 files of
stale-base/merged-commit noise plus unrelated map-feature WIP). This is
just the persist-view improvement, ported cleanly onto current dev. The
null-island and coordinate-search parts of #815 targeted URL-param code
that never landed on dev, so they don't apply here.
The map style names each source by its date-stripped region (both
"washington.pmtiles" and "washington_2025-12.pmtiles" -> "washington").
When an old and new copy of the same region are both on disk, the style
emitted two sources with the same key and duplicate layer ids, which
MapLibre rejects outright -- blanking the ENTIRE map, not just that region.
Old copies linger when a newer curated version installs (#634), so a user
who updates maps can silently lose all map rendering until the stale file
is removed by hand.
generateSourcesArray() now keeps only the newest file per region: a dated
build beats an undated legacy file, and between two dated builds the later
YYYY-MM wins. The skipped duplicate is logged. The style stays valid even
when stale files are present.
Complements #981, which removes superseded curated files on install. This
is the runtime safety net that also recovers installs already in the broken
state (which a cleanup-on-install alone can't reach).
Refs #634