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>
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>
Closes#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>
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.