Merge branch 'dev' into feat/maps-marker-management

Resolve map marker conflicts by keeping the branch's full marker management UI, API payloads, and hook signatures.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kenneth Brewer 2026-07-18 14:01:32 -04:00
commit 87b6d882ba
61 changed files with 852 additions and 352 deletions

View File

@ -1,5 +1,5 @@
name: Bug Report
description: Report a bug or issue with Project N.O.M.A.D.
description: Report a bug or issue with Project NOMAD
title: "[Bug]: "
labels: ["bug", "needs-triage"]
body:
@ -10,9 +10,9 @@ body:
**Before submitting:**
- Search existing issues to avoid duplicates
- Ensure you're running the latest version of N.O.M.A.D.
- Ensure you're running the latest version of NOMAD
- Redact any personal or sensitive information from logs/configs
- Please don't submit issues related to running N.O.M.A.D. on Unraid or another NAS - we don't have plans to support these kinds of platforms at this time
- Please don't submit issues related to running NOMAD on Unraid or another NAS - we don't have plans to support these kinds of platforms at this time
- type: dropdown
id: issue-category
@ -75,8 +75,8 @@ body:
- type: input
id: nomad-version
attributes:
label: N.O.M.A.D. Version
description: What version of N.O.M.A.D. are you running? (Check Settings > Update or run `docker ps` and check nomad_admin image tag)
label: NOMAD Version
description: What version of NOMAD are you running? (Check Settings > Update or run `docker ps` and check nomad_admin image tag)
placeholder: "e.g., 1.29.0"
validations:
required: true
@ -85,7 +85,7 @@ body:
id: os
attributes:
label: Operating System
description: What OS are you running N.O.M.A.D. on?
description: What OS are you running NOMAD on?
options:
- Ubuntu 24.04
- Ubuntu 22.04
@ -150,7 +150,7 @@ body:
Include any relevant logs or error messages. **Please redact any personal/sensitive information.**
Useful commands for collecting logs:
- N.O.M.A.D. management app: `docker logs nomad_admin`
- NOMAD management app: `docker logs nomad_admin`
- Ollama: `docker logs nomad_ollama`
- Qdrant: `docker logs nomad_qdrant`
- Specific service: `docker logs nomad_<service-name>`
@ -185,9 +185,9 @@ body:
options:
- label: I have searched for existing issues that might be related to this bug
required: true
- label: I am running the latest version of Project N.O.M.A.D. (or have noted my version above)
- label: I am running the latest version of Project NOMAD (or have noted my version above)
required: true
- label: I have redacted any personal or sensitive information from logs and screenshots
required: true
- label: This issue is NOT related to running N.O.M.A.D. on an unsupported/non-Debian-based OS
- label: This issue is NOT related to running NOMAD on an unsupported/non-Debian-based OS
required: false

View File

@ -8,10 +8,10 @@ contact_links:
about: Check the official documentation and guides
- name: 🏆 Community Leaderboard
url: https://benchmark.projectnomad.us
about: View the N.O.M.A.D. benchmark leaderboard
about: View the NOMAD benchmark leaderboard
- name: 🤝 Contributing Guide
url: https://github.com/Crosstalk-Solutions/project-nomad/blob/main/CONTRIBUTING.md
about: Learn how to contribute to Project N.O.M.A.D.
about: Learn how to contribute to Project NOMAD
- name: 📅 Roadmap
url: https://roadmap.projectnomad.us
about: See our public roadmap, vote on features, and suggest new ones

View File

@ -1,20 +1,20 @@
name: Feature Request
description: Suggest a new feature or enhancement for Project N.O.M.A.D.
description: Suggest a new feature or enhancement for Project NOMAD
title: "[Feature]: "
labels: ["enhancement", "needs-discussion"]
body:
- type: markdown
attributes:
value: |
Thanks for your interest in improving Project N.O.M.A.D.! Before you submit a feature request, consider checking our [roadmap](https://roadmap.projectnomad.us) to see if it's already planned or in progress. You're welcome to suggest new ideas there if you don't plan on opening PRs yourself.
Thanks for your interest in improving Project NOMAD! Before you submit a feature request, consider checking our [roadmap](https://roadmap.projectnomad.us) to see if it's already planned or in progress. You're welcome to suggest new ideas there if you don't plan on opening PRs yourself.
**Please note:** Feature requests are not guaranteed to be implemented. All requests are evaluated based on alignment with the project's goals, feasibility, and community demand.
**Before submitting:**
- Search existing feature requests and our [roadmap](https://roadmap.projectnomad.us) to avoid duplicates
- Consider if this aligns with N.O.M.A.D.'s mission: offline-first knowledge and education
- Consider the technical feasibility of the feature: N.O.M.A.D. is designed to be containerized and run on a wide range of hardware, so features that require heavy resources (aside from GPU-intensive tasks) or complex host configurations may be less likely to be implemented
- Consider if this aligns with NOMAD's mission: offline-first knowledge and education
- Consider the technical feasibility of the feature: NOMAD is designed to be containerized and run on a wide range of hardware, so features that require heavy resources (aside from GPU-intensive tasks) or complex host configurations may be less likely to be implemented
- Consider the scope of the feature: Small, focused enhancements that can be implemented incrementally are more likely to be implemented than large, broad features that would require significant development effort or have an unclear path forward
- If you're able to contribute code, testing, or documentation, that significantly increases the chances of your feature being implemented
@ -95,7 +95,7 @@ body:
attributes:
label: How important is this feature to you?
options:
- Critical - Blocking my use of N.O.M.A.D.
- Critical - Blocking my use of NOMAD
- High - Would significantly improve my experience
- Medium - Would be nice to have
- Low - Minor convenience
@ -144,7 +144,7 @@ body:
options:
- label: I have searched for existing feature requests that might be similar
required: true
- label: This feature aligns with N.O.M.A.D.'s mission of offline-first knowledge and education
- label: This feature aligns with NOMAD's mission of offline-first knowledge and education
required: true
- label: I understand that feature requests are not guaranteed to be implemented
required: true

View File

@ -1,8 +1,8 @@
# Contributing to Project N.O.M.A.D.
# Contributing to Project NOMAD
Thank you for your interest in contributing to Project N.O.M.A.D.! Community contributions are what keep this project growing and improving. Please read this guide fully before getting started — it will save you (and the maintainers) a lot of time.
Thank you for your interest in contributing to Project NOMAD! Community contributions are what keep this project growing and improving. Please read this guide fully before getting started — it will save you (and the maintainers) a lot of time.
> **Note:** Acceptance of contributions is not guaranteed. All pull requests are evaluated based on quality, relevance, and alignment with the project's goals. The maintainers of Project N.O.M.A.D. ("Nomad") reserve the right accept, deny, or modify any pull request at their sole discretion.
> **Note:** Acceptance of contributions is not guaranteed. All pull requests are evaluated based on quality, relevance, and alignment with the project's goals. The maintainers of Project NOMAD ("NOMAD") reserve the right accept, deny, or modify any pull request at their sole discretion.
---
@ -12,6 +12,7 @@ Thank you for your interest in contributing to Project N.O.M.A.D.! Community con
- [Before You Start](#before-you-start)
- [Getting Started](#getting-started)
- [Development Workflow](#development-workflow)
- [UI Consistency](#ui-consistency)
- [Commit Messages](#commit-messages)
- [Release Notes](#release-notes)
- [Versioning](#versioning)
@ -48,7 +49,7 @@ When opening an issue:
---
## Getting Started with Contributing
**Please note**: this is the Getting Started guide for developing and contributing to Nomad, NOT [installing Nomad](https://github.com/Crosstalk-Solutions/project-nomad/blob/main/README.md) for regular use!
**Please note**: this is the Getting Started guide for developing and contributing to NOMAD, NOT [installing NOMAD](https://github.com/Crosstalk-Solutions/project-nomad/blob/main/README.md) for regular use!
### Prerequisites
@ -72,7 +73,7 @@ When opening an issue:
```
### Avoid Installing a Release Version Locally
Because Nomad relies heavily on Docker, we actually recommend against installing a release version of the project on the same local machine where you are developing. This can lead to conflicts with ports, volumes, and other resources. Instead, you can run your development version in a separate Docker environment while keeping your local machine clean. It certainly __can__ be done, but it adds complexity to your setup and workflow. If you choose to install a release version locally, please ensure you have a clear strategy for managing potential conflicts and resource usage.
Because NOMAD relies heavily on Docker, we actually recommend against installing a release version of the project on the same local machine where you are developing. This can lead to conflicts with ports, volumes, and other resources. Instead, you can run your development version in a separate Docker environment while keeping your local machine clean. It certainly __can__ be done, but it adds complexity to your setup and workflow. If you choose to install a release version locally, please ensure you have a clear strategy for managing potential conflicts and resource usage.
---
@ -92,7 +93,7 @@ Because Nomad relies heavily on Docker, we actually recommend against installing
git checkout -b feature/add-new-tool
```
3. **Make your changes.** Follow existing code style and conventions. Test your changes locally against a running N.O.M.A.D. instance before submitting.
3. **Make your changes.** Follow existing code style and conventions. Test your changes locally against a running NOMAD instance before submitting.
4. **Add release notes** (see [Release Notes](#release-notes) below).
@ -102,6 +103,34 @@ Because Nomad relies heavily on Docker, we actually recommend against installing
---
## UI Consistency
N.O.M.A.D.'s guiding principle is that **user-friendliness is paramount**: a control that looks or behaves differently from the rest of the app reads as broken to a non-technical user. New frontend (inertia/React) work should be visually and behaviorally uniform with what is already there. Before adding a UI element, look at its neighbors and reuse the shared building blocks rather than hand-rolling a one-off.
**Reuse the shared components** in `admin/inertia/components/` (and `.../components/inputs/`):
| Need | Use | Not |
|------|-----|-----|
| Binary on/off setting | `Switch` | a raw `<input type="checkbox">` |
| Explanatory hover help | `InfoTooltip` | a raw `title=` attribute or a bespoke tooltip |
| Modal / confirmation dialog | `StyledModal` | a hand-built overlay |
| Text field | `Input` | a bare `<input>` |
| Section heading | `StyledSectionHeader` | ad-hoc heading markup |
Grep for an existing component before building a new one.
**Match the neighbors.** Copy the exact classes and conventions of adjacent elements:
- **Labels:** match punctuation and casing of sibling labels. If the field beside yours reads `Model:` (with a colon), yours should read `Thinking:`, not `Thinking`. Reuse the same typography tokens (e.g. `text-sm text-text-secondary`).
- **Theme:** use design tokens (`text-*`, `bg-*`, `border-*`) so the element works in light and dark mode. Never hardcode colors.
- **Placement:** make sure popovers and tooltips are not clipped or crushed against a viewport edge, including when the trigger sits near an edge of the screen.
**When a raw control is fine.** These conventions are about matching intent, not banning primitives. A raw checkbox is appropriate for a multi-select list or a consent box; radio groups and native selects are fine where a shared component does not exist. The point is to reach for the shared component when your case matches its intent (a binary setting toggle should be a `Switch`), not to eliminate primitives.
**Test UI changes in a real browser.** Most of these conventions are judgment calls that tooling cannot fully enforce, so the single most important habit is to load your change in a browser against a running instance before submitting. Several classes of issue (clipped or cramped tooltips, layout breaking at different window widths, blank-screen render errors) are invisible to type-checking and only show up when you actually look at the page. Check the states that should appear *and* the states that should be hidden, and try more than one window width when layout or positioning is involved.
---
## Commit Messages
This project uses [Conventional Commits](https://www.conventionalcommits.org/). All commit messages must follow this format:
@ -166,10 +195,10 @@ This project uses [Semantic Versioning](https://semver.org/). Versions are manag
Have questions or want to discuss ideas before opening an issue? Join the community:
- **Discord:** [Join the Crosstalk Solutions server](https://discord.com/invite/crosstalksolutions) — the best place to get help, share your builds, and talk with other N.O.M.A.D. users
- **Discord:** [Join the Crosstalk Solutions server](https://discord.com/invite/crosstalksolutions) — the best place to get help, share your builds, and talk with other NOMAD users
- **Website:** [www.projectnomad.us](https://www.projectnomad.us)
- **Benchmark Leaderboard:** [benchmark.projectnomad.us](https://benchmark.projectnomad.us)
---
*Project N.O.M.A.D. is licensed under the [Apache License 2.0](LICENSE).*
*Project NOMAD is licensed under the [Apache License 2.0](LICENSE).*

View File

@ -62,8 +62,8 @@ RUN set -eux; \
/usr/local/bin/pmtiles version
# Labels
LABEL org.opencontainers.image.title="Project N.O.M.A.D" \
org.opencontainers.image.description="The Project N.O.M.A.D Official Docker image" \
LABEL org.opencontainers.image.title="Project NOMAD" \
org.opencontainers.image.description="The Project NOMAD Official Docker image" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.revision="${VCS_REF}" \

24
FAQ.md
View File

@ -1,6 +1,6 @@
# Frequently Asked Questions (FAQ)
Find answers to some of the most common questions about Project N.O.M.A.D.
Find answers to some of the most common questions about Project NOMAD
## Can I customize the port(s) that NOMAD uses?
@ -26,30 +26,30 @@ Long answer: Custom storage paths, mount points, and external drives (like iSCSI
## Why does NOMAD require a Debian-based OS?
Project N.O.M.A.D. is currently designed to run on Debian-based Linux distributions (with Ubuntu being the recommended distro) because our installation scripts and Docker configurations are optimized for this environment. While it's technically possible to run the Docker containers on other operating systems that support Docker, we have not tested or optimized the installation process for non-Debian-based systems, so we cannot guarantee a smooth experience on those platforms at this time.
Project NOMAD is currently designed to run on Debian-based Linux distributions (with Ubuntu being the recommended distro) because our installation scripts and Docker configurations are optimized for this environment. While it's technically possible to run the Docker containers on other operating systems that support Docker, we have not tested or optimized the installation process for non-Debian-based systems, so we cannot guarantee a smooth experience on those platforms at this time.
Support for other operating systems will come in the future, but because our development resources are limited as a free and open-source project, we needed to prioritize our efforts and focus on a narrower set of supported platforms for the initial release. We chose Debian-based Linux as our starting point because it's widely used, easy to spin up, and provides a stable environment for running Docker containers.
For Windows users, the [WSL2 install guide](https://www.projectnomad.us/install/wsl2) provides a community-supported path. Community members have also published guides for other platforms (e.g. macOS) in our Discord community and [Github Discussions](https://github.com/Crosstalk-Solutions/project-nomad/discussions), so if you're interested in running N.O.M.A.D. on a non-Debian-based system, we recommend checking there for any available resources or guides. However, keep in mind that if you choose to run N.O.M.A.D. on a non-Debian-based system, you may encounter issues that we won't be able to provide support for, and you may need to have a higher level of technical expertise to troubleshoot and resolve any problems that arise.
For Windows users, the [WSL2 install guide](https://www.projectnomad.us/install/wsl2) provides a community-supported path. Community members have also published guides for other platforms (e.g. macOS) in our Discord community and [Github Discussions](https://github.com/Crosstalk-Solutions/project-nomad/discussions), so if you're interested in running NOMAD on a non-Debian-based system, we recommend checking there for any available resources or guides. However, keep in mind that if you choose to run NOMAD on a non-Debian-based system, you may encounter issues that we won't be able to provide support for, and you may need to have a higher level of technical expertise to troubleshoot and resolve any problems that arise.
## Can I run NOMAD on a Raspberry Pi or other ARM-based device?
Project N.O.M.A.D. is currently designed to run on x86-64 architecture, and we have not yet tested or optimized it for ARM-based devices like the Raspberry Pi (and have not published any official images for ARM architecture).
Project NOMAD is currently designed to run on x86-64 architecture, and we have not yet tested or optimized it for ARM-based devices like the Raspberry Pi (and have not published any official images for ARM architecture).
Support for ARM-based devices is on our roadmap, but our initial focus was on x86-64 hardware due to its widespread use and compatibility with a wide range of applications.
Community members have forked and published their own ARM-compatible images and installation guides for running N.O.M.A.D. on Raspberry Pi and other ARM-based devices in our Discord community and [Github Discussions](https://github.com/Crosstalk-Solutions/project-nomad/discussions), but these are not officially supported by the core development team, and we cannot guarantee their functionality or provide support for any issues that arise when using these community-created resources.
Community members have forked and published their own ARM-compatible images and installation guides for running NOMAD on Raspberry Pi and other ARM-based devices in our Discord community and [Github Discussions](https://github.com/Crosstalk-Solutions/project-nomad/discussions), but these are not officially supported by the core development team, and we cannot guarantee their functionality or provide support for any issues that arise when using these community-created resources.
## What are the hardware requirements for running NOMAD?
Project N.O.M.A.D. itself is quite lightweight and can run on even modest x86-64 hardware, but the tools and resources you choose to install with N.O.M.A.D. will determine the specs required for your unique deployment. Please see the [Hardware Guide](https://www.projectnomad.us/hardware) for detailed build recommendations at various price points.
Project NOMAD itself is quite lightweight and can run on even modest x86-64 hardware, but the tools and resources you choose to install with NOMAD will determine the specs required for your unique deployment. Please see the [Hardware Guide](https://www.projectnomad.us/hardware) for detailed build recommendations at various price points.
## Does NOMAD support languages other than English?
As of March 2026, Project N.O.M.A.D.'s UI is only available in English, and the majority of the tools and resources available through N.O.M.A.D. are also primarily in English. However, we have multi-language support on our roadmap for a future release, and we are actively working on adding support for additional languages both in the UI and in the available tools/resources. If you're interested in contributing to this effort, please check out our [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines on how to get involved.
As of March 2026, Project NOMAD's UI is only available in English, and the majority of the tools and resources available through NOMAD are also primarily in English. However, we have multi-language support on our roadmap for a future release, and we are actively working on adding support for additional languages both in the UI and in the available tools/resources. If you're interested in contributing to this effort, please check out our [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines on how to get involved.
## What technologies is NOMAD built with?
Project N.O.M.A.D. is built using a combination of technologies, including:
Project NOMAD is built using a combination of technologies, including:
- **Docker:** for containerization of the Command Center and its dependencies
- **Node.js & TypeScript:** for the backend of the Command Center, particularly the [AdonisJS](https://adonisjs.com/) framework
- **React:** for the frontend of the Command Center, utilizing [Vite](https://vitejs.dev/) and [Inertia.js](https://inertiajs.com/) under the hood
@ -59,7 +59,7 @@ Project N.O.M.A.D. is built using a combination of technologies, including:
NOMAD makes use of the Docker-outside-of-Docker ("DooD") pattern, which allows the Command Center to manage and orchestrate other Docker containers on the host machine without needing to run Docker itself inside a container. This approach provides better performance and compatibility with a wider range of host environments while still allowing for powerful container management capabilities through the Command Center's UI.
## Can I run NOMAD if I have existing Docker containers on my machine?
Yes, you can safely run Project N.O.M.A.D. on a machine that already has existing Docker containers. NOMAD is designed to coexist with other Docker containers and will not interfere with them as long as there are no port conflicts or resource constraints.
Yes, you can safely run Project NOMAD on a machine that already has existing Docker containers. NOMAD is designed to coexist with other Docker containers and will not interfere with them as long as there are no port conflicts or resource constraints.
All of NOMAD's containers are prefixed with `nomad_` in their names, so they can be easily identified and managed separately from any other containers you may have running. Just make sure to review the ports that NOMAD's core services (Command Center, MySQL, Redis) use during installation and adjust them if necessary to avoid conflicts with your existing containers.
@ -76,17 +76,17 @@ NOMAD by default uses Ollama inside of a docker container to run LLM Models for
No, the AI features in NOMAD (Ollama, Qdrant, custom RAG pipeline, etc.) are all optional and not required to use the core functionality of NOMAD.
## Is NOMAD actually free? Are there any hidden costs?
Yes, Project N.O.M.A.D. is completely free and open-source software licensed under the Apache License 2.0. There are no hidden costs or fees associated with using NOMAD itself, and we don't have any plans to introduce "premium" features or paid tiers.
Yes, Project NOMAD is completely free and open-source software licensed under the Apache License 2.0. There are no hidden costs or fees associated with using NOMAD itself, and we don't have any plans to introduce "premium" features or paid tiers.
Aside from the cost of the hardware you choose to run it on, there are no costs associated with using NOMAD.
## Do you sell hardware or pre-built devices with NOMAD pre-installed?
No, we do not sell hardware or pre-built devices with NOMAD pre-installed at this time. Project N.O.M.A.D. is a free and open-source software project, and we provide detailed installation instructions and hardware recommendations for users to set up their own NOMAD instances on compatible hardware of their choice. The tradeoff to this DIY approach is some additional setup time and technical know-how required on the user's end, but it also allows for greater flexibility and customization in terms of hardware selection and configuration to best suit each user's unique needs, budget, and preferences.
No, we do not sell hardware or pre-built devices with NOMAD pre-installed at this time. Project NOMAD is a free and open-source software project, and we provide detailed installation instructions and hardware recommendations for users to set up their own NOMAD instances on compatible hardware of their choice. The tradeoff to this DIY approach is some additional setup time and technical know-how required on the user's end, but it also allows for greater flexibility and customization in terms of hardware selection and configuration to best suit each user's unique needs, budget, and preferences.
## How quickly are issues resolved when reported?
We strive to address and resolve issues as quickly as possible, but please keep in mind that Project N.O.M.A.D. is a free and open-source project maintained by a small team of volunteers. We prioritize issues based on their severity, impact on users, and the resources required to resolve them. Critical issues that affect a large number of users are typically addressed more quickly, while less severe issues may take longer to resolve. Aside from the development efforts needed to address the issue, we do our best to conduct thorough testing and validation to ensure that any fix we implement doesn't introduce new issues or regressions, which also adds to the time it takes to resolve an issue.
We strive to address and resolve issues as quickly as possible, but please keep in mind that Project NOMAD is a free and open-source project maintained by a small team of volunteers. We prioritize issues based on their severity, impact on users, and the resources required to resolve them. Critical issues that affect a large number of users are typically addressed more quickly, while less severe issues may take longer to resolve. Aside from the development efforts needed to address the issue, we do our best to conduct thorough testing and validation to ensure that any fix we implement doesn't introduce new issues or regressions, which also adds to the time it takes to resolve an issue.
We also encourage community involvement in troubleshooting and resolving issues, so if you encounter a problem, please consider checking our Discord community and Github Discussions for potential solutions or workarounds while we work on an official fix.

View File

@ -1,10 +1,8 @@
<div align="center">
<img src="admin/public/project_nomad_logo.webp" width="200" height="200"/>
# Project N.O.M.A.D.
### Node for Offline Media, Archives, and Data
**Knowledge That Never Goes Offline**
# Project NOMAD
### Knowledge That Never Goes Offline
[![Website](https://img.shields.io/badge/Website-projectnomad.us-blue)](https://www.projectnomad.us)
[![Discord](https://img.shields.io/badge/Discord-Join%20Community-5865F2)](https://discord.com/invite/crosstalksolutions)
@ -14,10 +12,10 @@
---
Project N.O.M.A.D. is a self-contained, offline-first knowledge and education server packed with critical tools, knowledge, and AI to keep you informed and empowered — anytime, anywhere.
Project NOMAD is a self-contained, offline-first knowledge and education server packed with critical tools, knowledge, and AI to keep you informed and empowered — anytime, anywhere.
## Installation & Quickstart
Project N.O.M.A.D. can be installed on any Debian-based operating system (we recommend Ubuntu). Installation is completely terminal-based, and all tools and resources are designed to be accessed through the browser, so there's no need for a desktop environment if you'd rather setup N.O.M.A.D. as a "server" and access it through other clients.
Project NOMAD can be installed on any Debian-based operating system (we recommend Ubuntu). Installation is completely terminal-based, and all tools and resources are designed to be accessed through the browser, so there's no need for a desktop environment if you'd rather setup NOMAD as a "server" and access it through other clients.
*Note: sudo/root privileges are required to run the install script*
@ -30,7 +28,7 @@ curl -fsSL https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/r
sudo bash install_nomad.sh
```
Project N.O.M.A.D. is now installed on your device! Open a browser and navigate to `http://localhost:8080` (or `http://DEVICE_IP:8080`) to start exploring!
Project NOMAD is now installed on your device! Open a browser and navigate to `http://localhost:8080` (or `http://DEVICE_IP:8080`) to start exploring!
For a complete step-by-step walkthrough (including Ubuntu installation), see the [Installation Guide](https://www.projectnomad.us/install). For Windows users, see the [WSL2 install guide](https://www.projectnomad.us/install/wsl2) — community-supported path covering native Docker and Docker Desktop install routes.
@ -38,7 +36,7 @@ For a complete step-by-step walkthrough (including Ubuntu installation), see the
For more control over the installation process, copy and paste the [Docker Compose template](https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/management_compose.yaml) into a `docker-compose.yml` file and customize it to your liking (be sure to replace any placeholders with your actual values). Then, run `docker compose up -d` to start the Command Center and its dependencies. Note: this method is recommended for advanced users only, as it requires familiarity with Docker and manual configuration before starting.
## How It Works
N.O.M.A.D. is a management UI ("Command Center") and API that orchestrates a collection of containerized tools and resources via [Docker](https://www.docker.com/). It handles installation, configuration, and updates for everything — so you don't have to.
NOMAD is a management UI ("Command Center") and API that orchestrates a collection of containerized tools and resources via [Docker](https://www.docker.com/). It handles installation, configuration, and updates for everything — so you don't have to.
**Built-in capabilities include:**
- **AI Chat with Knowledge Base** — local AI chat powered by [Ollama](https://ollama.com/) or you can use OpenAI API compatible software such as LM Studio or llama.cpp, with document upload and semantic search (RAG via [Qdrant](https://qdrant.tech/))
@ -52,7 +50,7 @@ N.O.M.A.D. is a management UI ("Command Center") and API that orchestrates a col
- **Automatic Updates** — opt-in, hands-off updates for the core software, installed apps, and offline content, on a schedule you control
- **Easy Setup Wizard** — guided first-time configuration with curated content collections
N.O.M.A.D. also includes built-in tools like a Wikipedia content selector, ZIM library manager, and content explorer.
NOMAD also includes built-in tools like a Wikipedia content selector, ZIM library manager, and content explorer.
## What's Included
@ -68,12 +66,12 @@ N.O.M.A.D. also includes built-in tools like a Wikipedia content selector, ZIM l
| Supply Depot | Built-in | One-click app catalog + bring-your-own custom Docker containers |
## Device Requirements
While many similar offline survival computers are designed to be run on bare-minimum, lightweight hardware, Project N.O.M.A.D. is quite the opposite. To install and run the
While many similar offline survival computers are designed to be run on bare-minimum, lightweight hardware, Project NOMAD is quite the opposite. To install and run the
available AI tools, we highly encourage the use of a beefy, GPU-backed device to make the most of your install.
At its core, however, N.O.M.A.D. is still very lightweight. For a barebones installation of the management application itself, the following minimal specs are required:
At its core, however, NOMAD is still very lightweight. For a barebones installation of the management application itself, the following minimal specs are required:
*Note: Project N.O.M.A.D. is not sponsored by any hardware manufacturer and is designed to be as hardware-agnostic as possible. The hardware listed below is for example/comparison use only*
*Note: Project NOMAD is not sponsored by any hardware manufacturer and is designed to be as hardware-agnostic as possible. The hardware listed below is for example/comparison use only*
#### Minimum Specs
- Processor: 2 GHz dual-core processor or better
@ -94,30 +92,30 @@ To run LLMs and other included AI tools:
**For detailed build recommendations at three price points ($150$1,000+), see the [Hardware Guide](https://www.projectnomad.us/hardware).**
Again, Project N.O.M.A.D. itself is quite lightweight — it's the tools and resources you choose to install with N.O.M.A.D. that will determine the specs required for your unique deployment
Again, Project NOMAD itself is quite lightweight — it's the tools and resources you choose to install with NOMAD that will determine the specs required for your unique deployment
#### Running AI models on a different host
By default, N.O.M.A.D.'s installer will attempt to setup Ollama on the host when the AI Assistant is installed. However, if you would like to run the AI model on a different host, you can go to the settings of the AI assistant and input a URL for either an ollama or OpenAI-compatible API server (such as LM Studio).
By default, NOMAD's installer will attempt to setup Ollama on the host when the AI Assistant is installed. However, if you would like to run the AI model on a different host, you can go to the settings of the AI assistant and input a URL for either an ollama or OpenAI-compatible API server (such as LM Studio).
Note that if you use Ollama on a different host, you must start the server with this option: `OLLAMA_HOST=0.0.0.0`.
Ollama is the preferred way to use the AI assistant, as it has features such as model download that OpenAI API does not support. So when using LM Studio, for example, you will have to use LM Studio to download models.
You are responsible for the setup of Ollama/OpenAI server on the other host.
## Frequently Asked Questions (FAQ)
For answers to common questions about Project N.O.M.A.D., please see our [FAQ](FAQ.md) page.
For answers to common questions about Project NOMAD, please see our [FAQ](FAQ.md) page.
## About Internet Usage & Privacy
Project N.O.M.A.D. is designed for offline usage. An internet connection is only required during the initial installation (to download dependencies) and if you (the user) decide to download additional tools and resources at a later time. Otherwise, N.O.M.A.D. does not require an internet connection and has ZERO built-in telemetry.
Project NOMAD is designed for offline usage. An internet connection is only required during the initial installation (to download dependencies) and if you (the user) decide to download additional tools and resources at a later time. Otherwise, NOMAD does not require an internet connection and has ZERO built-in telemetry.
To test internet connectivity, N.O.M.A.D. first attempts to make a request to Cloudflare's utility endpoint, `https://1.1.1.1/cdn-cgi/trace`. If that endpoint is unreachable (for example, because your network blocks `1.1.1.1`), it falls back to other endpoints the application already contacts (the GitHub API and the Project N.O.M.A.D. API) and considers the connection online if any of them respond.
To test internet connectivity, NOMAD first attempts to make a request to Cloudflare's utility endpoint, `https://1.1.1.1/cdn-cgi/trace`. If that endpoint is unreachable (for example, because your network blocks `1.1.1.1`), it falls back to other endpoints the application already contacts (the GitHub API and the Project NOMAD API) and considers the connection online if any of them respond.
You can override the endpoint used for this check in two ways. The connectivity test URL can be configured from the UI under **Settings → Advanced** (stored locally on your instance), or you can set the `INTERNET_STATUS_TEST_URL` environment variable. When set, the environment variable always takes precedence over the UI-configured value. If neither is set, the built-in defaults above are used.
## About Security
By design, Project N.O.M.A.D. is intended to be open and available without hurdles — it includes no authentication. If you decide to connect your device to a local network after install (e.g. for allowing other devices to access its resources), you can block/open ports to control which services are exposed.
By design, Project NOMAD is intended to be open and available without hurdles — it includes no authentication. If you decide to connect your device to a local network after install (e.g. for allowing other devices to access its resources), you can block/open ports to control which services are exposed.
**Will authentication be added in the future?** Maybe. It's not currently a priority, but if there's enough demand for it, we may consider building in an optional authentication layer in a future release to support use cases where multiple users need access to the same instance but with different permission levels (e.g. family use with parental controls, classroom use with teacher/admin accounts, etc.). We have a suggestion for this on our public roadmap, so if this is something you'd like to see, please upvote it here: https://roadmap.projectnomad.us/posts/1/user-authentication-please-build-in-user-auth-with-admin-user-roles
For now, we recommend using network-level controls to manage access if you're planning to expose your N.O.M.A.D. instance to other devices on a local network. N.O.M.A.D. is not designed to be exposed directly to the internet, and we strongly advise against doing so unless you really know what you're doing, have taken appropriate security measures, and understand the risks involved.
For now, we recommend using network-level controls to manage access if you're planning to expose your NOMAD instance to other devices on a local network. NOMAD is not designed to be exposed directly to the internet, and we strongly advise against doing so unless you really know what you're doing, have taken appropriate security measures, and understand the risks involved.
## Contributing
Contributions are welcome and appreciated! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to contribute to the project.
@ -167,10 +165,10 @@ It prints the resolved decision — current version, whether the clock is inside
## License
Project N.O.M.A.D. is licensed under the [Apache License 2.0](LICENSE).
Project NOMAD is licensed under the [Apache License 2.0](LICENSE).
## Helper Scripts
Once installed, Project N.O.M.A.D. has a few helper scripts should you ever need to troubleshoot issues or perform maintenance that can't be done through the Command Center. All of these scripts are found in Project N.O.M.A.D.'s install directory, `/opt/project-nomad`
Once installed, Project NOMAD has a few helper scripts should you ever need to troubleshoot issues or perform maintenance that can't be done through the Command Center. All of these scripts are found in Project NOMAD's install directory, `/opt/project-nomad`
###

View File

@ -24,4 +24,8 @@ export default class DownloadsController {
async cancelJob({ params }: HttpContext) {
return this.downloadService.cancelJob(params.jobId)
}
async retryJob({ params }: HttpContext) {
return this.downloadService.retryFailedJob(params.jobId)
}
}

View File

@ -164,14 +164,32 @@ export default class OllamaController {
if (reqData.stream) {
logger.debug(`[OllamaController] Initiating streaming response for model: "${reqData.model}" with think: ${think}`)
// Headers already flushed above
const stream = await this.ollamaService.chatStream({ ...ollamaRequest, think, numCtx })
// Headers already flushed above.
// Abort the upstream generation if the client disconnects — otherwise an abandoned
// request keeps decoding server-side and, with Ollama's default OLLAMA_NUM_PARALLEL=1,
// blocks every later chat/RAG request until the model is manually stopped (#1065).
const abortController = new AbortController()
response.response.on('close', () => abortController.abort())
const stream = await this.ollamaService.chatStream({
...ollamaRequest,
think,
numCtx,
signal: abortController.signal,
})
let fullContent = ''
for await (const chunk of stream) {
if (chunk.message?.content) {
fullContent += chunk.message.content
try {
for await (const chunk of stream) {
if (chunk.message?.content) {
fullContent += chunk.message.content
}
response.response.write(`data: ${JSON.stringify(chunk)}\n\n`)
}
response.response.write(`data: ${JSON.stringify(chunk)}\n\n`)
} catch (err) {
if (abortController.signal.aborted) {
logger.debug('[OllamaController] Client disconnected; aborted upstream Ollama generation')
return
}
throw err
}
response.response.end()

View File

@ -21,6 +21,7 @@ import { SERVICE_NAMES } from '../../constants/service_names.js'
import { exec } from 'child_process'
import { promisify } from 'util'
import { readFile, mkdir, copyFile, chown, chmod, access, writeFile } from 'node:fs/promises'
import { randomBytes } from 'node:crypto'
import KVStore from '#models/kv_store'
import { BROADCAST_CHANNELS } from '../../constants/broadcast.js'
import { KIWIX_LIBRARY_CMD } from '../../constants/kiwix.js'
@ -807,9 +808,21 @@ export class DockerService {
if (hsaOverride) {
ollamaEnv.push(`HSA_OVERRIDE_GFX_VERSION=${hsaOverride}`)
}
// Ollama's scheduler drops integrated GPUs unless this is set (issue #1056), so
// AMD APUs (780M/890M/8060S) fall back to CPU-only despite correct device
// passthrough. It's a no-op on discrete AMD cards, so it's safe to set whenever
// AMD acceleration is configured.
ollamaEnv.push('OLLAMA_IGPU_ENABLE=1')
}
}
const appEnv: string[] = []
if (service.service_name === SERVICE_NAMES.HOMEBOX) {
// Homebox >= 0.26 panics at boot without a >= 32-byte pepper (#1043). Generate once,
// persist, and reuse so updates/reinstalls don't invalidate issued API keys.
appEnv.push(`HBOX_AUTH_API_KEY_PEPPER=${await this._resolveHomeboxPepper()}`)
}
this._broadcast(
service.service_name,
'creating',
@ -827,7 +840,7 @@ export class DockerService {
HostConfig: gpuHostConfig,
...(containerConfig?.WorkingDir && { WorkingDir: containerConfig.WorkingDir }),
...(containerConfig?.ExposedPorts && { ExposedPorts: containerConfig.ExposedPorts }),
Env: [...(containerConfig?.Env ?? []), ...ollamaEnv],
Env: [...(containerConfig?.Env ?? []), ...ollamaEnv, ...appEnv],
...(service.container_command ? { Cmd: service.container_command.split(' ') } : {}),
// Ensure container is attached to the Nomad docker network in production
...(process.env.NODE_ENV === 'production' && {
@ -1455,16 +1468,40 @@ export class DockerService {
* gfx1030 (RX 6800/6700/etc.), gfx1100/1101/1102 (RX 7900/7800/7600) are on AMD's
* official ROCm allowlist forcing an override on these breaks GPU discovery.
* gfx1035 / gfx1036 (RDNA 2 iGPUs like 680M) need 10.3.0 to coerce to gfx1030.
* gfx1103 / gfx1150 / gfx1151 (RDNA 3/3.5 iGPUs like 780M / 890M / Strix Halo) need 11.0.0.
* gfx1103 / gfx1150 / gfx1151 (RDNA 3/3.5 iGPUs like 780M / 890M / Strix Halo) are
* natively supported by the ROCm 7.2 the current ollama:rocm image bundles, so they
* need NO override. Forcing 11.0.0 coerces them to gfx1100's kernels unnecessary on
* the 890M and a source of faults on the 780M (gfx1100 WMMA instructions it lacks). See #1056.
*
* Resolution order:
* 1. KV `ai.amdHsaOverride` manual user override; accepts 'none' (disable) or a semver-style value.
* 2. Marker file `/app/storage/.nomad-amd-gfx` written by install_nomad.sh.
* 3. Default: '11.0.0' preserves prior behavior so existing iGPU users don't regress on
* upgrade. Discrete-card users on existing installs can opt out via the KV.
* 3. Default: none let ROCm discover the GPU natively. Users on hardware that still
* needs coercion can force a value via the KV. A hardcoded default gets more wrong
* as ROCm adds native targets, so null is the safer forward-looking default.
*
* Returns null when no override should be applied.
*/
/**
* Resolve the Homebox API-key pepper, generating and persisting one on first use.
*
* Homebox >= 0.26 panics at boot unless HBOX_AUTH_API_KEY_PEPPER is set to a value of at
* least 32 bytes (#1043). The pepper must be STABLE across updates and reinstalls:
* rotating it invalidates every API key a user has issued from Homebox. So we generate it
* once and persist it in the KV store, then reuse it for the life of the install.
*/
private async _resolveHomeboxPepper(): Promise<string> {
const existing = await KVStore.getValue('apps.homebox.apiKeyPepper')
if (typeof existing === 'string' && existing.length >= 32) {
return existing
}
// 48 raw bytes -> 64 base64 chars, comfortably over Homebox's 32-byte floor.
const pepper = randomBytes(48).toString('base64')
await KVStore.setValue('apps.homebox.apiKeyPepper', pepper)
logger.info('[DockerService] Generated and persisted Homebox API key pepper')
return pepper
}
private async _resolveAmdHsaOverride(): Promise<string | null> {
const manualRaw = await KVStore.getValue('ai.amdHsaOverride')
if (manualRaw !== null && manualRaw !== undefined && String(manualRaw).trim() !== '') {
@ -1490,8 +1527,8 @@ export class DockerService {
// install_nomad.sh. Fall through to the default.
}
logger.info('[DockerService] No AMD gfx marker; defaulting HSA override to 11.0.0 for backward compatibility')
return '11.0.0'
logger.info('[DockerService] No AMD gfx marker; applying no HSA override (native ROCm discovery)')
return null
}
private _mapGfxToHsaOverride(gfx: string): string | null {
@ -1499,15 +1536,18 @@ export class DockerService {
if (gfx === 'gfx1030' || gfx === 'gfx1100' || gfx === 'gfx1101' || gfx === 'gfx1102') {
return null
}
// RDNA 2 variants + iGPUs (gfx1031..gfx1036, e.g. Rembrandt 680M)
// RDNA 2 variants + iGPUs (gfx1031..gfx1036, e.g. Rembrandt 680M) — not natively
// supported, still need coercion to gfx1030.
if (/^gfx103[1-6]$/.test(gfx)) {
return '10.3.0'
}
// RDNA 3 / 3.5 mobile parts (Phoenix 780M = gfx1103, Strix 890M = gfx1150, Strix Halo = gfx1151)
// RDNA 3 / 3.5 mobile parts (Phoenix 780M = gfx1103, Strix 890M = gfx1150, Strix Halo =
// gfx1151) are native under ROCm 7.2 — no override (forcing 11.0.0 faults the 780M). See #1056.
if (gfx === 'gfx1103' || gfx === 'gfx1150' || gfx === 'gfx1151') {
return '11.0.0'
return null
}
return '11.0.0'
// Unknown/newer target: prefer native discovery over a coercion that's likely wrong.
return null
}
/**
@ -1688,10 +1728,25 @@ export class DockerService {
let finalEnv = baseEnv
if (updatedAmdGpuConfigured) {
const hsaOverride = await this._resolveAmdHsaOverride()
finalEnv = baseEnv.filter((e: string) => !e.startsWith('HSA_OVERRIDE_GFX_VERSION='))
finalEnv = baseEnv.filter(
(e: string) =>
!e.startsWith('HSA_OVERRIDE_GFX_VERSION=') && !e.startsWith('OLLAMA_IGPU_ENABLE=')
)
if (hsaOverride) {
finalEnv.push(`HSA_OVERRIDE_GFX_VERSION=${hsaOverride}`)
}
// Re-assert the iGPU flag so updates from a container provisioned before the
// issue #1056 fix (which wouldn't have it in the captured env) pick it up.
finalEnv.push('OLLAMA_IGPU_ENABLE=1')
}
// Heal a Homebox container that predates the #1043 fix: inject the persisted pepper on
// update if it's missing, so an update (not just a force-reinstall) fixes the crash loop.
if (
serviceName === SERVICE_NAMES.HOMEBOX &&
!finalEnv.some((e: string) => e.startsWith('HBOX_AUTH_API_KEY_PEPPER='))
) {
finalEnv = [...finalEnv, `HBOX_AUTH_API_KEY_PEPPER=${await this._resolveHomeboxPepper()}`]
}
const newContainerConfig: any = {
@ -2141,6 +2196,17 @@ export class DockerService {
await this.pullImage(service.container_image)
}
// Runtime-injected secrets (e.g. Homebox's API-key pepper, #1043) live in the KV store, not
// in container_config. This path rebuilds Env from container_config alone, so re-inject them
// here — otherwise an Edit or in-place recreate drops the pepper and Homebox crash-loops again.
let recreateEnv: string[] = containerConfig?.Env ?? []
if (
serviceName === SERVICE_NAMES.HOMEBOX &&
!recreateEnv.some((e: string) => e.startsWith('HBOX_AUTH_API_KEY_PEPPER='))
) {
recreateEnv = [...recreateEnv, `HBOX_AUTH_API_KEY_PEPPER=${await this._resolveHomeboxPepper()}`]
}
const newContainer = await this.docker.createContainer({
Image: service.container_image,
name: serviceName,
@ -2152,7 +2218,7 @@ export class DockerService {
...(containerConfig?.User && { User: containerConfig.User }),
HostConfig: containerConfig?.HostConfig ?? {},
...(containerConfig?.ExposedPorts && { ExposedPorts: containerConfig.ExposedPorts }),
...(containerConfig?.Env && { Env: containerConfig.Env }),
...(recreateEnv.length ? { Env: recreateEnv } : {}),
...(service.container_command ? { Cmd: service.container_command.split(' ') } : {}),
...(process.env.NODE_ENV === 'production' && {
NetworkingConfig: { EndpointsConfig: { [DockerService.NOMAD_NETWORK]: {} } },

View File

@ -4,7 +4,7 @@ import { RunDownloadJob } from '#jobs/run_download_job'
import { RunExtractPmtilesJob } from '#jobs/run_extract_pmtiles_job'
import type { RunExtractPmtilesJobParams } from '#jobs/run_extract_pmtiles_job'
import { DownloadModelJob } from '#jobs/download_model_job'
import { DownloadJobWithProgress, DownloadProgressData } from '../../types/downloads.js'
import { DownloadJobWithProgress, DownloadProgressData, RunDownloadJobParams } from '../../types/downloads.js'
import type { Job, Queue } from 'bullmq'
import { normalize } from 'path'
import { deleteFileIfExists } from '../utils/fs.js'
@ -137,6 +137,40 @@ export class DownloadService {
}
}
async retryFailedJob(jobId: string): Promise<{ success: boolean; message: string }> {
// Search both the file download queue and the model download queue
for (const queueName of [RunDownloadJob.queue, DownloadModelJob.queue]) {
const queue = this.queueService.getQueue(queueName)
const job = await queue.getJob(jobId)
if (job) {
// For Ollama model downloads, re-dispatch with the model name
if (queueName === DownloadModelJob.queue) {
const modelName = job.data.modelName
if (!modelName) {
return { success: false, message: 'Cannot retry: model name not found in job data' }
}
await DownloadModelJob.dispatch({ modelName })
await job.remove().catch(() => {})
return { success: true, message: `Retrying download for model ${modelName}` }
}
// For file downloads (zim, map, etc.), re-dispatch with original params
const params = job.data as RunDownloadJobParams
if (!params.url || !params.filepath) {
return { success: false, message: 'Cannot retry: missing URL or filepath in job data' }
}
// Remove the old failed job, then dispatch a fresh one
await job.remove().catch(() => {})
await RunDownloadJob.dispatch(params)
return { success: true, message: `Retrying download for ${params.url}` }
}
}
return { success: false, message: 'Failed job not found. It may have already been dismissed.' }
}
async cancelJob(jobId: string): Promise<{ success: boolean; message: string }> {
const queue = this.queueService.getQueue(RunDownloadJob.queue)
const job = await queue.getJob(jobId)

View File

@ -45,6 +45,9 @@ type ChatInput = {
think?: boolean | 'medium'
stream?: boolean
numCtx?: number
// Aborts the upstream request when the client disconnects, so an abandoned generation
// doesn't keep decoding server-side and block Ollama's single parallel slot (#1065).
signal?: AbortSignal
}
@inject()
@ -333,13 +336,15 @@ export class OllamaService {
params.num_ctx = chatRequest.numCtx
}
const response = await this.openai.chat.completions.create(params)
const response = await this.openai.chat.completions.create(params, { signal: chatRequest.signal })
const choice = response.choices[0]
return {
message: {
content: choice.message.content ?? '',
thinking: (choice.message as any).thinking ?? undefined,
// Ollama's OpenAI-compat endpoint (/v1) emits thinking as `reasoning`; its native
// shape uses `thinking`. Read both so thinking is never silently dropped (#1065).
thinking: (choice.message as any).thinking ?? (choice.message as any).reasoning ?? undefined,
},
done: true,
model: response.model,
@ -364,7 +369,9 @@ export class OllamaService {
params.num_ctx = chatRequest.numCtx
}
const stream = (await this.openai.chat.completions.create(params)) as unknown as Stream<ChatCompletionChunk>
const stream = (await this.openai.chat.completions.create(params, {
signal: chatRequest.signal,
})) as unknown as Stream<ChatCompletionChunk>
// Returns how many trailing chars of `text` could be the start of `tag`
function partialTagSuffix(tag: string, text: string): number {
@ -383,7 +390,8 @@ export class OllamaService {
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const nativeThinking: string = (delta as any)?.thinking ?? ''
// /v1 emits thinking as `reasoning`; native Ollama uses `thinking`. Read both (#1065).
const nativeThinking: string = (delta as any)?.thinking ?? (delta as any)?.reasoning ?? ''
const rawContent: string = delta?.content ?? ''
// Parse <think> tags out of the content stream

View File

@ -38,7 +38,7 @@ export class SystemService {
async getInternetStatus(): Promise<boolean> {
// Primary endpoint stays Cloudflare's privacy-respecting utility endpoint.
// The fallbacks are hosts the application already contacts elsewhere
// (GitHub API for update checks, the Project N.O.M.A.D. API for release-note
// (GitHub API for update checks, the Project NOMAD API for release-note
// subscriptions), so no new third-party services are introduced. They exist
// to avoid false "offline" reports on networks that block 1.1.1.1.
const DEFAULT_TEST_URLS = [

View File

@ -1,6 +1,7 @@
import { Archive, Entry } from '@openzim/libzim'
import * as cheerio from 'cheerio'
import { HTML_SELECTORS_TO_REMOVE, NON_CONTENT_HEADING_PATTERNS } from '../../constants/zim_extraction.js'
import { extractStructuredContent } from '../utils/zim_html.js'
import logger from '@adonisjs/core/services/logger'
import { ExtractZIMChunkingStrategy, ExtractZIMContentOptions, ZIMContentChunk, ZIMArchiveMetadata } from '../../types/zim.js'
import { randomUUID } from 'node:crypto'
@ -115,7 +116,7 @@ export class ZIMExtractionService {
let chunks: ZIMContentChunk[]
if (strategy === 'structured') {
const structured = this.extractStructuredContent(html)
const structured = extractStructuredContent(html)
chunks = structured.sections.map(s => ({
text: s.text,
articleTitle,
@ -210,76 +211,6 @@ export class ZIMExtractionService {
}
}
private extractStructuredContent(html: string) {
const $ = cheerio.load(html);
const title = $('h1').first().text().trim() || $('title').text().trim();
// Extract sections with their headings and heading levels
const sections: Array<{ heading: string; text: string; level: number }> = [];
let currentSection = { heading: 'Introduction', content: [] as string[], level: 2 };
// Walk the full DOM rather than only direct children of <body>. Modern ZIMs (Devdocs,
// Wikipedia, FreeCodeCamp, etc.) wrap article content in a container div, which under
// .children() would be a single non-heading/non-paragraph element and yield zero sections.
$('body').find('h2, h3, h4, p, ul, ol, dl, table').each((_, element) => {
const $el = $(element);
const tagName = element.tagName?.toLowerCase();
if (['h2', 'h3', 'h4'].includes(tagName)) {
// Save current section if it has content
if (currentSection.content.length > 0) {
sections.push({
heading: currentSection.heading,
text: currentSection.content.join(' ').replace(/\s+/g, ' ').trim(),
level: currentSection.level,
});
}
// Start new section
const level = parseInt(tagName.substring(1)); // Extract number from h2, h3, h4
currentSection = {
heading: $el.text().replace(/\[edit\]/gi, '').trim(),
content: [],
level,
};
} else if (['p', 'ul', 'ol', 'dl', 'table'].includes(tagName)) {
const text = $el.text().trim();
if (text.length > 0) {
currentSection.content.push(text);
}
}
});
// Push the last section if it has content
if (currentSection.content.length > 0) {
sections.push({
heading: currentSection.heading,
text: currentSection.content.join(' ').replace(/\s+/g, ' ').trim(),
level: currentSection.level,
});
}
// Fallback: if the selector walk produced no sections but the body has meaningful
// text (unusual structure, minimal markup), emit one section with the full body text
// so the article still contributes to the knowledge base.
if (sections.length === 0) {
const bodyText = $('body').text().replace(/\s+/g, ' ').trim();
if (bodyText.length > 0) {
sections.push({
heading: title || 'Content',
text: bodyText,
level: 2,
});
}
}
return {
title,
sections,
fullText: sections.map(s => `${s.heading}\n${s.text}`).join('\n\n'),
};
}
private hasStructuredHeadings(html: string): boolean {
const $ = cheerio.load(html);

View File

@ -164,6 +164,13 @@ export class ZimService {
download_url,
author: entry.author.name,
file_name,
language: entry.language,
category: entry.category,
tags: entry.tags,
article_count: entry.articleCount,
media_count: entry.mediaCount,
publisher: entry.publisher?.name,
issued: entry['dc:issued'],
})
}

127
admin/app/utils/zim_html.ts Normal file
View File

@ -0,0 +1,127 @@
import * as cheerio from 'cheerio'
import { NON_CONTENT_HEADING_PATTERNS } from '../../constants/zim_extraction.js'
export interface ZIMSection {
heading: string
text: string
level: number
}
export interface StructuredContent {
title: string
sections: ZIMSection[]
fullText: string
}
/**
* True when a section heading is one of the low-signal boilerplate headings
* (See also / References / External links / etc.). Sections under these
* headings are reference apparatus, not article content, and shouldn't reach
* the embedder. (#902)
*/
export function isNonContentHeading(heading: string): boolean {
return NON_CONTENT_HEADING_PATTERNS.some((pattern) => pattern.test(heading))
}
/**
* Render an HTML <table> into delimited text. cheerio's `.text()` concatenates
* every cell with no separators ("AgeDoseAdult500mg" word salad), which is
* unsearchable and pollutes embeddings. Instead, join cells with " | " and rows
* with newlines so row/column structure survives into the chunk. (#902)
*/
export function tableToText($: cheerio.CheerioAPI, table: any): string {
const rows: string[] = []
$(table)
.find('tr')
.each((_, tr) => {
const cells = $(tr)
.find('th, td')
.map((__, cell) => $(cell).text().replace(/\s+/g, ' ').trim())
.get()
.filter((cell) => cell.length > 0)
if (cells.length > 0) {
rows.push(cells.join(' | '))
}
})
return rows.join('\n')
}
/**
* Break a cleaned article's HTML into heading-delimited sections for chunking.
* Skips non-content sections (References, See also, ...) at emit time and
* renders tables as delimited text rather than concatenated cell soup. (#902)
*/
export function extractStructuredContent(html: string): StructuredContent {
const $ = cheerio.load(html)
const title = $('h1').first().text().trim() || $('title').text().trim()
const sections: ZIMSection[] = []
let currentSection = { heading: 'Introduction', content: [] as string[], level: 2, skip: false }
const flushSection = () => {
if (!currentSection.skip && currentSection.content.length > 0) {
sections.push({
heading: currentSection.heading,
text: currentSection.content.join(' ').replace(/\s+/g, ' ').trim(),
level: currentSection.level,
})
}
}
// Walk the full DOM rather than only direct children of <body>. Modern ZIMs (Devdocs,
// Wikipedia, FreeCodeCamp, etc.) wrap article content in a container div, which under
// .children() would be a single non-heading/non-paragraph element and yield zero sections.
$('body')
.find('h2, h3, h4, p, ul, ol, dl, table')
.each((_, element) => {
const $el = $(element)
const tagName = element.tagName?.toLowerCase()
if (['h2', 'h3', 'h4'].includes(tagName)) {
// Save the section we just finished, then open the next one.
flushSection()
const heading = $el
.text()
.replace(/\[edit\]/gi, '')
.trim()
const level = Number.parseInt(tagName.substring(1)) // Extract number from h2, h3, h4
currentSection = {
heading,
content: [],
level,
skip: isNonContentHeading(heading),
}
} else if (['p', 'ul', 'ol', 'dl', 'table'].includes(tagName)) {
// Don't bother collecting content for a section we're going to drop.
if (currentSection.skip) return
const text = tagName === 'table' ? tableToText($, element) : $el.text().trim()
if (text.length > 0) {
currentSection.content.push(text)
}
}
})
// Push the last section if it has content
flushSection()
// Fallback: if the selector walk produced no sections but the body has meaningful
// text (unusual structure, minimal markup), emit one section with the full body text
// so the article still contributes to the knowledge base.
if (sections.length === 0) {
const bodyText = $('body').text().replace(/\s+/g, ' ').trim()
if (bodyText.length > 0) {
sections.push({
heading: title || 'Content',
text: bodyText,
level: 2,
})
}
}
return {
title,
sections,
fullText: sections.map((s) => `${s.heading}\n${s.text}`).join('\n\n'),
}
}

View File

@ -0,0 +1,37 @@
import { BaseSchema } from '@adonisjs/lucid/schema'
import { SERVICE_NAMES } from '../../constants/service_names.js'
export default class extends BaseSchema {
protected tableName = 'services'
async up() {
// Sunset the Meshtastic Daemon catalog entry. It was removed from the seeder's DEFAULT_SERVICES
// because it can't work without hands-on setup (the user's radio MAC address, etc.), so leaving
// the card in the catalog only offers a broken install. The seeder is additive + sync-existing
// and never deletes, so every deployment seeded while it was in the catalog (all early-access
// boxes) keeps an orphaned `nomad_meshtasticd` row and still shows the non-functional card.
// Mirror the legacy-Kolibri sunset; the `is_deprecated` column already exists from that migration.
this.defer(async (db) => {
// Never installed → just an orphaned catalog row; drop it outright so the card disappears.
await db
.from(this.tableName)
.where('service_name', SERVICE_NAMES.MESHTASTICD)
.where('installed', false)
.delete()
// Currently installed (rare) → keep the row so it stays Nomad's handle to stop/uninstall the
// container, but flag it deprecated: it drops out of the catalog (see SystemService.getServices)
// and shows a "Legacy" badge. Honors the "we don't remove pre-installed apps" policy.
await db
.from(this.tableName)
.where('service_name', SERVICE_NAMES.MESHTASTICD)
.where('installed', true)
.update({ is_deprecated: true })
})
}
async down() {
// The orphaned-row deletion is a one-way data change and is not restored here. The is_deprecated
// column is owned by the legacy-Kolibri migration, so there is nothing schema-level to revert.
}
}

View File

@ -1,12 +1,12 @@
# About Project N.O.M.A.D.
# About Project NOMAD
Project N.O.M.A.D. (Node for Offline Media, Archives, and Data; "Nomad" for short) is a project started in 2025 by Chris Sherwood of [Crosstalk Solutions, LLC](https://crosstalksolutions.com). The goal of the project is not to create just another utility for storing offline resources, but rather to allow users to run their own ultimate "survival computer".
Project NOMAD ("NOMAD" for short) is a project started in 2025 by Chris Sherwood of [Crosstalk Solutions, LLC](https://crosstalksolutions.com). The goal of the project is not to create just another utility for storing offline resources, but rather to allow users to run their own ultimate "survival computer". The name started as a backronym, Node for Offline Maps, Archives, and Data, but these days we just call it NOMAD.
While many similar offline survival computers are designed to be run on bare-minimum, lightweight hardware, Project N.O.M.A.D. is quite the opposite. To install and run the available AI tools, we highly encourage the use of a beefy, GPU-backed device to make the most of your install. See the [Hardware Guide](https://www.projectnomad.us/hardware) for detailed build recommendations at three price points.
While many similar offline survival computers are designed to be run on bare-minimum, lightweight hardware, Project NOMAD is quite the opposite. To install and run the available AI tools, we highly encourage the use of a beefy, GPU-backed device to make the most of your install. See the [Hardware Guide](https://www.projectnomad.us/hardware) for detailed build recommendations at three price points.
Since its initial release, NOMAD has grown to include built-in AI chat with a Knowledge Base for document-aware responses, a System Benchmark with a community leaderboard, curated content collections with tiered options, and an Easy Setup Wizard to get new users up and running quickly.
Project N.O.M.A.D. is open source, released under the [Apache License 2.0](https://github.com/Crosstalk-Solutions/project-nomad/blob/main/LICENSE).
Project NOMAD is open source, released under the [Apache License 2.0](https://github.com/Crosstalk-Solutions/project-nomad/blob/main/LICENSE).
## Links

View File

@ -1,6 +1,6 @@
# API Reference
N.O.M.A.D. exposes a REST API for all operations. All endpoints are under `/api/` and return JSON.
NOMAD exposes a REST API for all operations. All endpoints are under `/api/` and return JSON.
---
@ -32,7 +32,7 @@ N.O.M.A.D. exposes a REST API for all operations. All endpoints are under `/api/
| GET | `/api/system/info` | CPU, memory, disk, and platform info |
| GET | `/api/system/internet-status` | Check internet connectivity |
| GET | `/api/system/debug-info` | Detailed debug information |
| GET | `/api/system/latest-version` | Check for the latest N.O.M.A.D. version |
| GET | `/api/system/latest-version` | Check for the latest NOMAD version |
| POST | `/api/system/update` | Trigger a system update |
| GET | `/api/system/update/status` | Get update progress |
| GET | `/api/system/update/logs` | Get update operation logs |

View File

@ -1,8 +1,8 @@
# Community Add-Ons
Project N.O.M.A.D. ships with a curated set of built-in tools and content, but the community has started building add-ons that extend the platform with specialized offline content packs. These are third-party projects, not maintained by the N.O.M.A.D. team. Install them at your own discretion, and please direct any bugs or feature requests to the add-on's own repository.
Project NOMAD ships with a curated set of built-in tools and content, but the community has started building add-ons that extend the platform with specialized offline content packs. These are third-party projects, not maintained by the NOMAD team. Install them at your own discretion, and please direct any bugs or feature requests to the add-on's own repository.
Have you built a NOMAD add-on? Open an issue on the [Project N.O.M.A.D. GitHub repository](https://github.com/Crosstalk-Solutions/project-nomad/issues/new) or send us a note through the [contact form on projectnomad.us](https://www.projectnomad.us/contact), and we'll review it for inclusion on this page.
Have you built a NOMAD add-on? Open an issue on the [Project NOMAD GitHub repository](https://github.com/Crosstalk-Solutions/project-nomad/issues/new) or send us a note through the [contact form on projectnomad.us](https://www.projectnomad.us/contact), and we'll review it for inclusion on this page.
---
@ -45,4 +45,4 @@ Expect the initial build to take anywhere from a few minutes to an hour or more
## A Note on Support
These add-ons are community-built and community-maintained. If something goes wrong with an install script or the content inside a ZIM, please open an issue on the add-on's own repository rather than Project N.O.M.A.D.'s. We're happy to help if the issue is with NOMAD itself, for example if Kiwix isn't picking up a new ZIM after an install, but we can't maintain or support third-party content.
These add-ons are community-built and community-maintained. If something goes wrong with an install script or the content inside a ZIM, please open an issue on the add-on's own repository rather than Project NOMAD's. We're happy to help if the issue is with NOMAD itself, for example if Kiwix isn't picking up a new ZIM after an install, but we can't maintain or support third-party content.

View File

@ -2,17 +2,17 @@
## General Questions
### What is N.O.M.A.D.?
N.O.M.A.D. (Node for Offline Media, Archives, and Data) is a personal server that gives you access to knowledge, education, and AI assistance without requiring an internet connection. It runs on your own hardware, keeping your data private and accessible anytime.
### What is NOMAD?
NOMAD is a personal server that gives you access to knowledge, education, and AI assistance without requiring an internet connection. It runs on your own hardware, keeping your data private and accessible anytime.
### Do I need internet to use N.O.M.A.D.?
### Do I need internet to use NOMAD?
No — that's the whole point. Once your content is downloaded, everything works offline. You only need internet to:
- Download new content
- Update the software
- Sync the latest versions of Wikipedia, maps, etc.
### What hardware do I need?
N.O.M.A.D. is designed for capable hardware, especially if you want to use the AI features. Recommended:
NOMAD is designed for capable hardware, especially if you want to use the AI features. Recommended:
- Modern multi-core CPU (AMD Ryzen 7 with Radeon graphics is the community sweet spot)
- 16GB+ RAM (32GB+ for best AI performance)
- SSD storage (size depends on content — 500GB minimum, 1TB+ recommended)
@ -54,7 +54,7 @@ Content is as current as when it was last downloaded. Wikipedia snapshots are ty
### Can I add my own files?
Yes — with the Knowledge Base. Upload PDFs, text files, and other documents to the [Knowledge Base](/knowledge-base), and the AI can reference them when answering your questions. This uses semantic search to find relevant information from your uploaded files.
For Kiwix content, N.O.M.A.D. uses standard ZIM files. For educational content, Kolibri uses its own channel format.
For Kiwix content, NOMAD uses standard ZIM files. For educational content, Kolibri uses its own channel format.
### What are curated collection tiers?
When selecting content in the Easy Setup wizard or Content Explorer, collections are organized into three tiers:
@ -136,19 +136,19 @@ Local AI requires significant computing power. To improve speed:
### How do I enable GPU acceleration for AI?
N.O.M.A.D. automatically detects NVIDIA GPUs when the NVIDIA Container Toolkit is installed on the host system. To set up GPU acceleration:
NOMAD automatically detects NVIDIA GPUs when the NVIDIA Container Toolkit is installed on the host system. To set up GPU acceleration:
1. **Install an NVIDIA GPU** in your server (if not already present)
2. **Install the NVIDIA Container Toolkit** on the host — follow the [official installation guide](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)
3. **Reinstall the AI Assistant** — Go to [Supply Depot](/supply-depot), find AI Assistant, and click **Force Reinstall**
N.O.M.A.D. will detect the GPU during installation and configure the AI to use it automatically. You'll see "NVIDIA container runtime detected" in the installation progress.
NOMAD will detect the GPU during installation and configure the AI to use it automatically. You'll see "NVIDIA container runtime detected" in the installation progress.
**Tip:** Run a [System Benchmark](/settings/benchmark) before and after to see the difference. GPU-accelerated systems typically see 100+ tokens per second vs 10-15 on CPU only.
### I added/changed my GPU but AI is still slow
When you add or swap a GPU, N.O.M.A.D. needs to reconfigure the AI container to use it:
When you add or swap a GPU, NOMAD needs to reconfigure the AI container to use it:
1. Make sure the **NVIDIA Container Toolkit** is installed on the host
2. Go to **[Supply Depot](/supply-depot)**
@ -158,7 +158,7 @@ Force Reinstall recreates the AI container with GPU support enabled. Without thi
### I see a "GPU passthrough not working" warning
N.O.M.A.D. checks whether your GPU is actually accessible inside the AI container. If a GPU is detected on the host but isn't working inside the container, you'll see a warning banner on the System Information and AI Settings pages. Click the **"Fix: Reinstall AI Assistant"** button to recreate the container with proper GPU access. This preserves your downloaded AI models.
NOMAD checks whether your GPU is actually accessible inside the AI container. If a GPU is detected on the host but isn't working inside the container, you'll see a warning banner on the System Information and AI Settings pages. Click the **"Fix: Reinstall AI Assistant"** button to recreate the container with proper GPU access. This preserves your downloaded AI models.
### AI Chat not available
@ -220,7 +220,7 @@ Kolibri passwords are managed separately:
## Updates and Maintenance
### How do I update N.O.M.A.D.?
### How do I update NOMAD?
1. Go to **Settings → Check for Updates**
2. If an update is available, click to install
3. The system will download updates and restart automatically
@ -233,8 +233,8 @@ Yes, while you have internet access. Updates include:
- Security improvements
- Performance enhancements
### Can N.O.M.A.D. update itself automatically?
Yes. N.O.M.A.D. can keep its software, its installed apps, and its content current on its own. Automatic updates are **opt-in and off by default** — you turn on what you want from **Settings → Updates** (and, for apps, a per-app toggle in the Supply Depot). They only run inside a time window you choose, after safety checks, and never apply major version jumps automatically. See the **[Updates guide](/docs/updates)** for a full walkthrough.
### Can NOMAD update itself automatically?
Yes. NOMAD can keep its software, its installed apps, and its content current on its own. Automatic updates are **opt-in and off by default** — you turn on what you want from **Settings → Updates** (and, for apps, a per-app toggle in the Supply Depot). They only run inside a time window you choose, after safety checks, and never apply major version jumps automatically. See the **[Updates guide](/docs/updates)** for a full walkthrough.
### How do I update content (Wikipedia, etc.)?
Content updates are separate from software updates:
@ -254,7 +254,7 @@ The system is designed to recover gracefully. If an update fails:
### Command-Line Maintenance
For advanced troubleshooting or when you can't access the web interface, N.O.M.A.D. includes helper scripts in `/opt/project-nomad`:
For advanced troubleshooting or when you can't access the web interface, NOMAD includes helper scripts in `/opt/project-nomad`:
**Start all services:**
```bash
@ -272,7 +272,7 @@ sudo bash /opt/project-nomad/update_nomad.sh
```
*Note: This updates the Command Center only, not individual apps. Update apps through the web interface.*
**Uninstall N.O.M.A.D.:**
**Uninstall NOMAD:**
```bash
curl -fsSL https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/uninstall_nomad.sh -o uninstall_nomad.sh
sudo bash uninstall_nomad.sh
@ -284,10 +284,10 @@ sudo bash uninstall_nomad.sh
## Privacy and Security
### Is my data private?
Yes. N.O.M.A.D. runs entirely on your hardware. Your searches, AI conversations, and usage data never leave your server.
Yes. NOMAD runs entirely on your hardware. Your searches, AI conversations, and usage data never leave your server.
### Can others access my server?
By default, N.O.M.A.D. is accessible on your local network. Anyone on the same network can access it. For public networks, consider additional security measures.
By default, NOMAD is accessible on your local network. Anyone on the same network can access it. For public networks, consider additional security measures.
### Does the AI send data anywhere?
No. The AI runs completely locally. Your conversations are not sent to any external service. The AI chat is built into the Command Center — there's no separate service to configure.

View File

@ -1,12 +1,12 @@
# Getting Started with N.O.M.A.D.
# Getting Started with NOMAD
This guide will help you get the most out of your N.O.M.A.D. server.
This guide will help you get the most out of your NOMAD server.
---
## Easy Setup Wizard
If this is your first time using N.O.M.A.D., the Easy Setup wizard will help you get everything configured.
If this is your first time using NOMAD, the Easy Setup wizard will help you get everything configured.
**[Launch Easy Setup →](/easy-setup)**
@ -66,7 +66,7 @@ The Education Platform provides complete educational courses that work offline.
![AI Chat interface](/docs/ai-chat.webp)
N.O.M.A.D. includes a built-in AI chat interface powered by Ollama. It runs entirely on your server — no internet needed, no data sent anywhere.
NOMAD includes a built-in AI chat interface powered by Ollama. It runs entirely on your server — no internet needed, no data sent anywhere.
**What can it do:**
- Answer questions on any topic
@ -84,7 +84,7 @@ N.O.M.A.D. includes a built-in AI chat interface powered by Ollama. It runs enti
**Note:** The AI Assistant must be installed first. Enable it during Easy Setup or install it from the [Supply Depot](/supply-depot).
**GPU Acceleration:** If your server has an NVIDIA GPU with the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) installed, N.O.M.A.D. will automatically use it for AI — dramatically faster responses (10-20x improvement). If you add a GPU later, go to the [Supply Depot](/supply-depot) and **Force Reinstall** the AI Assistant to enable it.
**GPU Acceleration:** If your server has an NVIDIA GPU with the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) installed, NOMAD will automatically use it for AI — dramatically faster responses (10-20x improvement). If you add a GPU later, go to the [Supply Depot](/supply-depot) and **Force Reinstall** the AI Assistant to enable it.
---
@ -150,7 +150,7 @@ As your needs change, you can add more content anytime:
![Content Explorer — browse and download Wikipedia packages and curated collections](/docs/content-explorer.webp)
N.O.M.A.D. includes a dedicated Wikipedia content management tool for browsing and downloading Wikipedia packages.
NOMAD includes a dedicated Wikipedia content management tool for browsing and downloading Wikipedia packages.
**How to use it:**
1. Go to **[Content Explorer →](/settings/zim/remote-explorer)**
@ -184,7 +184,7 @@ While you have internet, periodically check for updates:
Content updates (Wikipedia, maps, etc.) can be managed separately from software updates.
**Automatic updates:** N.O.M.A.D. can also keep itself current without you having to check. Software, installed apps, and content can each be set to update automatically on an opt-in basis, with safety checks and a time window you control. See the **[Updates guide](/docs/updates)** for the full picture.
**Automatic updates:** NOMAD can also keep itself current without you having to check. Software, installed apps, and content can each be set to update automatically on an opt-in basis, with safety checks and a time window you control. See the **[Updates guide](/docs/updates)** for the full picture.
**Early Access Channel:** Want the latest features before they hit stable? Enable the Early Access Channel from the Check for Updates page to receive release candidate builds. You can switch back to stable anytime.
@ -226,7 +226,7 @@ Check storage usage in **Settings → System**.
## Next Steps
You're ready to use N.O.M.A.D. Here are some things to try:
You're ready to use NOMAD Here are some things to try:
1. **Look something up** — Search for a topic in the Information Library
2. **Learn something** — Start a Khan Academy course in the Education Platform

View File

@ -1,10 +1,10 @@
# Welcome to Project N.O.M.A.D.
# Welcome to Project NOMAD
Your personal offline knowledge server is ready to use.
## What is N.O.M.A.D.?
## What is NOMAD?
**N.O.M.A.D.** stands for **Node for Offline Media, Archives, and Data**. It's your personal server for accessing knowledge, education, and AI assistance — even when you have no internet connection.
**NOMAD** is an offline-first knowledge and education server. It's your personal server for accessing knowledge, education, and AI assistance — even when you have no internet connection.
Think of it as having Wikipedia, Khan Academy, an AI assistant, and offline maps all in one place, running on hardware you control.
@ -46,7 +46,7 @@ Run a System Benchmark to see how your hardware performs and compare your NOMAD
## Getting Started
**New to N.O.M.A.D.?** Use the Easy Setup wizard to configure your server and download content collections.
**New to NOMAD?** Use the Easy Setup wizard to configure your server and download content collections.
**[Run Easy Setup →](/easy-setup)**
@ -72,7 +72,7 @@ Or explore the **[Getting Started Guide](/docs/getting-started)** for a walkthro
## Keeping Your Server Updated
N.O.M.A.D. works best when kept up to date while you have internet access. This ensures you have the latest:
NOMAD works best when kept up to date while you have internet access. This ensures you have the latest:
- Software features and bug fixes
- Wikipedia and reference content
- Educational materials
@ -80,6 +80,6 @@ N.O.M.A.D. works best when kept up to date while you have internet access. This
When you go offline, you'll have everything you need — the last synced versions of all your content.
You can update on demand, or turn on **automatic updates** so N.O.M.A.D. keeps its software, apps, and content current on its own while you have internet. See the **[Updates guide](/docs/updates)** for how it works.
You can update on demand, or turn on **automatic updates** so NOMAD keeps its software, apps, and content current on its own while you have internet. See the **[Updates guide](/docs/updates)** for how it works.
**[Check for Updates →](/settings/update)**

View File

@ -7,7 +7,7 @@
- **Supply Depot — Curated App Onboarding & Fixes**: Each curated app now ships with NOMAD-specific getting-started docs (first run, default logins, where data lives, what does and doesn't work offline), deep-linked from a **Manage Docs** item on the card. Alongside it is a round of install fixes so the nine documented apps — Stirling PDF, File Browser, Calibre-Web, IT Tools, Excalidraw, Homebox, Vaultwarden, Jellyfin, Meshtastic Web — work out of the box: seeded logins instead of random passwords buried in logs, a bundled Calibre library, HTTPS-by-default where the app requires a secure context (Vaultwarden), corrected internal ports (Meshtastic Web), pre-created media folders (Jellyfin), and a swap to a maintained image (Homebox). You can now also **edit curated apps**, not just custom ones — edits are merged into the app's existing config (preserving advanced settings like GPU device requests) and flag the app so the seeder stops overwriting it, while untouched apps still receive catalog updates. Thanks @chriscrosstalk for the contribution!
- **Automatic Core Updates**: NOMAD's own admin/core image can now update itself hands-off, gated by layered safety checks. It's opt-in and off by default, runs only inside a user-configured time window, and applies only same-major, strictly-newer GA releases (major bumps stay manual) past a configurable cool-off — behind pre-flight checks for the update sidecar, no in-flight updates/downloads/installs, and sufficient host disk. It auto-disables after repeated genuine failures, while transient offline release lookups are treated as harmless skips. Settings → Updates exposes the toggle, window, cool-off, and live status. This is the first leg of the auto-update trilogy. Thanks @jakeaturner for the contribution!
- **Automatic App Updates**: Installed apps (the "Supply Depot" sibling containers) can now keep themselves up to date with opt-in, hands-off minor/patch updates, mirroring the core auto-update feature. Updates are gated behind a two-level opt-in — a global master switch in Settings → Updates **and** a per-app toggle in the Supply Depot — and respect the shared update window, cool-off period, disk/in-progress pre-flight checks, and per-app failure backoff. Major versions are never auto-applied. Thanks @jakeaturner for the contribution!
- **Automatic Content Updates**: Completing the auto-update trilogy, installed Kiwix ZIM files and PMTiles maps can now update themselves on an opt-in basis. Content updates run on their own dedicated overnight window and bandwidth cap (separate from the app/core schedule, since content downloads are multi-GB), check the upstream Kiwix and PMTiles catalogs directly (no more reliance on external Project N.O.M.A.D. API), and keep the AI Knowledge Base in sync when a ZIM is replaced. Thanks @jakeaturner for the contribution!
- **Automatic Content Updates**: Completing the auto-update trilogy, installed Kiwix ZIM files and PMTiles maps can now update themselves on an opt-in basis. Content updates run on their own dedicated overnight window and bandwidth cap (separate from the app/core schedule, since content downloads are multi-GB), check the upstream Kiwix and PMTiles catalogs directly (no more reliance on external Project NOMAD API), and keep the AI Knowledge Base in sync when a ZIM is replaced. Thanks @jakeaturner for the contribution!
- **Supply Depot — Custom Launch URLs**: You can now override an app's "Open" link with a reverse-proxy or local-DNS address (e.g. `https://jellyfin.myhomelab.net`). The override is stored separately so the default link is always recoverable, survives reseeds/upgrades, and is validated on both client and server. Thanks @jakeaturner for the contribution!
- **Supply Depot — Version & Update visibility**: App cards now show the installed version next to the app name (e.g. `Kiwix · 3.7.0`), and the "Update available" pill now stands out with a solid desert-orange fill so available updates actually draw the eye. Thanks @chriscrosstalk for the contribution!
- **Maps — Persistent View**: The Maps page now remembers your position and zoom across refreshes instead of resetting to the default US-wide view. The saved view is bounds-checked, so a corrupt value safely falls back to the default. Thanks @chriscrosstalk for the contribution!
@ -22,7 +22,7 @@
- **System**: Service install failures caused by a host port conflict (commonly a native Ollama install already on port 11434) now show a clear, actionable message with the exact commands to resolve it, instead of a raw Docker error. Thanks @chriscrosstalk for the fix!
- **System**: The per-service Update button is now disabled and shows "Updating..." while an update is in flight, preventing double-clicks that previously raced into Docker errors. The in-progress state is durable, so it survives a page reload during a multi-GB pull. Thanks @chriscrosstalk for the fix!
- **System Updates**: Update checks no longer crash for images with more than 1,000 tags (e.g Ollama). Registry pagination URLs are now resolved correctly, so the "Check for Updates" flow returns versions instead of failing silently — fixing Ollama appearing pinned at an old version. Thanks @chriscrosstalk for the fix!
- **System**: Internet status checks no longer report "No internet connection" on networks that block or hijack Cloudflare's 1.1.1.1. The check now probes additional hosts the app already contacts (GitHub and the Project N.O.M.A.D. API) in parallel and accepts any HTTP response as "online." Thanks @akashsalan for the fix!
- **System**: Internet status checks no longer report "No internet connection" on networks that block or hijack Cloudflare's 1.1.1.1. The check now probes additional hosts the app already contacts (GitHub and the Project NOMAD API) in parallel and accepts any HTTP response as "online." Thanks @akashsalan for the fix!
- **AI Assistant**: Oversized embedding chunks are now truncated and retried instead of being silently dropped, ending the retry storm that could peg the GPU and flood logs (the "api/embed for weeks" issue). The OpenAI-compatible fallback path now also passes context and truncation settings. Thanks @chriscrosstalk for the fix!
- **AI Assistant**: Chat suggestions now use your selected model (falling back to the *smallest* installed model) instead of the largest. This prevents a flagship model that exceeds available VRAM from hanging the chat page and returning a 500 error. Thanks @johno10661 for the fix!
- **AI Assistant**: The assistant no longer disclaims "Sorry, I couldn't find specific context regarding X..." when relevant material was actually retrieved. The RAG prompt now treats retrieved context as the authoritative source and falls back to general knowledge silently, the model-visible relevance scores that primed smaller models to distrust correct context were replaced with neutral source-title labels, and a conservative heading-match boost improves the ranking of already-retrieved chunks. Thanks @jakeaturner for the fix!
@ -335,7 +335,7 @@
- **Night Ops**: Added our most requested feature — a dark mode theme for the Command Center interface! Activate it from the footer and enjoy the sleek new look during your late-night missions. Thanks @chriscrosstalk for the contribution!
- **Debug Info**: Added a new "Debug Info" modal accessible from the footer that provides detailed system and application information for troubleshooting and support. Thanks @chriscrosstalk for the contribution!
- **Support the Project**: Added a new "Support the Project" page in settings with links to community resources, donation options, and ways to contribute.
- **Install**: The main Nomad image is now fully self-contained and directly usable with Docker Compose, allowing for more flexible and customizable installations without relying on external scripts. The image remains fully backwards compatible with existing installations, and the install script has been updated to reflect the simpler deployment process.
- **Install**: The main NOMAD image is now fully self-contained and directly usable with Docker Compose, allowing for more flexible and customizable installations without relying on external scripts. The image remains fully backwards compatible with existing installations, and the install script has been updated to reflect the simpler deployment process.
### Bug Fixes
- **Settings**: Storage usage display now prefers real block devices over tempfs. Thanks @Bortlesboat for the fix!
@ -353,7 +353,7 @@
- **Ollama**: The detected GPU type is now persisted in the database for more reliable configuration and troubleshooting across updates and restarts. Thanks @chriscrosstalk for the contribution!
- **Downloads**: Users can now dismiss failed download notifications to reduce clutter in the UI. Thanks @chriscrosstalk for the contribution!
- **Logging**: Changed the default log level to "info" to reduce noise and focus on important messages. Thanks @traxeon for the suggestion!
- **Logging**: Nomad's internal logger now creates it's own log directory on startup if it doesn't already exist to prevent errors on fresh installs where the logs directory hasn't been created yet.
- **Logging**: NOMAD's internal logger now creates it's own log directory on startup if it doesn't already exist to prevent errors on fresh installs where the logs directory hasn't been created yet.
- **Dozzle**: Dozzle shell access and container actions are now disabled by default. Thanks @traxeon for the recommendation!
- **MySQL & Redis**: Removed port exposure to host by default for improved security. Ports can still be exposed manually if needed. Thanks @traxeon for the recommendation!
- **Dependencies**: Various dependency updates to close security vulnerabilities and improve stability
@ -367,7 +367,7 @@
### Features
- **AI Assistant**: Added improved user guidance for troubleshooting GPU pass-through issues
- **AI Assistant**: The last used model is now automatically selected when a new chat is started
- **Settings**: Nomad now automatically performs nightly checks for available app updates, and users can select and apply updates from the Apps page in Settings
- **Settings**: NOMAD now automatically performs nightly checks for available app updates, and users can select and apply updates from the Apps page in Settings
### Bug Fixes
- **Settings**: Fixed an issue where the AI Assistant settings page would be shown in navigation even if the AI Assistant was not installed, thus causing 404 errors when clicked
@ -819,7 +819,7 @@
### 🚀 New Features
- Uninstall script now removes non-management Nomad app containers
- Uninstall script now removes non-management NOMAD app containers
### ✨ Improvements
@ -844,7 +844,7 @@
- Fixed renderer file permissions
- Fixed absolute host path issue
- **ZIM Manager**:
- Initial ZIM download now hosted in Project Nomad GitHub repo for better availability
- Initial ZIM download now hosted in Project NOMAD GitHub repo for better availability
---
@ -868,7 +868,7 @@
### ⚠️ Breaking Changes
- **Container Naming**: As a result of standardized container naming, it is recommend that you do a fresh install of Project N.O.M.A.D. and any apps to avoid potential conflicts/duplication of containers
- **Container Naming**: As a result of standardized container naming, it is recommend that you do a fresh install of Project NOMAD and any apps to avoid potential conflicts/duplication of containers
### 📚 Documentation

View File

@ -1,6 +1,6 @@
# Keeping N.O.M.A.D. Updated
# Keeping NOMAD Updated
N.O.M.A.D. works best when it's kept current while you have internet, so it's ready with the latest software and content the next time you go offline. This page explains what can be updated, how to do it on demand, and how to let N.O.M.A.D. handle it for you automatically.
NOMAD works best when it's kept current while you have internet, so it's ready with the latest software and content the next time you go offline. This page explains what can be updated, how to do it on demand, and how to let NOMAD handle it for you automatically.
---
@ -8,7 +8,7 @@ N.O.M.A.D. works best when it's kept current while you have internet, so it's re
There are three separate things that can be updated, and you control each one independently:
1. **Software (the core)** — N.O.M.A.D. itself: the Command Center, new features, bug fixes, and security improvements.
1. **Software (the core)** — NOMAD itself: the Command Center, new features, bug fixes, and security improvements.
2. **Apps** — the installable apps from the [Supply Depot](/supply-depot) (Kiwix, the AI Assistant, and any others you've added).
3. **Content** — your offline material: Wikipedia and other Kiwix libraries, and downloaded map regions.
@ -21,28 +21,28 @@ You can update any of these on demand, or set any of them to update automaticall
To check for and install updates yourself:
1. Go to **[Settings → Check for Updates](/settings/update)**.
2. If a software update is available, click to install it. N.O.M.A.D. downloads the update and restarts (usually 25 minutes).
2. If a software update is available, click to install it. NOMAD downloads the update and restarts (usually 25 minutes).
3. Apps can be updated from their card in the [Supply Depot](/supply-depot) using **Manage Update**.
4. Content is managed from **Settings → Content Manager** and **Content Explorer**, where you can download newer versions of installed libraries and maps.
If a software or app update ever fails, N.O.M.A.D. is designed to recover gracefully — the previous working version keeps running, so your server stays up.
If a software or app update ever fails, NOMAD is designed to recover gracefully — the previous working version keeps running, so your server stays up.
---
## Automatic updates
N.O.M.A.D. can keep itself current without you having to remember to check. **Automatic updates are opt-in and off by default** — nothing updates on its own until you turn it on. You manage all of it from **Settings → Updates**.
NOMAD can keep itself current without you having to remember to check. **Automatic updates are opt-in and off by default** — nothing updates on its own until you turn it on. You manage all of it from **Settings → Updates**.
A few things are true across all three:
- **You choose a time window.** Automatic updates only run during the hours you set, so they never interrupt you mid-use.
- **Major versions are never automatic.** Only minor and patch updates apply on their own; a big version jump always waits for you to do it manually, on purpose.
- **Safety checks come first.** Before applying anything, N.O.M.A.D. confirms there's enough disk space and that no other update, download, or install is already in progress.
- **Being offline is harmless.** If N.O.M.A.D. can't reach the internet to check, it simply skips that round and tries again later.
- **Safety checks come first.** Before applying anything, NOMAD confirms there's enough disk space and that no other update, download, or install is already in progress.
- **Being offline is harmless.** If NOMAD can't reach the internet to check, it simply skips that round and tries again later.
### Automatic software (core) updates
Turn this on from **Settings → Updates**. When enabled, N.O.M.A.D. updates its own core to newer releases within the same major version, during your chosen window, after a configurable **cool-off** period (so a brand-new release has time to prove itself before your server takes it). The same page shows the toggle, the window, the cool-off setting, and live status. If updates fail repeatedly for a real reason, N.O.M.A.D. turns the feature back off and lets you know rather than retrying forever.
Turn this on from **Settings → Updates**. When enabled, NOMAD updates its own core to newer releases within the same major version, during your chosen window, after a configurable **cool-off** period (so a brand-new release has time to prove itself before your server takes it). The same page shows the toggle, the window, the cool-off setting, and live status. If updates fail repeatedly for a real reason, NOMAD turns the feature back off and lets you know rather than retrying forever.
### Automatic app updates
@ -50,7 +50,7 @@ App auto-updates are opt-in at **two levels**: a master switch in **Settings →
### Automatic content updates
Installed Wikipedia/ZIM libraries and map regions can refresh themselves too. Because content downloads are large (often many gigabytes), content updates run on their **own dedicated overnight window** with a **bandwidth cap**, separate from the software and app schedule. N.O.M.A.D. checks the upstream Kiwix and map catalogs directly, and when a Wikipedia library is replaced with a newer version, it keeps the AI Knowledge Base in sync automatically.
Installed Wikipedia/ZIM libraries and map regions can refresh themselves too. Because content downloads are large (often many gigabytes), content updates run on their **own dedicated overnight window** with a **bandwidth cap**, separate from the software and app schedule. NOMAD checks the upstream Kiwix and map catalogs directly, and when a Wikipedia library is replaced with a newer version, it keeps the AI Knowledge Base in sync automatically.
---

View File

@ -1,12 +1,12 @@
# What Can You Do With N.O.M.A.D.?
# What Can You Do With NOMAD?
N.O.M.A.D. is designed to be your information lifeline when internet isn't available. Here's how different people use it.
NOMAD is designed to be your information lifeline when internet isn't available. Here's how different people use it.
---
## Emergency Preparedness
When disasters strike, internet and cell service often go down first. N.O.M.A.D. keeps critical information at your fingertips.
When disasters strike, internet and cell service often go down first. NOMAD keeps critical information at your fingertips.
**What you can do:**
- Look up first aid and emergency medical procedures
@ -184,13 +184,13 @@ Add your own documents to the [Knowledge Base](/knowledge-base) — emergency pl
Keep your server updated while you have internet. You never know when you'll need to go offline.
### Step 5: Practice
Try using N.O.M.A.D. before you need it. Familiarity with the tools makes them more useful in a crisis.
Try using NOMAD before you need it. Familiarity with the tools makes them more useful in a crisis.
---
## Need Something Specific?
N.O.M.A.D. content is customizable. If you don't see what you need:
NOMAD content is customizable. If you don't see what you need:
1. **Browse [Content Explorer](/settings/zim/remote-explorer)** — Thousands of ZIM files including Wikipedia packages
2. **Check [Content Manager](/settings/zim)** — Manage your installed content

View File

@ -14,7 +14,7 @@ import NotificationsProvider from '~/providers/NotificationProvider'
import { ThemeProvider } from '~/providers/ThemeProvider'
import { UsePageProps } from '../../types/system'
const appName = import.meta.env.VITE_APP_NAME || 'Project N.O.M.A.D.'
const appName = import.meta.env.VITE_APP_NAME || 'Project NOMAD'
const queryClient = new QueryClient()
// Patch the global crypto object for non-HTTPS/localhost contexts

View File

@ -2,7 +2,7 @@ import { useRef, useState, useCallback } from 'react'
import useDownloads, { useDownloadsProps } from '~/hooks/useDownloads'
import { extractFileName, formatBytes } from '~/lib/util'
import StyledSectionHeader from './StyledSectionHeader'
import { IconAlertTriangle, IconX, IconLoader2 } from '@tabler/icons-react'
import { IconAlertTriangle, IconX, IconLoader2, IconRefresh, IconExternalLink } from '@tabler/icons-react'
import api from '~/lib/api'
interface ActiveDownloadProps {
@ -39,6 +39,7 @@ const ActiveDownloads = ({ filetype, withHeader = false }: ActiveDownloadProps)
const { data: downloads, invalidate } = useDownloads({ filetype })
const [cancellingJobs, setCancellingJobs] = useState<Set<string>>(new Set())
const [confirmingCancel, setConfirmingCancel] = useState<string | null>(null)
const [retryingJobs, setRetryingJobs] = useState<Set<string>>(new Set())
// Track previous downloadedBytes for speed calculation
const prevBytesRef = useRef<Map<string, { bytes: number; time: number }>>(new Map())
@ -83,6 +84,20 @@ const ActiveDownloads = ({ filetype, withHeader = false }: ActiveDownloadProps)
invalidate()
}
const handleRetry = async (jobId: string) => {
setRetryingJobs((prev) => new Set(prev).add(jobId))
try {
await api.retryDownloadJob(jobId)
} finally {
setRetryingJobs((prev) => {
const next = new Set(prev)
next.delete(jobId)
return next
})
invalidate()
}
}
const handleCancel = async (jobId: string) => {
setCancellingJobs((prev) => new Set(prev).add(jobId))
setConfirmingCancel(null)
@ -113,6 +128,8 @@ const ActiveDownloads = ({ filetype, withHeader = false }: ActiveDownloadProps)
const isCancelling = cancellingJobs.has(download.jobId)
const isConfirming = confirmingCancel === download.jobId
const isRetrying = retryingJobs.has(download.jobId)
return (
<div
key={download.jobId}
@ -123,26 +140,55 @@ const ActiveDownloads = ({ filetype, withHeader = false }: ActiveDownloadProps)
}`}
>
{status === 'failed' ? (
<div className="flex items-center gap-2">
<IconAlertTriangle className="w-5 h-5 text-red-500 flex-shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-text-primary truncate">
{download.title || filename}
</p>
{download.title && (
<p className="text-xs text-text-muted truncate">{filename}</p>
)}
<p className="text-xs text-red-600 mt-0.5">
Download failed{download.failedReason ? `: ${download.failedReason}` : ''}
</p>
<div className="space-y-3">
<div className="flex items-start gap-2">
<IconAlertTriangle className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-text-primary truncate">
{download.title || filename}
</p>
{download.title && (
<p className="text-xs text-text-muted truncate">{filename}</p>
)}
<p className="text-xs text-red-600 mt-0.5">
Download failed{download.failedReason ? `: ${download.failedReason}` : ''}
</p>
</div>
<button
onClick={() => handleDismiss(download.jobId)}
className="flex-shrink-0 p-1 rounded hover:bg-red-100 transition-colors"
title="Dismiss failed download"
>
<IconX className="w-4 h-4 text-red-400 hover:text-red-600" />
</button>
</div>
<div className="flex items-center gap-2 pl-7">
<button
onClick={() => handleRetry(download.jobId)}
disabled={isRetrying}
className="flex items-center gap-1.5 text-xs px-3 py-1.5 rounded bg-desert-green text-white hover:bg-desert-green-dark transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Retry download"
>
{isRetrying ? (
<IconLoader2 className="w-3.5 h-3.5 animate-spin" />
) : (
<IconRefresh className="w-3.5 h-3.5" />
)}
{isRetrying ? 'Retrying...' : 'Retry'}
</button>
{download.url && download.url.startsWith('http') && (
<a
href={download.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-xs px-3 py-1.5 rounded bg-desert-stone-lighter text-text-secondary hover:bg-desert-stone-light transition-colors"
title="Open resource download page"
>
<IconExternalLink className="w-3.5 h-3.5" />
Download page
</a>
)}
</div>
<button
onClick={() => handleDismiss(download.jobId)}
className="flex-shrink-0 p-1 rounded hover:bg-red-100 transition-colors"
title="Dismiss failed download"
>
<IconX className="w-4 h-4 text-red-400 hover:text-red-600" />
</button>
</div>
) : (
<div className="space-y-2">

View File

@ -13,7 +13,7 @@ export default function Footer() {
<footer>
<div className="flex items-center justify-center gap-3 border-t border-border-subtle py-4">
<p className="text-sm/6 text-text-secondary">
Project N.O.M.A.D. Command Center v{appVersion}
Project NOMAD&trade; Command Center v{appVersion}
</p>
<span className="text-gray-300">|</span>
<button

View File

@ -63,7 +63,7 @@ const StyledSidebar: React.FC<StyledSidebarProps> = ({ title, items }) => {
return (
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-desert-sand px-6 ring-1 ring-white/5 pt-4 shadow-md">
<div className="flex h-16 shrink-0 items-center">
<img src="/project_nomad_logo.webp" alt="Project Nomad Logo" className="h-16 w-16" />
<img src="/project_nomad_logo.webp" alt="Project NOMAD Logo" className="h-16 w-16" />
<h1 className="ml-3 text-xl font-semibold text-text-primary">{title}</h1>
</div>
<nav className="flex flex-1 flex-col">
@ -87,7 +87,7 @@ const StyledSidebar: React.FC<StyledSidebarProps> = ({ title, items }) => {
</ul>
</nav>
<div className="mb-4 flex flex-col items-center gap-1 text-sm text-text-secondary text-center">
<p>Project N.O.M.A.D. Command Center v{appVersion}</p>
<p>Project NOMAD&trade; Command Center v{appVersion}</p>
<button
onClick={() => setDebugModalOpen(true)}
className="text-gray-500 hover:text-desert-green inline-flex items-center gap-1 cursor-pointer"

View File

@ -89,7 +89,7 @@ export default function ChatSidebar({
)}
</div>
<div className="p-4 flex flex-col items-center justify-center gap-y-2">
<img src="/project_nomad_logo.webp" alt="Project Nomad Logo" className="h-28 w-28 mb-6" />
<img src="/project_nomad_logo.webp" alt="Project NOMAD Logo" className="h-28 w-28 mb-6" />
<StyledButton
onClick={() => {
if (isInModal) {

View File

@ -311,6 +311,7 @@ export default function MapComponent({
[hideCoordinates, isHoveringUI, isDraggingMap, showCoordinatesEnabled]
)
const handleFlyTo = useCallback((longitude: number, latitude: number) => {
setTargetIndicator(null)
mapRef.current?.flyTo({ center: [longitude, latitude], zoom: 12, duration: 1500 })

View File

@ -25,7 +25,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
className="p-2 flex gap-2 flex-col items-center justify-center cursor-pointer"
onClick={() => router.visit('/home')}
>
<img src="/project_nomad_logo.webp" alt="Project Nomad Logo" className="h-40 w-40" />
<img src="/project_nomad_logo.webp" alt="Project NOMAD Logo" className="h-40 w-40" />
<h1 className="text-5xl font-bold text-desert-green">Command Center</h1>
</div>
<hr className={

View File

@ -843,6 +843,15 @@ class API {
})()
}
async retryDownloadJob(jobId: string): Promise<{ success: boolean; message: string } | undefined> {
return catchInternal(async () => {
const response = await this.client.post<{ success: boolean; message: string }>(
`/downloads/jobs/${jobId}/retry`
)
return response.data
})()
}
async runBenchmark(type: BenchmarkType, sync: boolean = false) {
return catchInternal(async () => {
const response = await this.client.post<RunBenchmarkResponse>(

View File

@ -35,7 +35,7 @@ const SYSTEM_ITEMS = [
to: '/easy-setup',
target: '',
description:
'Not sure where to start? Use the setup wizard to quickly configure your N.O.M.A.D.!',
'Not sure where to start? Use the setup wizard to quickly configure your NOMAD!',
icon: <IconBolt size={48} />,
installed: true,
displayOrder: 50,
@ -55,7 +55,7 @@ const SYSTEM_ITEMS = [
label: 'Docs',
to: '/docs/home',
target: '',
description: 'Read Project N.O.M.A.D. manuals and guides',
description: 'Read Project NOMAD manuals and guides',
icon: <IconHelp size={48} />,
installed: true,
displayOrder: 52,
@ -65,7 +65,7 @@ const SYSTEM_ITEMS = [
label: 'Settings',
to: '/settings/system',
target: '',
description: 'Configure your N.O.M.A.D. settings',
description: 'Configure your NOMAD settings',
icon: <IconSettings size={48} />,
installed: true,
displayOrder: 53,
@ -141,7 +141,7 @@ export default function Home(props: {
updateInfo?.updateAvailable && (
<div className='flex justify-center items-center p-4 w-full'>
<Alert
title="An update is available for Project N.O.M.A.D.!"
title="An update is available for Project NOMAD!"
type="info-inverted"
variant="solid"
className="w-full"

View File

@ -68,7 +68,7 @@ export default function AdvancedPage(props: {
return (
<SettingsLayout>
<Head title="Advanced Settings | Project N.O.M.A.D." />
<Head title="Advanced Settings | Project NOMAD" />
<div className="xl:pl-72 w-full">
<main className="px-12 py-6">
<h1 className="text-4xl font-semibold mb-4">Advanced</h1>
@ -80,7 +80,7 @@ export default function AdvancedPage(props: {
<StyledSectionHeader title="Connectivity" className="mt-8 mb-4" />
<div className="bg-surface-primary rounded-lg border-2 border-border-subtle p-6">
<p className="text-sm text-text-secondary mb-4">
N.O.M.A.D. periodically checks whether it can reach the internet. By default it probes
NOMAD periodically checks whether it can reach the internet. By default it probes
Cloudflare's utility endpoint with a few fallbacks. Set a custom endpoint below if your
network blocks the defaults. Leave blank to use the built-in defaults.
</p>

View File

@ -96,7 +96,7 @@ export default function SettingsPage(props: { system: { services: ServiceSlim[]
>
<p className="text-text-primary">
Are you sure you want to install {service.friendly_name || service.service_name}? This
will start the service and make it available in your Project N.O.M.A.D. instance. It may
will start the service and make it available in your Project NOMAD instance. It may
take some time to complete.
</p>
</StyledModal>,
@ -357,7 +357,7 @@ export default function SettingsPage(props: { system: { services: ServiceSlim[]
<div>
<h1 className="text-4xl font-semibold">Apps</h1>
<p className="text-text-muted mt-1">
Manage the applications that are available in your Project N.O.M.A.D. instance. Nightly update checks will automatically detect when new versions of these apps are available.
Manage the applications that are available in your Project NOMAD instance. Nightly update checks will automatically detect when new versions of these apps are available.
</p>
</div>
<StyledButton

View File

@ -4,7 +4,7 @@ import SettingsLayout from '~/layouts/SettingsLayout'
export default function LegalPage() {
return (
<SettingsLayout>
<Head title="Legal Notices | Project N.O.M.A.D." />
<Head title="Legal Notices | Project NOMAD" />
<div className="xl:pl-72 w-full">
<main className="px-12 py-6 max-w-4xl">
<h1 className="text-4xl font-semibold mb-8">Legal Notices</h1>
@ -34,7 +34,7 @@ export default function LegalPage() {
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">Third-Party Software Attribution</h2>
<p className="text-text-primary mb-4">
Project N.O.M.A.D. integrates the following open source projects. We are grateful to
Project NOMAD&trade; integrates the following open source projects. We are grateful to
their developers and communities:
</p>
<ul className="space-y-3 text-text-primary">
@ -75,12 +75,12 @@ export default function LegalPage() {
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">Privacy Statement</h2>
<p className="text-text-primary mb-3">
Project N.O.M.A.D. is designed with privacy as a core principle:
Project NOMAD is designed with privacy as a core principle:
</p>
<ul className="list-disc list-inside space-y-2 text-text-primary">
<li><strong>Zero Telemetry:</strong> N.O.M.A.D. does not collect, transmit, or store any usage data, analytics, or telemetry.</li>
<li><strong>Zero Telemetry:</strong> NOMAD does not collect, transmit, or store any usage data, analytics, or telemetry.</li>
<li><strong>Local-First:</strong> All your data, downloaded content, AI conversations, and notes remain on your device.</li>
<li><strong>No Accounts Required:</strong> N.O.M.A.D. operates without user accounts or authentication by default.</li>
<li><strong>No Accounts Required:</strong> NOMAD operates without user accounts or authentication by default.</li>
<li><strong>Network Optional:</strong> An internet connection is only required to download content or updates. All installed features work fully offline.</li>
</ul>
</section>
@ -89,7 +89,7 @@ export default function LegalPage() {
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">Content Disclaimer</h2>
<p className="text-text-primary mb-3">
Project N.O.M.A.D. provides tools to download and access content from third-party sources
Project NOMAD provides tools to download and access content from third-party sources
including Wikipedia, Wikibooks, medical references, educational platforms, and other
publicly available resources.
</p>
@ -108,7 +108,7 @@ export default function LegalPage() {
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">Medical and Emergency Information Disclaimer</h2>
<p className="text-text-primary mb-3">
Some content available through N.O.M.A.D. includes medical references, first aid guides,
Some content available through NOMAD includes medical references, first aid guides,
and emergency preparedness information. This content is provided for general
informational purposes only.
</p>
@ -127,7 +127,7 @@ export default function LegalPage() {
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">Data Storage</h2>
<p className="text-text-primary mb-3">
All data associated with Project N.O.M.A.D. is stored locally on your device:
All data associated with Project NOMAD is stored locally on your device:
</p>
<ul className="list-disc list-inside space-y-2 text-text-primary">
<li><strong>Installation Directory:</strong> /opt/project-nomad</li>
@ -135,7 +135,7 @@ export default function LegalPage() {
<li><strong>Application Data:</strong> Stored in Docker volumes on your local system</li>
</ul>
<p className="text-text-primary mt-3">
You maintain full control over your data. Uninstalling N.O.M.A.D. or deleting these
You maintain full control over your data. Uninstalling NOMAD or deleting these
directories will permanently remove all associated data.
</p>
</section>

View File

@ -261,7 +261,7 @@ export default function ModelsPage(props: {
return (
<SettingsLayout>
<Head title={`${aiAssistantName} Settings | Project N.O.M.A.D.`} />
<Head title={`${aiAssistantName} Settings | Project NOMAD`} />
<div className="xl:pl-72 w-full">
<main className="px-12 py-6">
<h1 className="text-4xl font-semibold mb-4">{aiAssistantName}</h1>

View File

@ -5,7 +5,7 @@ import SettingsLayout from '~/layouts/SettingsLayout'
export default function SupportPage() {
return (
<SettingsLayout>
<Head title="Support the Project | Project N.O.M.A.D." />
<Head title="Support the Project | Project NOMAD" />
<div className="xl:pl-72 w-full">
<main className="px-12 py-6 max-w-4xl">
<h1 className="text-4xl font-semibold mb-4">Support the Project</h1>

View File

@ -239,7 +239,7 @@ export default function SystemUpdatePage(props: { system: Props }) {
<div className="mb-8">
<h1 className="text-4xl font-bold text-desert-green mb-2">System Update</h1>
<p className="text-desert-stone-dark">
Keep your Project N.O.M.A.D. instance up to date with the latest features and
Keep your Project NOMAD instance up to date with the latest features and
improvements.
</p>
</div>
@ -287,7 +287,7 @@ export default function SystemUpdatePage(props: { system: Props }) {
</h2>
<p className="text-desert-stone-dark mb-6">
{versionInfo.updateAvailable
? `A new version (${versionInfo.latestVersion}) is available for your Project N.O.M.A.D. instance.`
? `A new version (${versionInfo.latestVersion}) is available for your Project NOMAD instance.`
: 'Your system is running the latest version!'}
</p>
</>
@ -460,7 +460,7 @@ export default function SystemUpdatePage(props: { system: Props }) {
<div className="flex flex-col md:flex-row justify-between items-center p-8 gap-y-8 md:gap-y-0 gap-x-8">
<div>
<h2 className="max-w-xl text-lg font-bold text-desert-green sm:text-xl lg:col-span-7">
Want to stay updated with the latest from Project N.O.M.A.D.? Subscribe to receive
Want to stay updated with the latest from Project NOMAD? Subscribe to receive
release notes directly to your inbox. Unsubscribe anytime.
</h2>
</div>
@ -487,7 +487,7 @@ export default function SystemUpdatePage(props: { system: Props }) {
</StyledButton>
</div>
<p className="mt-2 text-sm text-desert-stone-dark">
We care about your privacy. Project N.O.M.A.D. will never share your email with
We care about your privacy. Project NOMAD will never share your email with
third parties or send you spam.
</p>
</div>

View File

@ -128,7 +128,7 @@ export default function ZimPage() {
return (
<SettingsLayout>
<Head title="Content Manager | Project N.O.M.A.D." />
<Head title="Content Manager | Project NOMAD" />
<div className="xl:pl-72 w-full">
<main className="px-12 py-6">
<div className="flex items-center justify-between gap-4">

View File

@ -7,7 +7,6 @@ import {
} from '@tanstack/react-query'
import api from '~/lib/api'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useVirtualizer } from '@tanstack/react-virtual'
import StyledTable from '~/components/StyledTable'
import SettingsLayout from '~/layouts/SettingsLayout'
import { Head } from '@inertiajs/react'
@ -191,13 +190,6 @@ export default function ZimRemoteExplorer() {
[fetchNextPage, isFetching, hasMore]
)
const virtualizer = useVirtualizer({
count: flatData.length,
estimateSize: () => 48, // Estimate row height
getScrollElement: () => tableParentRef.current,
overscan: 5, // Number of items to render outside the visible area
})
//a check on mount and after a fetch to see if the table is already scrolled to the bottom and immediately needs to fetch more data
useEffect(() => {
fetchOnBottomReached(tableParentRef.current)
@ -447,7 +439,7 @@ export default function ZimRemoteExplorer() {
return (
<SettingsLayout>
<Head title="Content Explorer | Project N.O.M.A.D." />
<Head title="Content Explorer | Project NOMAD" />
<div className="xl:pl-72 w-full">
<main className="px-12 py-6">
<div className="flex justify-between items-center">
@ -591,14 +583,7 @@ export default function ZimRemoteExplorer() {
/>
</div>
<StyledTable<RemoteZimFileEntry & { actions?: any }>
data={flatData.map((i, idx) => {
const row = virtualizer.getVirtualItems().find((v) => v.index === idx)
return {
...i,
height: `${row?.size || 48}px`,
translateY: row?.start || 0,
}
})}
data={flatData}
ref={tableParentRef}
loading={isLoading}
columns={[
@ -610,6 +595,13 @@ export default function ZimRemoteExplorer() {
},
{
accessor: 'summary',
render(record) {
return (
<span className="block max-w-md truncate text-text-muted" title={record.summary}>
{record.summary}
</span>
)
},
},
{
accessor: 'updated',
@ -644,11 +636,97 @@ export default function ZimRemoteExplorer() {
},
},
]}
className="relative overflow-x-auto overflow-y-auto h-[600px] w-full mt-4"
tableBodyStyle={{
position: 'relative',
height: `${virtualizer.getTotalSize()}px`,
expandable={{
expandedRowRender(record) {
const issuedDate = record.issued ? new Date(record.issued) : null
const hasValidIssuedDate = issuedDate && !Number.isNaN(issuedDate.getTime())
return (
<div className="py-4 px-6">
<p className="text-sm text-text-primary mb-4">{record.summary}</p>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 text-sm">
{record.author && (
<div>
<span className="text-text-muted">Author: </span>
<span className="text-text-primary">{record.author}</span>
</div>
)}
{record.publisher && (
<div>
<span className="text-text-muted">Publisher: </span>
<span className="text-text-primary">{record.publisher}</span>
</div>
)}
{record.language && (
<div>
<span className="text-text-muted">Language: </span>
<span className="text-text-primary">{record.language}</span>
</div>
)}
{record.category && (
<div>
<span className="text-text-muted">Category: </span>
<span className="text-text-primary">{record.category}</span>
</div>
)}
{record.article_count != null && record.article_count > 0 && (
<div>
<span className="text-text-muted">Articles: </span>
<span className="text-text-primary">
{record.article_count.toLocaleString()}
</span>
</div>
)}
{record.media_count != null && record.media_count > 0 && (
<div>
<span className="text-text-muted">Media: </span>
<span className="text-text-primary">
{record.media_count.toLocaleString()}
</span>
</div>
)}
{hasValidIssuedDate && (
<div>
<span className="text-text-muted">Issued: </span>
<span className="text-text-primary">
{new Intl.DateTimeFormat('en-US', {
dateStyle: 'medium',
}).format(issuedDate!)}
</span>
</div>
)}
{record.size_bytes > 0 && (
<div>
<span className="text-text-muted">Size: </span>
<span className="text-text-primary">{formatBytes(record.size_bytes)}</span>
</div>
)}
</div>
{record.tags && (
<div className="mt-4 flex flex-wrap gap-2">
{record.tags
.split(';')
.filter(Boolean)
.map((tag, i) => (
<span
key={i}
className="inline-flex items-center rounded-full bg-surface-elevated px-2.5 py-0.5 text-xs font-medium text-text-muted"
>
{tag.trim()}
</span>
))}
</div>
)}
{record.file_name && (
<div className="mt-4">
<span className="text-text-muted text-sm">File: </span>
<code className="text-xs text-text-muted">{record.file_name}</code>
</div>
)}
</div>
)
},
}}
className="overflow-y-auto h-[600px] w-full mt-4"
containerProps={{
onScroll: (e) => fetchOnBottomReached(e.currentTarget as HTMLDivElement),
}}
@ -760,7 +838,6 @@ export default function ZimRemoteExplorer() {
)}
</div>
)}
<ActiveDownloads filetype="zim" withHeader />
{/* Manage Custom Libraries Modal */}

View File

@ -9,7 +9,7 @@
<link rel="icon" type="image/png" sizes="192x192" href="/favicon-192x192.png">
<link rel="apple-touch-icon" sizes="180x180" href="/favicon-180x180.png">
<title inertia>Project N.O.M.A.D</title>
<title inertia>Project NOMAD</title>
<script>
(function() {

View File

@ -107,6 +107,7 @@ router
router.get('/jobs/:filetype', [DownloadsController, 'filetype'])
router.delete('/jobs/:jobId', [DownloadsController, 'removeJob'])
router.post('/jobs/:jobId/cancel', [DownloadsController, 'cancelJob'])
router.post('/jobs/:jobId/retry', [DownloadsController, 'retryJob'])
})
.prefix('/api/downloads')

View File

@ -0,0 +1,99 @@
import * as assert from 'node:assert/strict'
import { test } from 'node:test'
import {
extractStructuredContent,
isNonContentHeading,
tableToText,
} from '../../app/utils/zim_html.js'
import * as cheerio from 'cheerio'
test('isNonContentHeading flags boilerplate headings, case-insensitive', () => {
assert.equal(isNonContentHeading('References'), true)
assert.equal(isNonContentHeading('see also'), true)
assert.equal(isNonContentHeading('EXTERNAL LINKS'), true)
assert.equal(isNonContentHeading('Further reading'), true)
})
test('isNonContentHeading keeps real content headings', () => {
assert.equal(isNonContentHeading('Uses'), false)
assert.equal(isNonContentHeading('Side effects'), false)
assert.equal(isNonContentHeading('History'), false)
})
test('extractStructuredContent drops non-content sections at emit time (#902)', () => {
const html = `<body>
<h1>Aspirin</h1>
<p>Aspirin is a medication used to reduce pain and fever.</p>
<h2>Uses</h2>
<p>It is used to treat fever, pain, and inflammation.</p>
<h2>References</h2>
<ul><li>Smith 2020</li><li>Jones 2019</li></ul>
<h2>See also</h2>
<p>Ibuprofen, Paracetamol</p>
<h2>External links</h2>
<p>https://example.com/aspirin</p>
</body>`
const { sections } = extractStructuredContent(html)
const headings = sections.map((s) => s.heading)
assert.deepEqual(headings, ['Introduction', 'Uses'])
assert.equal(
sections.some((s) => /Smith 2020|Ibuprofen|example\.com/.test(s.text)),
false,
'boilerplate-section content must not reach any emitted chunk'
)
})
test('extractStructuredContent keeps real sections including their content', () => {
const html = `<body>
<h1>Aspirin</h1>
<p>Intro text here.</p>
<h2>Side effects</h2>
<p>May cause stomach upset.</p>
</body>`
const { sections } = extractStructuredContent(html)
const sideEffects = sections.find((s) => s.heading === 'Side effects')
assert.ok(sideEffects, 'real content section should be emitted')
assert.match(sideEffects.text, /stomach upset/)
})
test('tableToText renders rows as delimited text, not concatenated cell soup (#902)', () => {
const html = `<table>
<tr><th>Age</th><th>Dose</th></tr>
<tr><td>Adult</td><td>500 mg</td></tr>
<tr><td>Child</td><td>250 mg</td></tr>
</table>`
const $ = cheerio.load(html)
const out = tableToText($, $('table').get(0))
assert.match(out, /Age \| Dose/)
assert.match(out, /Adult \| 500 mg/)
assert.match(out, /Child \| 250 mg/)
assert.ok(out.includes('\n'), 'rows should be newline-separated')
assert.equal(
/AgeDose|Adult500/.test(out),
false,
'cells must not be concatenated without separators'
)
})
test('extractStructuredContent keeps table structure inside a content section', () => {
const html = `<body>
<h2>Dosage</h2>
<table>
<tr><th>Age</th><th>Dose</th></tr>
<tr><td>Adult</td><td>500 mg</td></tr>
</table>
</body>`
const { sections } = extractStructuredContent(html)
const dosage = sections.find((s) => s.heading === 'Dosage')
assert.ok(dosage, 'section with a table should be emitted')
assert.match(dosage.text, /Age \| Dose/)
assert.match(dosage.text, /Adult \| 500 mg/)
})

View File

@ -42,6 +42,7 @@ export const KV_STORE_SCHEMA = {
'ai.amdHsaOverride': 'string',
'ai.autoFixGpuPassthrough': 'boolean',
'gpu.autoRemediatedAt': 'string',
'apps.homebox.apiKeyPepper': 'string',
} as const
type KVTagToType<T extends string> = T extends 'boolean' ? boolean : string

View File

@ -64,6 +64,13 @@ export type RemoteZimFileEntry = {
download_url: string
author: string
file_name: string
language?: string
category?: string
tags?: string
article_count?: number
media_count?: number
publisher?: string
issued?: string
}
export type ExtractZIMContentOptions = {
@ -71,8 +78,8 @@ export type ExtractZIMContentOptions = {
maxArticles?: number
onProgress?: (processedArticles: number, totalArticles: number) => void
// Batch processing options to avoid lock timeouts
startOffset?: number // Article index to start from for resuming
batchSize?: number // Max articles to process in this batch
startOffset?: number // Article index to start from for resuming
batchSize?: number // Max articles to process in this batch
}
export type ExtractZIMChunkingStrategy = 'structured' | 'simple'
@ -108,4 +115,4 @@ export type ZIMContentChunk = {
// Extraction metadata
strategy: ExtractZIMChunkingStrategy
}
}

View File

@ -5,7 +5,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title inertia>Project N.O.M.A.D</title>
<title inertia>Project NOMAD</title>
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=instrument-sans:400,400i,500,500i,600,600i,700,700i" rel="stylesheet" />

View File

@ -1,10 +1,10 @@
#!/bin/bash
# Project N.O.M.A.D. Installation Script
# Project NOMAD Installation Script
###################################################################################################################################################################################################
# Script | Project N.O.M.A.D. Installation Script
# Script | Project NOMAD Installation Script
# Version | 1.0.0
# Author | Crosstalk Solutions, LLC
# Website | https://crosstalksolutions.com
@ -28,7 +28,7 @@ GREEN='\033[1;32m' # Light Green.
# #
###################################################################################################################################################################################################
WHIPTAIL_TITLE="Project N.O.M.A.D Installation"
WHIPTAIL_TITLE="Project NOMAD Installation"
NOMAD_DIR="/opt/project-nomad"
MANAGEMENT_COMPOSE_FILE_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/management_compose.yaml"
START_SCRIPT_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/start_nomad.sh"
@ -353,8 +353,8 @@ setup_nvidia_container_toolkit() {
}
get_install_confirmation(){
echo -e "${YELLOW}#${RESET} This script will install Project N.O.M.A.D. and its dependencies on your machine."
echo -e "${YELLOW}#${RESET} If you already have Project N.O.M.A.D. installed with customized config or data, please be aware that running this installation script may overwrite existing files and configurations. It is highly recommended to back up any important data/configs before proceeding."
echo -e "${YELLOW}#${RESET} This script will install Project NOMAD and its dependencies on your machine."
echo -e "${YELLOW}#${RESET} If you already have Project NOMAD installed with customized config or data, please be aware that running this installation script may overwrite existing files and configurations. It is highly recommended to back up any important data/configs before proceeding."
read -p "Are you sure you want to continue? (y/N): " choice
case "$choice" in
y|Y )
@ -372,9 +372,9 @@ accept_terms() {
echo "License Agreement & Terms of Use"
echo "__________________________"
printf "\n\n"
echo "Project N.O.M.A.D. is licensed under the Apache License 2.0. The full license can be found at https://www.apache.org/licenses/LICENSE-2.0 or in the LICENSE file of this repository."
echo "Project NOMAD is licensed under the Apache License 2.0. The full license can be found at https://www.apache.org/licenses/LICENSE-2.0 or in the LICENSE file of this repository."
printf "\n"
echo "By accepting this agreement, you acknowledge that you have read and understood the terms and conditions of the Apache License 2.0 and agree to be bound by them while using Project N.O.M.A.D."
echo "By accepting this agreement, you acknowledge that you have read and understood the terms and conditions of the Apache License 2.0 and agree to be bound by them while using Project NOMAD"
echo -e "\n\n"
read -p "I have read and accept License Agreement & Terms of Use (y/N)? " choice
case "$choice" in
@ -391,7 +391,7 @@ accept_terms() {
create_nomad_directory(){
# Ensure the main installation directory exists
if [[ ! -d "$NOMAD_DIR" ]]; then
echo -e "${YELLOW}#${RESET} Creating directory for Project N.O.M.A.D at $NOMAD_DIR...\\n"
echo -e "${YELLOW}#${RESET} Creating directory for Project NOMAD at $NOMAD_DIR...\\n"
sudo mkdir -p "$NOMAD_DIR"
sudo chown "$(whoami):$(whoami)" "$NOMAD_DIR"
@ -596,11 +596,11 @@ verify_gpu_setup() {
}
success_message() {
echo -e "${GREEN}#${RESET} Project N.O.M.A.D installation completed successfully!\\n"
echo -e "${GREEN}#${RESET} Project NOMAD installation completed successfully!\\n"
echo -e "${GREEN}#${RESET} Installation files are located at /opt/project-nomad\\n\n"
echo -e "${GREEN}#${RESET} Project N.O.M.A.D's Command Center should automatically start whenever your device reboots. However, if you need to start it manually, you can always do so by running: ${WHITE_R}${NOMAD_DIR}/start_nomad.sh${RESET}\\n"
echo -e "${GREEN}#${RESET} Project NOMAD's Command Center should automatically start whenever your device reboots. However, if you need to start it manually, you can always do so by running: ${WHITE_R}${NOMAD_DIR}/start_nomad.sh${RESET}\\n"
echo -e "${GREEN}#${RESET} You can now access the management interface at http://localhost:8080 or http://${local_ip_address}:8080\\n"
echo -e "${GREEN}#${RESET} Thank you for supporting Project N.O.M.A.D!\\n"
echo -e "${GREEN}#${RESET} Thank you for supporting Project NOMAD!\\n"
}
###################################################################################################################################################################################################

View File

@ -1,6 +1,6 @@
# Project N.O.M.A.D. management services Docker Compose configuration
# Project NOMAD management services Docker Compose configuration
#
# This compose file defines the admin server, database, and other supporting services required to run Project N.O.M.A.D.
# This compose file defines the admin server, database, and other supporting services required to run Project NOMAD
# You can use this with `docker-compose up -d` to start all the necessary services with a single command after installation.
#
# Note: we recommend leaving all of the environment variables as-is except for any "replaceme" values,

View File

@ -1,6 +1,6 @@
# Project N.O.M.A.D. — About the Disk Collector Migration Script
# Project NOMAD — About the Disk Collector Migration Script
This script migrates your Project N.O.M.A.D. installation from the old host-based disk info collector to the new disk-collector sidecar. It modifies `/opt/project-nomad/compose.yml` to add the new service and remove the old bind mount, then restarts the full compose stack to apply the changes.
This script migrates your Project NOMAD installation from the old host-based disk info collector to the new disk-collector sidecar. It modifies `/opt/project-nomad/compose.yml` to add the new service and remove the old bind mount, then restarts the full compose stack to apply the changes.
### Why the Migration?
The new disk-collector sidecar provides a more robust and scalable way to collect disk information from the host. It removes the original bind mount to `/tmp/nomad-disk-info.json`, which was fragile and prone to issues on host reboots.
@ -12,14 +12,14 @@ The original host-based collector relied on a process running on the host that w
The migration script automates the necessary changes to your compose configuration and ensures a smooth transition to the new architecture.
### Why does Nomad need the nomad-disk-info.json file?
Nomad uses the disk info stored and updated in `nomad-disk-info.json` to allow users to view disk usage and availability within the Nomad "Command Center". While not critical to the core functionality of Nomad, it provides a more pleasant experience for users with limited storage space and/or who aren't familiar with command-line tools and Linux management.
### Why does NOMAD need the nomad-disk-info.json file?
NOMAD uses the disk info stored and updated in `nomad-disk-info.json` to allow users to view disk usage and availability within the NOMAD "Command Center". While not critical to the core functionality of NOMAD, it provides a more pleasant experience for users with limited storage space and/or who aren't familiar with command-line tools and Linux management.
### Why a separate container?
The disk-collector runs in a separate container to isolate its functionality from the main admin container. This separation provides several benefits:
- **Stability**: If the disk-collector encounters an issue or crashes, it won't affect the main admin container and vice versa.
- **Security**: The main admin container already has significant host access via the Docker socket, storage directory, and host.docker.internal. Additionally, Nomad may add more features in the future that support multi-user environments and/or more network exposure, so isolating the disk-collector reduces the exposure of the host filesystem (even if read-only) to just the one container, which has a very limited scope of functionality and access.
- **Modularity**: Because having the host disk info is not a critical component of Nomad's core functionality, isolating it in a sidecar allows users who don't need/want the disk info features to simply not run that container, without impacting the main admin container or other services. It also allows for more flexible future development of the disk-collector without needing to modify the main admin container.
- **Security**: The main admin container already has significant host access via the Docker socket, storage directory, and host.docker.internal. Additionally, NOMAD may add more features in the future that support multi-user environments and/or more network exposure, so isolating the disk-collector reduces the exposure of the host filesystem (even if read-only) to just the one container, which has a very limited scope of functionality and access.
- **Modularity**: Because having the host disk info is not a critical component of NOMAD's core functionality, isolating it in a sidecar allows users who don't need/want the disk info features to simply not run that container, without impacting the main admin container or other services. It also allows for more flexible future development of the disk-collector without needing to modify the main admin container.
### What if I don't want to run the migration script?
No worries - you can replicate the changes manually by editing your `/opt/project-nomad/compose.yml` to add the new disk-collector service and remove the old bind mount from the admin service, then restarting your compose stack. The migration script just automates these steps and ensures they're done correctly, but the underlying changes are straightforward if you prefer to do it yourself. Just be sure to back up your `compose.yml` before making any changes.

View File

@ -1,8 +1,8 @@
#!/bin/bash
# Project N.O.M.A.D. — Disk Collector Migration Script
# Project NOMAD — Disk Collector Migration Script
#
# Script | Project N.O.M.A.D. Disk Collector Migration Script
# Script | Project NOMAD Disk Collector Migration Script
# Version | 1.0.0
# Author | Crosstalk Solutions, LLC
# Website | https://crosstalksolutions.com
@ -63,13 +63,13 @@ check_has_sudo() {
}
check_confirmation() {
echo -e "${YELLOW}#${RESET} This script migrates your Project N.O.M.A.D. installation from the"
echo -e "${YELLOW}#${RESET} This script migrates your Project NOMAD installation from the"
echo -e "${YELLOW}#${RESET} host-based disk info collector to the new disk-collector sidecar."
echo -e "${YELLOW}#${RESET} It will modify compose.yml and restart the full compose stack"
echo -e "${YELLOW}#${RESET} to drop the old /tmp bind mount and start the disk-collector sidecar."
echo -e "${YELLOW}#${RESET} Please ensure you have a backup of your data before proceeding.\n"
echo -e "${RED}#${RESET} STOP: If you have customized your compose.yml or Nomad's storage setup (not common), please make these changes manually instead of using this script!\n"
echo -e "${RED}#${RESET} STOP: If you have customized your compose.yml or NOMAD's storage setup (not common), please make these changes manually instead of using this script!\n"
read -rp "Do you want to continue? (y/N) " response
if [[ ! "$response" =~ ^[Yy]$ ]]; then
echo -e "${RED}#${RESET} Aborting. No changes have been made."
@ -93,7 +93,7 @@ check_docker_running() {
check_compose_file() {
if [[ ! -f "$COMPOSE_FILE" ]]; then
echo -e "${RED}#${RESET} compose.yml not found at ${COMPOSE_FILE}."
echo -e "${RED}#${RESET} Project N.O.M.A.D. does not appear to be installed or compose.yml is missing."
echo -e "${RED}#${RESET} Project NOMAD does not appear to be installed or compose.yml is missing."
exit 1
fi
echo -e "${GREEN}#${RESET} Found compose.yml at ${COMPOSE_FILE}.\n"
@ -213,7 +213,7 @@ verify_disk_collector_running() {
# Main
echo -e "${GREEN}#########################################################################${RESET}"
echo -e "${GREEN}#${RESET} Project N.O.M.A.D. — Disk Collector Migration Script ${GREEN}#${RESET}"
echo -e "${GREEN}#${RESET} Project NOMAD — Disk Collector Migration Script ${GREEN}#${RESET}"
echo -e "${GREEN}#########################################################################${RESET}\n"
check_is_bash

View File

@ -1,8 +1,8 @@
#!/bin/bash
# Project N.O.M.A.D. - One-Time Updater Fix Script
# Project NOMAD - One-Time Updater Fix Script
#
# Script | Project N.O.M.A.D. One-Time Updater Fix Script
# Script | Project NOMAD One-Time Updater Fix Script
# Version | 1.0.0
# Author | Crosstalk Solutions, LLC
# Website | https://crosstalksolutions.com
@ -62,7 +62,7 @@ check_is_bash() {
}
check_confirmation() {
echo -e "${YELLOW}#${RESET} This is a very specific fix script for a very specific issue. You probably don't need to run this unless you were specifically directed to by the N.O.M.A.D. team."
echo -e "${YELLOW}#${RESET} This is a very specific fix script for a very specific issue. You probably don't need to run this unless you were specifically directed to by the NOMAD team."
echo -e "${YELLOW}#${RESET} Please ensure you have a backup of your data before proceeding."
read -rp "Do you want to continue? (y/N) " response
if [[ ! "$response" =~ ^[Yy]$ ]]; then
@ -97,7 +97,7 @@ check_docker_running() {
check_compose_file() {
if [[ ! -f "$COMPOSE_FILE" ]]; then
echo -e "${RED}#${RESET} compose.yml not found at ${COMPOSE_FILE}."
echo -e "${RED}#${RESET} Please ensure Project N.O.M.A.D. is installed before running this script."
echo -e "${RED}#${RESET} Please ensure Project NOMAD is installed before running this script."
exit 1
fi
echo -e "${GREEN}#${RESET} Found compose.yml at ${COMPOSE_FILE}.\n"
@ -106,7 +106,7 @@ check_compose_file() {
check_sidecar_dir() {
if [[ ! -d "$SIDECAR_DIR" ]]; then
echo -e "${RED}#${RESET} Sidecar directory not found at ${SIDECAR_DIR}."
echo -e "${RED}#${RESET} Please ensure Project N.O.M.A.D. is installed before running this script."
echo -e "${RED}#${RESET} Please ensure Project NOMAD is installed before running this script."
exit 1
fi
echo -e "${GREEN}#${RESET} Found sidecar directory at ${SIDECAR_DIR}.\n"
@ -211,7 +211,7 @@ verify_sidecar_running() {
###############################################################################
echo -e "${GREEN}#########################################################################${RESET}"
echo -e "${GREEN}#${RESET} Project N.O.M.A.D. — One-Time Updater Fix Script ${GREEN}#${RESET}"
echo -e "${GREEN}#${RESET} Project NOMAD — One-Time Updater Fix Script ${GREEN}#${RESET}"
echo -e "${GREEN}#########################################################################${RESET}\n"
check_is_bash
@ -236,5 +236,5 @@ echo -e "${GREEN}#${RESET} All fixes applied successfully!"
echo -e "${GREEN}#${RESET}"
echo -e "${GREEN}#${RESET} The updater sidecar can now install RC and stable versions correctly."
echo -e "${GREEN}#${RESET} The remaining fix (admin service target_tag support) will apply"
echo -e "${GREEN}#${RESET} automatically the next time you update N.O.M.A.D. via the UI."
echo -e "${GREEN}#${RESET} automatically the next time you update NOMAD via the UI."
echo -e "${GREEN}#########################################################################${RESET}\n"

View File

@ -1,6 +1,6 @@
#!/bin/bash
# Project N.O.M.A.D. - Disk Info Collector Sidecar
# Project NOMAD - Disk Info Collector Sidecar
#
# Reads host block device and filesystem info via the /:/host:ro,rslave bind-mount.
# No special capabilities required. Writes JSON to /storage/nomad-disk-info.json, which is read by the admin container.

View File

@ -1,6 +1,6 @@
#!/bin/bash
# Project N.O.M.A.D. Update Sidecar - Polls for update requests and executes them
# Project NOMAD Update Sidecar - Polls for update requests and executes them
SHARED_DIR="/shared"
REQUEST_FILE="${SHARED_DIR}/update-request"

View File

@ -1,12 +1,12 @@
#!/bin/bash
echo "Finding Project N.O.M.A.D containers..."
echo "Finding Project NOMAD containers..."
# -a to include all containers (running and stopped)
containers=$(docker ps -a --filter "name=^nomad_" --format "{{.Names}}")
if [ -z "$containers" ]; then
echo "No containers found for Project N.O.M.A.D. Is it installed?"
echo "No containers found for Project NOMAD Is it installed?"
exit 0
fi
@ -24,4 +24,4 @@ for container in $containers; do
echo ""
done
echo "Finished initiating start of all Project N.O.M.A.D containers."
echo "Finished initiating start of all Project NOMAD containers."

View File

@ -1,11 +1,11 @@
#!/bin/bash
echo "Finding running Docker containers for Project N.O.M.A.D..."
echo "Finding running Docker containers for Project NOMAD.."
containers=$(docker ps --filter "name=^nomad_" --format "{{.Names}}")
if [ -z "$containers" ]; then
echo "No running containers found for Project N.O.M.A.D."
echo "No running containers found for Project NOMAD"
exit 0
fi
@ -23,4 +23,4 @@ for container in $containers; do
echo ""
done
echo "Finished initiating graceful shutdown of all Project N.O.M.A.D containers."
echo "Finished initiating graceful shutdown of all Project NOMAD containers."

View File

@ -1,10 +1,10 @@
#!/bin/bash
# Project N.O.M.A.D. Uninstall Script
# Project NOMAD Uninstall Script
###################################################################################################################################################################################################
# Script | Project N.O.M.A.D. Uninstall Script
# Script | Project NOMAD Uninstall Script
# Version | 1.0.0
# Author | Crosstalk Solutions, LLC
# Website | https://crosstalksolutions.com
@ -45,13 +45,13 @@ check_current_directory(){
ensure_management_compose_file_exists(){
if [ ! -f "${MANAGEMENT_COMPOSE_FILE}" ]; then
echo "Unable to find the management Docker Compose file at ${MANAGEMENT_COMPOSE_FILE}. There may be a problem with your Project N.O.M.A.D. installation."
echo "Unable to find the management Docker Compose file at ${MANAGEMENT_COMPOSE_FILE}. There may be a problem with your Project NOMAD installation."
exit 1
fi
}
get_uninstall_confirmation(){
read -p "This script will remove ALL Project N.O.M.A.D. files and containers. THIS CANNOT BE UNDONE. Are you sure you want to continue? (y/n): " choice
read -p "This script will remove ALL Project NOMAD files and containers. THIS CANNOT BE UNDONE. Are you sure you want to continue? (y/n): " choice
case "$choice" in
y|Y )
echo -e "User chose to continue with the uninstallation."
@ -86,12 +86,12 @@ check_docker_compose() {
}
storage_cleanup() {
read -p "Do you want to delete the Project N.O.M.A.D. storage directory (${NOMAD_DIR})? This is best if you want to start a completely fresh install. This will PERMANENTLY DELETE all stored Nomad data and can't be undone! (y/N): " delete_dir_choice
read -p "Do you want to delete the Project NOMAD storage directory (${NOMAD_DIR})? This is best if you want to start a completely fresh install. This will PERMANENTLY DELETE all stored NOMAD data and can't be undone! (y/N): " delete_dir_choice
case "$delete_dir_choice" in
y|Y )
echo "Removing Project N.O.M.A.D. files..."
echo "Removing Project NOMAD files..."
if rm -rf "${NOMAD_DIR}"; then
echo "Project N.O.M.A.D. files removed."
echo "Project NOMAD files removed."
else
echo "Warning: Failed to fully remove ${NOMAD_DIR}. You may need to remove it manually."
fi
@ -103,14 +103,14 @@ storage_cleanup() {
}
uninstall_nomad() {
echo "Stopping and removing Project N.O.M.A.D. management containers..."
echo "Stopping and removing Project NOMAD management containers..."
docker compose -p project-nomad -f "${MANAGEMENT_COMPOSE_FILE}" down
echo "Allowing some time for management containers to stop..."
sleep 5
# Stop and remove all containers where name starts with "nomad_"
echo "Stopping and removing all Project N.O.M.A.D. app containers..."
echo "Stopping and removing all Project NOMAD app containers..."
docker ps -a --filter "name=^nomad_" --format "{{.Names}}" | xargs -r docker rm -f
echo "Allowing some time for app containers to stop..."
sleep 5
@ -128,7 +128,7 @@ uninstall_nomad() {
# Prompt user for storage cleanup and handle it if so
storage_cleanup
echo "Project N.O.M.A.D. has been uninstalled. We hope to see you again soon!"
echo "Project NOMAD has been uninstalled. We hope to see you again soon!"
}
###################################################################################################################################################################################################

View File

@ -1,10 +1,10 @@
#!/bin/bash
# Project N.O.M.A.D. Update Script
# Project NOMAD Update Script
###################################################################################################################################################################################################
# Script | Project N.O.M.A.D. Update Script
# Script | Project NOMAD Update Script
# Version | 1.0.1
# Author | Crosstalk Solutions, LLC
# Website | https://crosstalksolutions.com
@ -61,7 +61,7 @@ check_is_debian_based() {
}
get_update_confirmation(){
read -p "This script will update Project N.O.M.A.D. and its dependencies on your machine. No data loss is expected, but you should always back up your data before proceeding. Are you sure you want to continue? (y/n): " choice
read -p "This script will update Project NOMAD and its dependencies on your machine. No data loss is expected, but you should always back up your data before proceeding. Are you sure you want to continue? (y/n): " choice
case "$choice" in
y|Y )
echo -e "${GREEN}#${RESET} User chose to continue with the update."
@ -80,7 +80,7 @@ get_update_confirmation(){
ensure_docker_installed_and_running() {
if ! command -v docker &> /dev/null; then
echo -e "${RED}#${RESET} Docker is not installed. This is unexpected, as Project N.O.M.A.D. requires Docker to run. Did you mean to use the install script instead of the update script?"
echo -e "${RED}#${RESET} Docker is not installed. This is unexpected, as Project NOMAD requires Docker to run. Did you mean to use the install script instead of the update script?"
exit 1
fi
@ -134,11 +134,11 @@ get_local_ip() {
}
success_message() {
echo -e "${GREEN}#${RESET} Project N.O.M.A.D installation completed successfully!\\n"
echo -e "${GREEN}#${RESET} Project NOMAD installation completed successfully!\\n"
echo -e "${GREEN}#${RESET} Installation files are located at /opt/project-nomad\\n\n"
echo -e "${GREEN}#${RESET} Project N.O.M.A.D's Command Center should automatically start whenever your device reboots. However, if you need to start it manually, you can always do so by running: ${WHITE_R}${nomad_dir}/start_nomad.sh${RESET}\\n"
echo -e "${GREEN}#${RESET} Project NOMAD's Command Center should automatically start whenever your device reboots. However, if you need to start it manually, you can always do so by running: ${WHITE_R}${nomad_dir}/start_nomad.sh${RESET}\\n"
echo -e "${GREEN}#${RESET} You can now access the management interface at http://localhost:8080 or http://${local_ip_address}:8080\\n"
echo -e "${GREEN}#${RESET} Thank you for supporting Project N.O.M.A.D!\\n"
echo -e "${GREEN}#${RESET} Thank you for supporting Project NOMAD!\\n"
}
###################################################################################################################################################################################################

View File

@ -1,7 +1,7 @@
{
"name": "project-nomad",
"version": "1.33.0",
"description": "Project N.O.M.A.D. (Node for Offline Media, Archives, and Data) - an offline-first knowledge and education server.",
"description": "Project NOMAD - an offline-first knowledge and education server.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"