mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-28 03:29:25 +01:00
Compare commits
24 Commits
v1.30.3-rc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44ecf41ca6 | ||
|
|
5c92c89813 | ||
|
|
f9e3773ec3 | ||
|
|
e5a7edca03 | ||
|
|
bd015f4c56 | ||
|
|
0e60e246e1 | ||
|
|
c67653b87a | ||
|
|
643eaea84b | ||
|
|
150134a9fa | ||
|
|
6b558531be | ||
|
|
4642dee6ce | ||
|
|
78c0b1d24d | ||
|
|
0226e651c7 | ||
|
|
7ab5e65826 | ||
|
|
b7ed8b6694 | ||
|
|
4443799cc9 | ||
|
|
4219e753da | ||
|
|
f00bfff77c | ||
|
|
5e93f2661b | ||
|
|
9a8378d63a | ||
|
|
982dceb949 | ||
|
|
6a1d0e83f9 | ||
|
|
edcfd937e2 | ||
|
|
f9062616b8 |
25
.github/workflows/build-admin-on-pr.yml
vendored
Normal file
25
.github/workflows/build-admin-on-pr.yml
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
name: Build Admin
|
||||||
|
|
||||||
|
on: pull_request
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: '24'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
working-directory: ./admin
|
||||||
|
|
||||||
|
- name: Run build
|
||||||
|
run: npm run build
|
||||||
|
working-directory: ./admin
|
||||||
2
.github/workflows/build-disk-collector.yml
vendored
2
.github/workflows/build-disk-collector.yml
vendored
|
|
@ -33,7 +33,7 @@ jobs:
|
||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: Log in to GitHub Container Registry
|
- name: Log in to GitHub Container Registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
2
.github/workflows/build-primary-image.yml
vendored
2
.github/workflows/build-primary-image.yml
vendored
|
|
@ -33,7 +33,7 @@ jobs:
|
||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: Log in to GitHub Container Registry
|
- name: Log in to GitHub Container Registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
2
.github/workflows/build-sidecar-updater.yml
vendored
2
.github/workflows/build-sidecar-updater.yml
vendored
|
|
@ -33,7 +33,7 @@ jobs:
|
||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: Log in to GitHub Container Registry
|
- name: Log in to GitHub Container Registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -22,7 +22,7 @@ jobs:
|
||||||
newVersion: ${{ steps.semver.outputs.new_release_version }}
|
newVersion: ${{ steps.semver.outputs.new_release_version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ jobs:
|
||||||
validate-urls:
|
validate-urls:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Extract and validate URLs
|
- name: Extract and validate URLs
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
100
FAQ.md
Normal file
100
FAQ.md
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
# Frequently Asked Questions (FAQ)
|
||||||
|
|
||||||
|
Find answers to some of the most common questions about Project N.O.M.A.D.
|
||||||
|
|
||||||
|
## Can I customize the port(s) that NOMAD uses?
|
||||||
|
|
||||||
|
Yes, you can customize the ports that NOMAD's core services (Command Center, MySQL, Redis) use. Please refer to the [Advanced Installation](README.md#advanced-installation) section of the README for more details on how to do this.
|
||||||
|
|
||||||
|
Note: As of 3/24/2026, only the core services defined in the `docker-compose.yml` file currently support port customization - the installable applications (e.g. Ollama, Kiwix, etc.) do not yet support this, but we have multiple PR's in the works to add this feature for all installable applications in a future release.
|
||||||
|
|
||||||
|
## Can I customize the storage location for NOMAD's data?
|
||||||
|
|
||||||
|
Yes, you can customize the storage location for NOMAD's content by modifying the `docker-compose.yml` file to adjust the appropriate bind mounts to point to your desired storage location on your host machine. Please refer to the [Advanced Installation](README.md#advanced-installation) section of the README for more details on how to do this.
|
||||||
|
|
||||||
|
## Can I run NOMAD on MAC, WSL2, or a non-Debian-based Distro?
|
||||||
|
|
||||||
|
See [Why does NOMAD require a Debian-based OS?](#why-does-nomad-require-a-debian-based-os)
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Community members have provided guides for running N.O.M.A.D. on other platforms (e.g. WSL2, Mac, etc.) 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.
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## What technologies is NOMAD built with?
|
||||||
|
|
||||||
|
Project N.O.M.A.D. 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
|
||||||
|
- **MySQL:** for the Command Center's database
|
||||||
|
- **Redis:** for various caching, background jobs, "cron" tasks, and other internal processes within the Command Center
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Why does NOMAD require access to the Docker socket?
|
||||||
|
|
||||||
|
See [What technologies is NOMAD built with?](#what-technologies-is-nomad-built-with)
|
||||||
|
|
||||||
|
## Can I use any AI models?
|
||||||
|
NOMAD by default uses Ollama inside of a docker container to run LLM Models for the AI Assistant. So if you find a model on HuggingFace for example, you won't be able to use that model in NOMAD. The list of available models in the AI Assistant settings (/settings/models) may not show all of the models you are looking for. If you found a model from https://ollama.com/search that you'd like to try and its not in the settings page, you can use a curl command to download the model.
|
||||||
|
`curl -X POST -H "Content-Type: application/json" -d '{"model":"MODEL_NAME_HERE"}' http://localhost:8080/api/ollama/models` replacing MODEL_NAME_HERE with the model name from whats in the ollama website.
|
||||||
|
|
||||||
|
## Do I have to install the AI features in NOMAD?
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 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 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.
|
||||||
|
|
||||||
|
## How often are new features added or updates released?
|
||||||
|
|
||||||
|
We aim to release updates and new features on a regular basis, but the exact timing can vary based on the complexity of the features being developed, the resources available to our volunteer development team, and the feedback and needs of our community. We typically release smaller "patch" versions more frequently to address bugs and make minor improvements, while larger feature releases may take more time to develop and test before they're ready for release.
|
||||||
|
|
||||||
|
## I opened a PR to contribute a new feature or fix a bug. How long does it usually take for PRs to be reviewed and merged?
|
||||||
|
We appreciate all contributions to the project and strive to review and merge pull requests (PRs) as quickly as possible. The time it takes for a PR to be reviewed and merged can vary based on several factors, including the complexity of the changes, the current workload of our maintainers, and the need for any additional testing or revisions.
|
||||||
|
|
||||||
|
Because NOMAD is still a young project, some PRs (particularly those for new features) may take longer to review and merge as we prioritize building out the core functionality and ensuring stability before adding new features. However, we do our best to provide timely feedback on all PRs and keep contributors informed about the status of their contributions.
|
||||||
|
|
||||||
|
## I have a question that isn't answered here. Where can I ask for help?
|
||||||
|
|
||||||
|
If you have a question that isn't answered in this FAQ, please feel free to ask for help in our Discord community (https://discord.com/invite/crosstalksolutions) or on our Github Discussions page (https://github.com/Crosstalk-Solutions/project-nomad/discussions).
|
||||||
|
|
||||||
|
## I have a suggestion for a new feature or improvement. How can I share it?
|
||||||
|
|
||||||
|
We welcome and encourage suggestions for new features and improvements! We highly encourage sharing your ideas (or upvoting existing suggestions) on our public roadmap at https://roadmap.projectnomad.us, where we track new feature requests. This is the best way to ensure that your suggestion is seen by the development team and the community, and it also allows other community members to upvote and show support for your idea, which can help prioritize it for future development.
|
||||||
48
README.md
48
README.md
|
|
@ -21,13 +21,15 @@ Project N.O.M.A.D. can be installed on any Debian-based operating system (we rec
|
||||||
|
|
||||||
*Note: sudo/root privileges are required to run the install script*
|
*Note: sudo/root privileges are required to run the install script*
|
||||||
|
|
||||||
#### Quick Install (Debian-based OS Only)
|
### Quick Install (Debian-based OS Only)
|
||||||
```bash
|
```bash
|
||||||
sudo apt-get update && sudo apt-get install -y curl && curl -fsSL https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/install_nomad.sh -o install_nomad.sh && sudo bash install_nomad.sh
|
sudo apt-get update && sudo apt-get install -y curl && curl -fsSL https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/install_nomad.sh -o install_nomad.sh && 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 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!
|
||||||
|
|
||||||
|
For a complete step-by-step walkthrough (including Ubuntu installation), see the [Installation Guide](https://www.projectnomad.us/install).
|
||||||
|
|
||||||
### Advanced Installation
|
### Advanced Installation
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|
@ -87,6 +89,9 @@ To run LLM's and other included AI tools:
|
||||||
|
|
||||||
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 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
|
||||||
|
|
||||||
|
## Frequently Asked Questions (FAQ)
|
||||||
|
For answers to common questions about Project N.O.M.A.D., please see our [FAQ](FAQ.md) page.
|
||||||
|
|
||||||
## About Internet Usage & Privacy
|
## 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 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.
|
||||||
|
|
||||||
|
|
@ -95,49 +100,20 @@ To test internet connectivity, N.O.M.A.D. attempts to make a request to Cloudfla
|
||||||
## About Security
|
## 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 it's resources), you can block/open ports to control which services are exposed.
|
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 it's 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 uses 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.). 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.
|
**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 uses 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.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
Contributions are welcome and appreciated! Please read this section fully to understand how to contribute to the project.
|
Contributions are welcome and appreciated! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to contribute to the project.
|
||||||
|
|
||||||
### General Guidelines
|
|
||||||
|
|
||||||
- **Open an issue first**: Before starting work on a new feature or bug fix, please open an issue to discuss your proposed changes. This helps ensure that your contribution aligns with the project's goals and avoids duplicate work. Title the issue clearly and provide a detailed description of the problem or feature you want to work on.
|
|
||||||
- **Fork the repository**: Click the "Fork" button at the top right of the repository page to create a copy of the project under your GitHub account.
|
|
||||||
- **Create a new branch**: In your forked repository, create a new branch for your work. Use a descriptive name for the branch that reflects the purpose of your changes (e.g., `fix/issue-123` or `feature/add-new-tool`).
|
|
||||||
- **Make your changes**: Implement your changes in the new branch. Follow the existing code style and conventions used in the project. Be sure to test your changes locally to ensure they work as expected.
|
|
||||||
- **Add Release Notes**: If your changes include new features, bug fixes, or improvements, please see the "Release Notes" section below to properly document your contribution for the next release.
|
|
||||||
- **Conventional Commits**: When committing your changes, please use conventional commit messages to provide clear and consistent commit history. The format is `<type>(<scope>): <description>`, where:
|
|
||||||
- `type` is the type of change (e.g., `feat` for new features, `fix` for bug fixes, `docs` for documentation changes, etc.)
|
|
||||||
- `scope` is an optional area of the codebase that your change affects (e.g., `api`, `ui`, `docs`, etc.)
|
|
||||||
- `description` is a brief summary of the change
|
|
||||||
- **Submit a pull request**: Once your changes are ready, submit a pull request to the main repository. Provide a clear description of your changes and reference any related issues. The project maintainers will review your pull request and may provide feedback or request changes before it can be merged.
|
|
||||||
- **Be responsive to feedback**: If the maintainers request changes or provide feedback on your pull request, please respond in a timely manner. Stale pull requests may be closed if there is no activity for an extended period.
|
|
||||||
- **Follow the project's code of conduct**: Please adhere to the project's code of conduct when interacting with maintainers and other contributors. Be respectful and considerate in your communications.
|
|
||||||
- **No guarantee of acceptance**: The project is community-driven, and all contributions are appreciated, but acceptance is not guaranteed. The maintainers will evaluate each contribution based on its quality, relevance, and alignment with the project's goals.
|
|
||||||
- **Thank you for contributing to Project N.O.M.A.D.!** Your efforts help make this project better for everyone.
|
|
||||||
|
|
||||||
### Versioning
|
|
||||||
This project uses semantic versioning. The version is managed in the root `package.json`
|
|
||||||
and automatically updated by semantic-release. For simplicity's sake, the "project-nomad" image
|
|
||||||
uses the same version defined there instead of the version in `admin/package.json` (stays at 0.0.0), as it's the only published image derived from the code.
|
|
||||||
|
|
||||||
### Release Notes
|
|
||||||
Human-readable release notes live in [`admin/docs/release-notes.md`](admin/docs/release-notes.md) and are displayed in the Command Center's built-in documentation.
|
|
||||||
|
|
||||||
When working on changes, add a summary to the `## Unreleased` section at the top of that file under the appropriate heading:
|
|
||||||
|
|
||||||
- **Features** — new user-facing capabilities
|
|
||||||
- **Bug Fixes** — corrections to existing behavior
|
|
||||||
- **Improvements** — enhancements, refactors, docs, or dependency updates
|
|
||||||
|
|
||||||
Use the format `- **Area**: Description` to stay consistent with existing entries. When a release is triggered, CI automatically stamps the version and date, commits the update, and pushes the content to the GitHub release.
|
|
||||||
|
|
||||||
## Community & Resources
|
## Community & Resources
|
||||||
|
|
||||||
- **Website:** [www.projectnomad.us](https://www.projectnomad.us) - Learn more about the project
|
- **Website:** [www.projectnomad.us](https://www.projectnomad.us) - Learn more about the project
|
||||||
- **Discord:** [Join the Community](https://discord.com/invite/crosstalksolutions) - Get help, share your builds, and connect with other NOMAD users
|
- **Discord:** [Join the Community](https://discord.com/invite/crosstalksolutions) - Get help, share your builds, and connect with other NOMAD users
|
||||||
- **Benchmark Leaderboard:** [benchmark.projectnomad.us](https://benchmark.projectnomad.us) - See how your hardware stacks up against other NOMAD builds
|
- **Benchmark Leaderboard:** [benchmark.projectnomad.us](https://benchmark.projectnomad.us) - See how your hardware stacks up against other NOMAD builds
|
||||||
|
- **Troubleshooting Guide:** [TROUBLESHOOTING.md](TROUBLESHOOTING.md) - Find solutions to common issues
|
||||||
|
- **FAQ:** [FAQ.md](FAQ.md) - Find answers to frequently asked questions
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export default class SystemController {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
response.send({ success: true, message: result.message });
|
response.send({ success: true, message: result.message });
|
||||||
} else {
|
} else {
|
||||||
response.status(400).send({ error: result.message });
|
response.status(400).send({ success: false, message: result.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Job } from 'bullmq'
|
import { Job, UnrecoverableError } from 'bullmq'
|
||||||
import { QueueService } from '#services/queue_service'
|
import { QueueService } from '#services/queue_service'
|
||||||
import { createHash } from 'crypto'
|
import { createHash } from 'crypto'
|
||||||
import logger from '@adonisjs/core/services/logger'
|
import logger from '@adonisjs/core/services/logger'
|
||||||
|
|
@ -63,6 +63,10 @@ export class DownloadModelJob {
|
||||||
logger.error(
|
logger.error(
|
||||||
`[DownloadModelJob] Failed to initiate download for model ${modelName}: ${result.message}`
|
`[DownloadModelJob] Failed to initiate download for model ${modelName}: ${result.message}`
|
||||||
)
|
)
|
||||||
|
// Don't retry errors that will never succeed (e.g., Ollama version too old)
|
||||||
|
if (result.retryable === false) {
|
||||||
|
throw new UnrecoverableError(result.message)
|
||||||
|
}
|
||||||
throw new Error(`Failed to initiate download for model: ${result.message}`)
|
throw new Error(`Failed to initiate download for model: ${result.message}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,6 +89,15 @@ export class DownloadModelJob {
|
||||||
const queue = queueService.getQueue(this.queue)
|
const queue = queueService.getQueue(this.queue)
|
||||||
const jobId = this.getJobId(params.modelName)
|
const jobId = this.getJobId(params.modelName)
|
||||||
|
|
||||||
|
// Clear any previous failed job so a fresh attempt can be dispatched
|
||||||
|
const existing = await queue.getJob(jobId)
|
||||||
|
if (existing) {
|
||||||
|
const state = await existing.getState()
|
||||||
|
if (state === 'failed') {
|
||||||
|
await existing.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const job = await queue.add(this.key, params, {
|
const job = await queue.add(this.key, params, {
|
||||||
jobId,
|
jobId,
|
||||||
|
|
@ -104,9 +117,9 @@ export class DownloadModelJob {
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.message.includes('job already exists')) {
|
if (error.message.includes('job already exists')) {
|
||||||
const existing = await queue.getJob(jobId)
|
const active = await queue.getJob(jobId)
|
||||||
return {
|
return {
|
||||||
job: existing,
|
job: active,
|
||||||
created: false,
|
created: false,
|
||||||
message: `Job already exists for model ${params.modelName}`,
|
message: `Job already exists for model ${params.modelName}`,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -571,10 +571,10 @@ export class BenchmarkService {
|
||||||
*/
|
*/
|
||||||
private _normalizeScore(value: number, reference: number): number {
|
private _normalizeScore(value: number, reference: number): number {
|
||||||
if (value <= 0) return 0
|
if (value <= 0) return 0
|
||||||
// Log scale: score = 50 * (1 + log2(value/reference))
|
// Log scale with widened range: dividing log2 by 3 prevents scores from
|
||||||
// This gives 50 at reference value, scales logarithmically
|
// clamping to 0% for below-average hardware. Gives 50% at reference value.
|
||||||
const ratio = value / reference
|
const ratio = value / reference
|
||||||
const score = 50 * (1 + Math.log2(Math.max(0.01, ratio)))
|
const score = 50 * (1 + Math.log2(Math.max(0.01, ratio)) / 3)
|
||||||
return Math.min(100, Math.max(0, score)) / 100
|
return Math.min(100, Math.max(0, score)) / 100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -583,9 +583,9 @@ export class BenchmarkService {
|
||||||
*/
|
*/
|
||||||
private _normalizeScoreInverse(value: number, reference: number): number {
|
private _normalizeScoreInverse(value: number, reference: number): number {
|
||||||
if (value <= 0) return 1
|
if (value <= 0) return 1
|
||||||
// Inverse: lower values = higher scores
|
// Inverse: lower values = higher scores, with widened log range
|
||||||
const ratio = reference / value
|
const ratio = reference / value
|
||||||
const score = 50 * (1 + Math.log2(Math.max(0.01, ratio)))
|
const score = 50 * (1 + Math.log2(Math.max(0.01, ratio)) / 3)
|
||||||
return Math.min(100, Math.max(0, score)) / 100
|
return Math.min(100, Math.max(0, score)) / 100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -619,6 +619,7 @@ export class BenchmarkService {
|
||||||
const eventsMatch = output.match(/events per second:\s*([\d.]+)/i)
|
const eventsMatch = output.match(/events per second:\s*([\d.]+)/i)
|
||||||
const totalTimeMatch = output.match(/total time:\s*([\d.]+)s/i)
|
const totalTimeMatch = output.match(/total time:\s*([\d.]+)s/i)
|
||||||
const totalEventsMatch = output.match(/total number of events:\s*(\d+)/i)
|
const totalEventsMatch = output.match(/total number of events:\s*(\d+)/i)
|
||||||
|
logger.debug(`[BenchmarkService] CPU output parsing - events/s: ${eventsMatch?.[1]}, total_time: ${totalTimeMatch?.[1]}, total_events: ${totalEventsMatch?.[1]}`)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
events_per_second: eventsMatch ? parseFloat(eventsMatch[1]) : 0,
|
events_per_second: eventsMatch ? parseFloat(eventsMatch[1]) : 0,
|
||||||
|
|
|
||||||
|
|
@ -615,8 +615,8 @@ export class DockerService {
|
||||||
* We'll download the lightweight mini Wikipedia Top 100 zim file for this purpose.
|
* We'll download the lightweight mini Wikipedia Top 100 zim file for this purpose.
|
||||||
**/
|
**/
|
||||||
const WIKIPEDIA_ZIM_URL =
|
const WIKIPEDIA_ZIM_URL =
|
||||||
'https://github.com/Crosstalk-Solutions/project-nomad/raw/refs/heads/main/install/wikipedia_en_100_mini_2025-06.zim'
|
'https://github.com/Crosstalk-Solutions/project-nomad/raw/refs/heads/main/install/wikipedia_en_100_mini_2026-01.zim'
|
||||||
const filename = 'wikipedia_en_100_mini_2025-06.zim'
|
const filename = 'wikipedia_en_100_mini_2026-01.zim'
|
||||||
const filepath = join(process.cwd(), ZIM_STORAGE_PATH, filename)
|
const filepath = join(process.cwd(), ZIM_STORAGE_PATH, filename)
|
||||||
logger.info(`[DockerService] Kiwix Serve pre-install: Downloading ZIM file to ${filepath}`)
|
logger.info(`[DockerService] Kiwix Serve pre-install: Downloading ZIM file to ${filepath}`)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export class OllamaService {
|
||||||
* @param model Model name to download
|
* @param model Model name to download
|
||||||
* @returns Success status and message
|
* @returns Success status and message
|
||||||
*/
|
*/
|
||||||
async downloadModel(model: string, progressCallback?: (percent: number) => void): Promise<{ success: boolean; message: string }> {
|
async downloadModel(model: string, progressCallback?: (percent: number) => void): Promise<{ success: boolean; message: string; retryable?: boolean }> {
|
||||||
try {
|
try {
|
||||||
await this._ensureDependencies()
|
await this._ensureDependencies()
|
||||||
if (!this.ollama) {
|
if (!this.ollama) {
|
||||||
|
|
@ -86,11 +86,21 @@ export class OllamaService {
|
||||||
logger.info(`[OllamaService] Model "${model}" downloaded successfully.`)
|
logger.info(`[OllamaService] Model "${model}" downloaded successfully.`)
|
||||||
return { success: true, message: 'Model downloaded successfully.' }
|
return { success: true, message: 'Model downloaded successfully.' }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
logger.error(
|
logger.error(
|
||||||
`[OllamaService] Failed to download model "${model}": ${error instanceof Error ? error.message : error
|
`[OllamaService] Failed to download model "${model}": ${errorMessage}`
|
||||||
}`
|
|
||||||
)
|
)
|
||||||
return { success: false, message: 'Failed to download model.' }
|
|
||||||
|
// Check for version mismatch (Ollama 412 response)
|
||||||
|
const isVersionMismatch = errorMessage.includes('newer version of Ollama')
|
||||||
|
const userMessage = isVersionMismatch
|
||||||
|
? 'This model requires a newer version of Ollama. Please update AI Assistant from the Apps page.'
|
||||||
|
: `Failed to download model: ${errorMessage}`
|
||||||
|
|
||||||
|
// Broadcast failure to connected clients so UI can show the error
|
||||||
|
this.broadcastDownloadError(model, userMessage)
|
||||||
|
|
||||||
|
return { success: false, message: userMessage, retryable: !isVersionMismatch }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -379,6 +389,15 @@ export class OllamaService {
|
||||||
return models
|
return models
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private broadcastDownloadError(model: string, error: string) {
|
||||||
|
transmit.broadcast(BROADCAST_CHANNELS.OLLAMA_MODEL_DOWNLOAD, {
|
||||||
|
model,
|
||||||
|
percent: -1,
|
||||||
|
error,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private broadcastDownloadProgress(model: string, percent: number) {
|
private broadcastDownloadProgress(model: string, percent: number) {
|
||||||
transmit.broadcast(BROADCAST_CHANNELS.OLLAMA_MODEL_DOWNLOAD, {
|
transmit.broadcast(BROADCAST_CHANNELS.OLLAMA_MODEL_DOWNLOAD, {
|
||||||
model,
|
model,
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ export default class ServiceSeeder extends BaseSeeder {
|
||||||
display_order: 3,
|
display_order: 3,
|
||||||
description: 'Local AI chat that runs entirely on your hardware - no internet required',
|
description: 'Local AI chat that runs entirely on your hardware - no internet required',
|
||||||
icon: 'IconWand',
|
icon: 'IconWand',
|
||||||
container_image: 'ollama/ollama:0.15.2',
|
container_image: 'ollama/ollama:0.18.1',
|
||||||
source_repo: 'https://github.com/ollama/ollama',
|
source_repo: 'https://github.com/ollama/ollama',
|
||||||
container_command: 'serve',
|
container_command: 'serve',
|
||||||
container_config: JSON.stringify({
|
container_config: JSON.stringify({
|
||||||
|
|
@ -94,7 +94,7 @@ export default class ServiceSeeder extends BaseSeeder {
|
||||||
display_order: 11,
|
display_order: 11,
|
||||||
description: 'Swiss Army knife for data encoding, encryption, and analysis',
|
description: 'Swiss Army knife for data encoding, encryption, and analysis',
|
||||||
icon: 'IconChefHat',
|
icon: 'IconChefHat',
|
||||||
container_image: 'ghcr.io/gchq/cyberchef:10.19.4',
|
container_image: 'ghcr.io/gchq/cyberchef:10.22.1',
|
||||||
source_repo: 'https://github.com/gchq/CyberChef',
|
source_repo: 'https://github.com/gchq/CyberChef',
|
||||||
container_command: null,
|
container_command: null,
|
||||||
container_config: JSON.stringify({
|
container_config: JSON.stringify({
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,25 @@
|
||||||
# Release Notes
|
# Release Notes
|
||||||
|
|
||||||
|
## Version 1.30.3 - March 25, 2026
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- **Benchmark**: Fixed an issue where CPU and Disk Write scores could be displayed as 0 if the measured values was less than half of the reference mark. Thanks @bortlesboat for the fix!
|
||||||
|
- **Content Manager**: Fixed a missing API client method that was causing ZIM file deletions to fail. Thanks @LuisMIguelFurlanettoSousa for the fix!
|
||||||
|
- **Install**: Fixed an issue where the install script could incorrectly report the Docker NVIDIA runtime as missing. Thanks @brenex for the fix!
|
||||||
|
- **Support the Project**: Fixed a broken link to Rogue Support. Thanks @chriscrosstalk for the fix!
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
- **AI Assistant**: Improved error reporting and handling for model downloads. Thanks @chriscrosstalk for the contribution!
|
||||||
|
- **AI Assistant**: Bumped the default version of Ollama installed to v0.18.1 to take advantage of the latest performance improvements and bug fixes.
|
||||||
|
- **Apps**: Improved error reporting and handling for service installation failures. Thanks @trek-e for the contribution!
|
||||||
|
- **Collections**: Updated various curated collection links to their latest versions. Thanks @builder555 for the contribution!
|
||||||
|
- **Cyberchef**: Bumped the default version of CyberChef installed to v10.22.1 to take advantage of the latest features and bug fixes.
|
||||||
|
- **Docs**: Added a link to the step-by-step installation guide and video tutorial. Thanks @chriscrosstalk for the contribution!
|
||||||
|
- **Install**: Increased the retries limit for the MySQL service in Docker Compose to improve stability during installation on systems with slower performance. Thanks @dx4956 for the contribution!
|
||||||
|
- **Install**: Fixed an issue where stale data could cause credentials mismatch in MySQL on reinstall. Thanks @chriscrosstalk for the fix!
|
||||||
|
|
||||||
## Version 1.30.0 - March 20, 2026
|
## Version 1.30.0 - March 20, 2026
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import useOllamaModelDownloads from '~/hooks/useOllamaModelDownloads'
|
import useOllamaModelDownloads from '~/hooks/useOllamaModelDownloads'
|
||||||
import HorizontalBarChart from './HorizontalBarChart'
|
import HorizontalBarChart from './HorizontalBarChart'
|
||||||
import StyledSectionHeader from './StyledSectionHeader'
|
import StyledSectionHeader from './StyledSectionHeader'
|
||||||
|
import { IconAlertTriangle } from '@tabler/icons-react'
|
||||||
|
|
||||||
interface ActiveModelDownloadsProps {
|
interface ActiveModelDownloadsProps {
|
||||||
withHeader?: boolean
|
withHeader?: boolean
|
||||||
|
|
@ -17,19 +18,31 @@ const ActiveModelDownloads = ({ withHeader = false }: ActiveModelDownloadsProps)
|
||||||
downloads.map((download) => (
|
downloads.map((download) => (
|
||||||
<div
|
<div
|
||||||
key={download.model}
|
key={download.model}
|
||||||
className="bg-desert-white rounded-lg p-4 border border-desert-stone-light shadow-sm hover:shadow-lg transition-shadow"
|
className={`bg-desert-white rounded-lg p-4 border shadow-sm hover:shadow-lg transition-shadow ${
|
||||||
|
download.error ? 'border-red-400' : 'border-desert-stone-light'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<HorizontalBarChart
|
{download.error ? (
|
||||||
items={[
|
<div className="flex items-start gap-3">
|
||||||
{
|
<IconAlertTriangle className="text-red-500 flex-shrink-0 mt-0.5" size={20} />
|
||||||
label: download.model,
|
<div>
|
||||||
value: download.percent,
|
<p className="font-medium text-text-primary">{download.model}</p>
|
||||||
total: '100%',
|
<p className="text-sm text-red-600 mt-1">{download.error}</p>
|
||||||
used: `${download.percent.toFixed(1)}%`,
|
</div>
|
||||||
type: 'ollama-model',
|
</div>
|
||||||
},
|
) : (
|
||||||
]}
|
<HorizontalBarChart
|
||||||
/>
|
items={[
|
||||||
|
{
|
||||||
|
label: download.model,
|
||||||
|
value: download.percent,
|
||||||
|
total: '100%',
|
||||||
|
used: `${download.percent.toFixed(1)}%`,
|
||||||
|
type: 'ollama-model',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -78,11 +78,11 @@ const StyledSidebar: React.FC<StyledSidebarProps> = ({ title, items }) => {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<div className="mb-4 flex flex-col items-center gap-1 text-sm text-text-secondary">
|
<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 N.O.M.A.D. Command Center v{appVersion}</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => setDebugModalOpen(true)}
|
onClick={() => setDebugModalOpen(true)}
|
||||||
className="mt-1 text-gray-500 hover:text-desert-green inline-flex items-center gap-1 cursor-pointer"
|
className="text-gray-500 hover:text-desert-green inline-flex items-center gap-1 cursor-pointer"
|
||||||
>
|
>
|
||||||
<IconBug className="size-3.5" />
|
<IconBug className="size-3.5" />
|
||||||
Debug Info
|
Debug Info
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export default function ThemeToggle({ compact = false }: ThemeToggleProps) {
|
||||||
<button
|
<button
|
||||||
onClick={toggleTheme}
|
onClick={toggleTheme}
|
||||||
className="flex items-center gap-1.5 rounded-md px-2 py-1 text-sm transition-colors
|
className="flex items-center gap-1.5 rounded-md px-2 py-1 text-sm transition-colors
|
||||||
text-desert-stone hover:text-desert-green-darker"
|
text-desert-stone hover:text-desert-green-darker cursor-pointer"
|
||||||
aria-label={isDark ? 'Switch to Day Ops' : 'Switch to Night Ops'}
|
aria-label={isDark ? 'Switch to Day Ops' : 'Switch to Night Ops'}
|
||||||
title={isDark ? 'Switch to Day Ops' : 'Switch to Night Ops'}
|
title={isDark ? 'Switch to Day Ops' : 'Switch to Night Ops'}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { resolveTierResources } from '~/lib/collections'
|
||||||
import { formatBytes } from '~/lib/util'
|
import { formatBytes } from '~/lib/util'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import DynamicIcon, { DynamicIconName } from './DynamicIcon'
|
import DynamicIcon, { DynamicIconName } from './DynamicIcon'
|
||||||
|
import StyledButton from './StyledButton'
|
||||||
|
|
||||||
interface TierSelectionModalProps {
|
interface TierSelectionModalProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
|
|
@ -213,18 +214,14 @@ const TierSelectionModal: React.FC<TierSelectionModalProps> = ({
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="bg-surface-secondary px-6 py-4 flex justify-end gap-3">
|
<div className="bg-surface-secondary px-6 py-4 flex justify-end gap-3">
|
||||||
<button
|
<StyledButton
|
||||||
|
variant='primary'
|
||||||
|
size='lg'
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={!localSelectedSlug}
|
disabled={!localSelectedSlug}
|
||||||
className={classNames(
|
|
||||||
'px-4 py-2 rounded-md font-medium transition-colors',
|
|
||||||
localSelectedSlug
|
|
||||||
? 'bg-desert-green text-white hover:bg-desert-green/90'
|
|
||||||
: 'bg-border-default text-text-muted cursor-not-allowed'
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
Submit
|
Submit
|
||||||
</button>
|
</StyledButton>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ export type OllamaModelDownload = {
|
||||||
model: string
|
model: string
|
||||||
percent: number
|
percent: number
|
||||||
timestamp: string
|
timestamp: string
|
||||||
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function useOllamaModelDownloads() {
|
export default function useOllamaModelDownloads() {
|
||||||
|
|
@ -17,7 +18,19 @@ export default function useOllamaModelDownloads() {
|
||||||
setDownloads((prev) => {
|
setDownloads((prev) => {
|
||||||
const updated = new Map(prev)
|
const updated = new Map(prev)
|
||||||
|
|
||||||
if (data.percent >= 100) {
|
if (data.percent === -1) {
|
||||||
|
// Download failed — show error state, auto-remove after 15 seconds
|
||||||
|
updated.set(data.model, data)
|
||||||
|
const errorTimeout = setTimeout(() => {
|
||||||
|
timeoutsRef.current.delete(errorTimeout)
|
||||||
|
setDownloads((current) => {
|
||||||
|
const next = new Map(current)
|
||||||
|
next.delete(data.model)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}, 15000)
|
||||||
|
timeoutsRef.current.add(errorTimeout)
|
||||||
|
} else if (data.percent >= 100) {
|
||||||
// If download is complete, keep it for a short time before removing to allow UI to show 100% progress
|
// If download is complete, keep it for a short time before removing to allow UI to show 100% progress
|
||||||
updated.set(data.model, data)
|
updated.set(data.model, data)
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import axios, { AxiosInstance } from 'axios'
|
import axios, { AxiosError, AxiosInstance } from 'axios'
|
||||||
import { ListRemoteZimFilesResponse, ListZimFilesResponse } from '../../types/zim'
|
import { ListRemoteZimFilesResponse, ListZimFilesResponse } from '../../types/zim'
|
||||||
import { ServiceSlim } from '../../types/services'
|
import { ServiceSlim } from '../../types/services'
|
||||||
import { FileEntry } from '../../types/files'
|
import { FileEntry } from '../../types/files'
|
||||||
|
|
@ -25,13 +25,19 @@ class API {
|
||||||
}
|
}
|
||||||
|
|
||||||
async affectService(service_name: string, action: 'start' | 'stop' | 'restart') {
|
async affectService(service_name: string, action: 'start' | 'stop' | 'restart') {
|
||||||
return catchInternal(async () => {
|
try {
|
||||||
const response = await this.client.post<{ success: boolean; message: string }>(
|
const response = await this.client.post<{ success: boolean; message: string }>(
|
||||||
'/system/services/affect',
|
'/system/services/affect',
|
||||||
{ service_name, action }
|
{ service_name, action }
|
||||||
)
|
)
|
||||||
return response.data
|
return response.data
|
||||||
})()
|
} catch (error) {
|
||||||
|
if (error instanceof AxiosError && error.response?.data?.message) {
|
||||||
|
return { success: false, message: error.response.data.message }
|
||||||
|
}
|
||||||
|
console.error('Error affecting service:', error)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkLatestVersion(force: boolean = false) {
|
async checkLatestVersion(force: boolean = false) {
|
||||||
|
|
@ -192,13 +198,19 @@ class API {
|
||||||
}
|
}
|
||||||
|
|
||||||
async forceReinstallService(service_name: string) {
|
async forceReinstallService(service_name: string) {
|
||||||
return catchInternal(async () => {
|
try {
|
||||||
const response = await this.client.post<{ success: boolean; message: string }>(
|
const response = await this.client.post<{ success: boolean; message: string }>(
|
||||||
`/system/services/force-reinstall`,
|
`/system/services/force-reinstall`,
|
||||||
{ service_name }
|
{ service_name }
|
||||||
)
|
)
|
||||||
return response.data
|
return response.data
|
||||||
})()
|
} catch (error) {
|
||||||
|
if (error instanceof AxiosError && error.response?.data?.message) {
|
||||||
|
return { success: false, message: error.response.data.message }
|
||||||
|
}
|
||||||
|
console.error('Error force reinstalling service:', error)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChatSuggestions(signal?: AbortSignal) {
|
async getChatSuggestions(signal?: AbortSignal) {
|
||||||
|
|
@ -459,13 +471,19 @@ class API {
|
||||||
}
|
}
|
||||||
|
|
||||||
async installService(service_name: string) {
|
async installService(service_name: string) {
|
||||||
return catchInternal(async () => {
|
try {
|
||||||
const response = await this.client.post<{ success: boolean; message: string }>(
|
const response = await this.client.post<{ success: boolean; message: string }>(
|
||||||
'/system/services/install',
|
'/system/services/install',
|
||||||
{ service_name }
|
{ service_name }
|
||||||
)
|
)
|
||||||
return response.data
|
return response.data
|
||||||
})()
|
} catch (error) {
|
||||||
|
if (error instanceof AxiosError && error.response?.data?.message) {
|
||||||
|
return { success: false, message: error.response.data.message }
|
||||||
|
}
|
||||||
|
console.error('Error installing service:', error)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async listCuratedMapCollections() {
|
async listCuratedMapCollections() {
|
||||||
|
|
@ -518,6 +536,13 @@ class API {
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteZimFile(filename: string) {
|
||||||
|
return catchInternal(async () => {
|
||||||
|
const response = await this.client.delete<{ message: string }>(`/zim/${filename}`)
|
||||||
|
return response.data
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
|
||||||
async listZimFiles() {
|
async listZimFiles() {
|
||||||
return catchInternal(async () => {
|
return catchInternal(async () => {
|
||||||
return await this.client.get<ListZimFilesResponse>('/zim/list')
|
return await this.client.get<ListZimFilesResponse>('/zim/list')
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export default function SupportPage() {
|
||||||
<section className="mb-12">
|
<section className="mb-12">
|
||||||
<h2 className="text-2xl font-semibold mb-3">Need Help With Your Home Network?</h2>
|
<h2 className="text-2xl font-semibold mb-3">Need Help With Your Home Network?</h2>
|
||||||
<a
|
<a
|
||||||
href="https://roguesupport.com"
|
href="https://rogue.support"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="block mb-4 rounded-lg overflow-hidden hover:opacity-90 transition-opacity"
|
className="block mb-4 rounded-lg overflow-hidden hover:opacity-90 transition-opacity"
|
||||||
|
|
@ -52,12 +52,12 @@ export default function SupportPage() {
|
||||||
Think of it as Uber for computer networking — expert help when you need it.
|
Think of it as Uber for computer networking — expert help when you need it.
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
href="https://roguesupport.com"
|
href="https://rogue.support"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center gap-2 text-blue-600 hover:underline font-medium"
|
className="inline-flex items-center gap-2 text-blue-600 hover:underline font-medium"
|
||||||
>
|
>
|
||||||
Visit RogueSupport.com
|
Visit Rogue.Support
|
||||||
<IconExternalLink size={16} />
|
<IconExternalLink size={16} />
|
||||||
</a>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"spec_version": "2026-02-11",
|
"spec_version": "2026-03-15",
|
||||||
"categories": [
|
"categories": [
|
||||||
{
|
{
|
||||||
"name": "Medicine",
|
"name": "Medicine",
|
||||||
|
|
@ -113,10 +113,10 @@
|
||||||
"resources": [
|
"resources": [
|
||||||
{
|
{
|
||||||
"id": "canadian_prepper_winterprepping_en",
|
"id": "canadian_prepper_winterprepping_en",
|
||||||
"version": "2025-11",
|
"version": "2026-02",
|
||||||
"title": "Canadian Prepper: Winter Prepping",
|
"title": "Canadian Prepper: Winter Prepping",
|
||||||
"description": "Video guides for winter survival and cold weather emergencies",
|
"description": "Video guides for winter survival and cold weather emergencies",
|
||||||
"url": "https://download.kiwix.org/zim/videos/canadian_prepper_winterprepping_en_2025-11.zim",
|
"url": "https://download.kiwix.org/zim/videos/canadian_prepper_winterprepping_en_2026-02.zim",
|
||||||
"size_mb": 1340
|
"size_mb": 1340
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -137,18 +137,18 @@
|
||||||
"resources": [
|
"resources": [
|
||||||
{
|
{
|
||||||
"id": "canadian_prepper_bugoutconcepts_en",
|
"id": "canadian_prepper_bugoutconcepts_en",
|
||||||
"version": "2025-11",
|
"version": "2026-02",
|
||||||
"title": "Canadian Prepper: Bug Out Concepts",
|
"title": "Canadian Prepper: Bug Out Concepts",
|
||||||
"description": "Strategies and planning for emergency evacuation",
|
"description": "Strategies and planning for emergency evacuation",
|
||||||
"url": "https://download.kiwix.org/zim/videos/canadian_prepper_bugoutconcepts_en_2025-11.zim",
|
"url": "https://download.kiwix.org/zim/videos/canadian_prepper_bugoutconcepts_en_2026-02.zim",
|
||||||
"size_mb": 2890
|
"size_mb": 2890
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "urban-prepper_en_all",
|
"id": "urban-prepper_en_all",
|
||||||
"version": "2025-11",
|
"version": "2026-02",
|
||||||
"title": "Urban Prepper",
|
"title": "Urban Prepper",
|
||||||
"description": "Comprehensive urban emergency preparedness video series",
|
"description": "Comprehensive urban emergency preparedness video series",
|
||||||
"url": "https://download.kiwix.org/zim/videos/urban-prepper_en_all_2025-11.zim",
|
"url": "https://download.kiwix.org/zim/videos/urban-prepper_en_all_2026-02.zim",
|
||||||
"size_mb": 2240
|
"size_mb": 2240
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -194,10 +194,10 @@
|
||||||
"resources": [
|
"resources": [
|
||||||
{
|
{
|
||||||
"id": "wikibooks_en_all_nopic",
|
"id": "wikibooks_en_all_nopic",
|
||||||
"version": "2025-10",
|
"version": "2026-01",
|
||||||
"title": "Wikibooks",
|
"title": "Wikibooks",
|
||||||
"description": "Open-content textbooks covering math, science, computing, and more",
|
"description": "Open-content textbooks covering math, science, computing, and more",
|
||||||
"url": "https://download.kiwix.org/zim/wikibooks/wikibooks_en_all_nopic_2025-10.zim",
|
"url": "https://download.kiwix.org/zim/wikibooks/wikibooks_en_all_nopic_2026-01.zim",
|
||||||
"size_mb": 3100
|
"size_mb": 3100
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -210,35 +210,35 @@
|
||||||
"resources": [
|
"resources": [
|
||||||
{
|
{
|
||||||
"id": "ted_mul_ted-ed",
|
"id": "ted_mul_ted-ed",
|
||||||
"version": "2025-07",
|
"version": "2026-01",
|
||||||
"title": "TED-Ed",
|
"title": "TED-Ed",
|
||||||
"description": "Educational video lessons on science, history, literature, and more",
|
"description": "Educational video lessons on science, history, literature, and more",
|
||||||
"url": "https://download.kiwix.org/zim/ted/ted_mul_ted-ed_2025-07.zim",
|
"url": "https://download.kiwix.org/zim/ted/ted_mul_ted-ed_2026-01.zim",
|
||||||
"size_mb": 5610
|
"size_mb": 5610
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "wikiversity_en_all_maxi",
|
"id": "wikiversity_en_all_maxi",
|
||||||
"version": "2025-11",
|
"version": "2026-02",
|
||||||
"title": "Wikiversity",
|
"title": "Wikiversity",
|
||||||
"description": "Tutorials, courses, and learning materials for all levels",
|
"description": "Tutorials, courses, and learning materials for all levels",
|
||||||
"url": "https://download.kiwix.org/zim/wikiversity/wikiversity_en_all_maxi_2025-11.zim",
|
"url": "https://download.kiwix.org/zim/wikiversity/wikiversity_en_all_maxi_2026-02.zim",
|
||||||
"size_mb": 2370
|
"size_mb": 2370
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "libretexts.org_en_math",
|
"id": "libretexts.org_en_math",
|
||||||
"version": "2025-01",
|
"version": "2026-01",
|
||||||
"title": "LibreTexts Mathematics",
|
"title": "LibreTexts Mathematics",
|
||||||
"description": "Open-source math textbooks from algebra to calculus",
|
"description": "Open-source math textbooks from algebra to calculus",
|
||||||
"url": "https://download.kiwix.org/zim/libretexts/libretexts.org_en_math_2025-01.zim",
|
"url": "https://download.kiwix.org/zim/libretexts/libretexts.org_en_math_2026-01.zim",
|
||||||
"size_mb": 831
|
"size_mb": 792
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "libretexts.org_en_phys",
|
"id": "libretexts.org_en_phys",
|
||||||
"version": "2025-01",
|
"version": "2026-01",
|
||||||
"title": "LibreTexts Physics",
|
"title": "LibreTexts Physics",
|
||||||
"description": "Physics courses and textbooks",
|
"description": "Physics courses and textbooks",
|
||||||
"url": "https://download.kiwix.org/zim/libretexts/libretexts.org_en_phys_2025-01.zim",
|
"url": "https://download.kiwix.org/zim/libretexts/libretexts.org_en_phys_2026-01.zim",
|
||||||
"size_mb": 560
|
"size_mb": 534
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "libretexts.org_en_chem",
|
"id": "libretexts.org_en_chem",
|
||||||
|
|
@ -266,18 +266,18 @@
|
||||||
"resources": [
|
"resources": [
|
||||||
{
|
{
|
||||||
"id": "wikibooks_en_all_maxi",
|
"id": "wikibooks_en_all_maxi",
|
||||||
"version": "2025-10",
|
"version": "2026-01",
|
||||||
"title": "Wikibooks (With Images)",
|
"title": "Wikibooks (With Images)",
|
||||||
"description": "Open textbooks with full illustrations and diagrams",
|
"description": "Open textbooks with full illustrations and diagrams",
|
||||||
"url": "https://download.kiwix.org/zim/wikibooks/wikibooks_en_all_maxi_2025-10.zim",
|
"url": "https://download.kiwix.org/zim/wikibooks/wikibooks_en_all_maxi_2026-01.zim",
|
||||||
"size_mb": 5400
|
"size_mb": 5400
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ted_mul_ted-conference",
|
"id": "ted_mul_ted-conference",
|
||||||
"version": "2025-08",
|
"version": "2026-02",
|
||||||
"title": "TED Conference",
|
"title": "TED Conference",
|
||||||
"description": "Main TED conference talks on ideas worth spreading",
|
"description": "Main TED conference talks on ideas worth spreading",
|
||||||
"url": "https://download.kiwix.org/zim/ted/ted_mul_ted-conference_2025-08.zim",
|
"url": "https://download.kiwix.org/zim/ted/ted_mul_ted-conference_2026-02.zim",
|
||||||
"size_mb": 16500
|
"size_mb": 16500
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -290,11 +290,11 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "libretexts.org_en_geo",
|
"id": "libretexts.org_en_geo",
|
||||||
"version": "2025-01",
|
"version": "2026-01",
|
||||||
"title": "LibreTexts Geosciences",
|
"title": "LibreTexts Geosciences",
|
||||||
"description": "Earth science, geology, and environmental studies",
|
"description": "Earth science, geology, and environmental studies",
|
||||||
"url": "https://download.kiwix.org/zim/libretexts/libretexts.org_en_geo_2025-01.zim",
|
"url": "https://download.kiwix.org/zim/libretexts/libretexts.org_en_geo_2026-01.zim",
|
||||||
"size_mb": 1190
|
"size_mb": 1127
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "libretexts.org_en_eng",
|
"id": "libretexts.org_en_eng",
|
||||||
|
|
@ -306,11 +306,11 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "libretexts.org_en_biz",
|
"id": "libretexts.org_en_biz",
|
||||||
"version": "2025-01",
|
"version": "2026-01",
|
||||||
"title": "LibreTexts Business",
|
"title": "LibreTexts Business",
|
||||||
"description": "Business, economics, and management textbooks",
|
"description": "Business, economics, and management textbooks",
|
||||||
"url": "https://download.kiwix.org/zim/libretexts/libretexts.org_en_biz_2025-01.zim",
|
"url": "https://download.kiwix.org/zim/libretexts/libretexts.org_en_biz_2026-01.zim",
|
||||||
"size_mb": 840
|
"size_mb": 801
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -331,18 +331,18 @@
|
||||||
"resources": [
|
"resources": [
|
||||||
{
|
{
|
||||||
"id": "woodworking.stackexchange.com_en_all",
|
"id": "woodworking.stackexchange.com_en_all",
|
||||||
"version": "2025-12",
|
"version": "2026-02",
|
||||||
"title": "Woodworking Q&A",
|
"title": "Woodworking Q&A",
|
||||||
"description": "Stack Exchange Q&A for carpentry, joinery, and woodcraft",
|
"description": "Stack Exchange Q&A for carpentry, joinery, and woodcraft",
|
||||||
"url": "https://download.kiwix.org/zim/stack_exchange/woodworking.stackexchange.com_en_all_2025-12.zim",
|
"url": "https://download.kiwix.org/zim/stack_exchange/woodworking.stackexchange.com_en_all_2026-02.zim",
|
||||||
"size_mb": 99
|
"size_mb": 99
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "mechanics.stackexchange.com_en_all",
|
"id": "mechanics.stackexchange.com_en_all",
|
||||||
"version": "2025-12",
|
"version": "2026-02",
|
||||||
"title": "Motor Vehicle Maintenance Q&A",
|
"title": "Motor Vehicle Maintenance Q&A",
|
||||||
"description": "Stack Exchange Q&A for car and motorcycle repair",
|
"description": "Stack Exchange Q&A for car and motorcycle repair",
|
||||||
"url": "https://download.kiwix.org/zim/stack_exchange/mechanics.stackexchange.com_en_all_2025-12.zim",
|
"url": "https://download.kiwix.org/zim/stack_exchange/mechanics.stackexchange.com_en_all_2026-02.zim",
|
||||||
"size_mb": 321
|
"size_mb": 321
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -355,10 +355,10 @@
|
||||||
"resources": [
|
"resources": [
|
||||||
{
|
{
|
||||||
"id": "diy.stackexchange.com_en_all",
|
"id": "diy.stackexchange.com_en_all",
|
||||||
"version": "2025-12",
|
"version": "2026-02",
|
||||||
"title": "DIY & Home Improvement Q&A",
|
"title": "DIY & Home Improvement Q&A",
|
||||||
"description": "Stack Exchange Q&A for home repairs, electrical, plumbing, and construction",
|
"description": "Stack Exchange Q&A for home repairs, electrical, plumbing, and construction",
|
||||||
"url": "https://download.kiwix.org/zim/stack_exchange/diy.stackexchange.com_en_all_2025-12.zim",
|
"url": "https://download.kiwix.org/zim/stack_exchange/diy.stackexchange.com_en_all_2026-02.zim",
|
||||||
"size_mb": 1900
|
"size_mb": 1900
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -375,7 +375,7 @@
|
||||||
"title": "iFixit Repair Guides",
|
"title": "iFixit Repair Guides",
|
||||||
"description": "Step-by-step repair guides for electronics, appliances, and vehicles",
|
"description": "Step-by-step repair guides for electronics, appliances, and vehicles",
|
||||||
"url": "https://download.kiwix.org/zim/ifixit/ifixit_en_all_2025-12.zim",
|
"url": "https://download.kiwix.org/zim/ifixit/ifixit_en_all_2025-12.zim",
|
||||||
"size_mb": 3570
|
"size_mb": 3380
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -396,18 +396,18 @@
|
||||||
"resources": [
|
"resources": [
|
||||||
{
|
{
|
||||||
"id": "foss.cooking_en_all",
|
"id": "foss.cooking_en_all",
|
||||||
"version": "2025-11",
|
"version": "2026-02",
|
||||||
"title": "FOSS Cooking",
|
"title": "FOSS Cooking",
|
||||||
"description": "Quick and easy cooking guides and recipes",
|
"description": "Quick and easy cooking guides and recipes",
|
||||||
"url": "https://download.kiwix.org/zim/zimit/foss.cooking_en_all_2025-11.zim",
|
"url": "https://download.kiwix.org/zim/zimit/foss.cooking_en_all_2026-02.zim",
|
||||||
"size_mb": 24
|
"size_mb": 24
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "based.cooking_en_all",
|
"id": "based.cooking_en_all",
|
||||||
"version": "2025-11",
|
"version": "2026-02",
|
||||||
"title": "Based.Cooking",
|
"title": "Based.Cooking",
|
||||||
"description": "Simple, practical recipes from the community",
|
"description": "Simple, practical recipes from the community",
|
||||||
"url": "https://download.kiwix.org/zim/zimit/based.cooking_en_all_2025-11.zim",
|
"url": "https://download.kiwix.org/zim/zimit/based.cooking_en_all_2026-02.zim",
|
||||||
"size_mb": 16
|
"size_mb": 16
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -420,18 +420,18 @@
|
||||||
"resources": [
|
"resources": [
|
||||||
{
|
{
|
||||||
"id": "gardening.stackexchange.com_en_all",
|
"id": "gardening.stackexchange.com_en_all",
|
||||||
"version": "2025-12",
|
"version": "2026-02",
|
||||||
"title": "Gardening Q&A",
|
"title": "Gardening Q&A",
|
||||||
"description": "Stack Exchange Q&A for growing your own food, plant care, and landscaping",
|
"description": "Stack Exchange Q&A for growing your own food, plant care, and landscaping",
|
||||||
"url": "https://download.kiwix.org/zim/stack_exchange/gardening.stackexchange.com_en_all_2025-12.zim",
|
"url": "https://download.kiwix.org/zim/stack_exchange/gardening.stackexchange.com_en_all_2026-02.zim",
|
||||||
"size_mb": 923
|
"size_mb": 923
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cooking.stackexchange.com_en_all",
|
"id": "cooking.stackexchange.com_en_all",
|
||||||
"version": "2025-12",
|
"version": "2026-02",
|
||||||
"title": "Cooking Q&A",
|
"title": "Cooking Q&A",
|
||||||
"description": "Stack Exchange Q&A for cooking techniques, food safety, and recipes",
|
"description": "Stack Exchange Q&A for cooking techniques, food safety, and recipes",
|
||||||
"url": "https://download.kiwix.org/zim/stack_exchange/cooking.stackexchange.com_en_all_2025-12.zim",
|
"url": "https://download.kiwix.org/zim/stack_exchange/cooking.stackexchange.com_en_all_2026-02.zim",
|
||||||
"size_mb": 236
|
"size_mb": 236
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -485,18 +485,18 @@
|
||||||
"resources": [
|
"resources": [
|
||||||
{
|
{
|
||||||
"id": "freecodecamp_en_all",
|
"id": "freecodecamp_en_all",
|
||||||
"version": "2025-11",
|
"version": "2026-02",
|
||||||
"title": "freeCodeCamp",
|
"title": "freeCodeCamp",
|
||||||
"description": "Interactive programming tutorials - JavaScript, algorithms, and data structures",
|
"description": "Interactive programming tutorials - JavaScript, algorithms, and data structures",
|
||||||
"url": "https://download.kiwix.org/zim/freecodecamp/freecodecamp_en_all_2025-11.zim",
|
"url": "https://download.kiwix.org/zim/freecodecamp/freecodecamp_en_all_2026-02.zim",
|
||||||
"size_mb": 8
|
"size_mb": 8
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "devdocs_en_python",
|
"id": "devdocs_en_python",
|
||||||
"version": "2026-01",
|
"version": "2026-02",
|
||||||
"title": "Python Documentation",
|
"title": "Python Documentation",
|
||||||
"description": "Complete Python language reference and tutorials",
|
"description": "Complete Python language reference and tutorials",
|
||||||
"url": "https://download.kiwix.org/zim/devdocs/devdocs_en_python_2026-01.zim",
|
"url": "https://download.kiwix.org/zim/devdocs/devdocs_en_python_2026-02.zim",
|
||||||
"size_mb": 4
|
"size_mb": 4
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -533,26 +533,26 @@
|
||||||
"resources": [
|
"resources": [
|
||||||
{
|
{
|
||||||
"id": "arduino.stackexchange.com_en_all",
|
"id": "arduino.stackexchange.com_en_all",
|
||||||
"version": "2025-12",
|
"version": "2026-02",
|
||||||
"title": "Arduino Q&A",
|
"title": "Arduino Q&A",
|
||||||
"description": "Stack Exchange Q&A for Arduino microcontroller projects",
|
"description": "Stack Exchange Q&A for Arduino microcontroller projects",
|
||||||
"url": "https://download.kiwix.org/zim/stack_exchange/arduino.stackexchange.com_en_all_2025-12.zim",
|
"url": "https://download.kiwix.org/zim/stack_exchange/arduino.stackexchange.com_en_all_2026-02.zim",
|
||||||
"size_mb": 247
|
"size_mb": 247
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "raspberrypi.stackexchange.com_en_all",
|
"id": "raspberrypi.stackexchange.com_en_all",
|
||||||
"version": "2025-12",
|
"version": "2026-02",
|
||||||
"title": "Raspberry Pi Q&A",
|
"title": "Raspberry Pi Q&A",
|
||||||
"description": "Stack Exchange Q&A for Raspberry Pi projects and troubleshooting",
|
"description": "Stack Exchange Q&A for Raspberry Pi projects and troubleshooting",
|
||||||
"url": "https://download.kiwix.org/zim/stack_exchange/raspberrypi.stackexchange.com_en_all_2025-12.zim",
|
"url": "https://download.kiwix.org/zim/stack_exchange/raspberrypi.stackexchange.com_en_all_2026-02.zim",
|
||||||
"size_mb": 285
|
"size_mb": 285
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "devdocs_en_node",
|
"id": "devdocs_en_node",
|
||||||
"version": "2026-01",
|
"version": "2026-02",
|
||||||
"title": "Node.js Documentation",
|
"title": "Node.js Documentation",
|
||||||
"description": "Node.js API reference and guides",
|
"description": "Node.js API reference and guides",
|
||||||
"url": "https://download.kiwix.org/zim/devdocs/devdocs_en_node_2026-01.zim",
|
"url": "https://download.kiwix.org/zim/devdocs/devdocs_en_node_2026-02.zim",
|
||||||
"size_mb": 1
|
"size_mb": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -581,18 +581,18 @@
|
||||||
"resources": [
|
"resources": [
|
||||||
{
|
{
|
||||||
"id": "electronics.stackexchange.com_en_all",
|
"id": "electronics.stackexchange.com_en_all",
|
||||||
"version": "2025-12",
|
"version": "2026-02",
|
||||||
"title": "Electronics Q&A",
|
"title": "Electronics Q&A",
|
||||||
"description": "Stack Exchange Q&A for circuit design, components, and electrical engineering",
|
"description": "Stack Exchange Q&A for circuit design, components, and electrical engineering",
|
||||||
"url": "https://download.kiwix.org/zim/stack_exchange/electronics.stackexchange.com_en_all_2025-12.zim",
|
"url": "https://download.kiwix.org/zim/stack_exchange/electronics.stackexchange.com_en_all_2026-02.zim",
|
||||||
"size_mb": 3800
|
"size_mb": 3800
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "robotics.stackexchange.com_en_all",
|
"id": "robotics.stackexchange.com_en_all",
|
||||||
"version": "2025-12",
|
"version": "2026-02",
|
||||||
"title": "Robotics Q&A",
|
"title": "Robotics Q&A",
|
||||||
"description": "Stack Exchange Q&A for robotics projects and automation",
|
"description": "Stack Exchange Q&A for robotics projects and automation",
|
||||||
"url": "https://download.kiwix.org/zim/stack_exchange/robotics.stackexchange.com_en_all_2025-12.zim",
|
"url": "https://download.kiwix.org/zim/stack_exchange/robotics.stackexchange.com_en_all_2026-02.zim",
|
||||||
"size_mb": 233
|
"size_mb": 233
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
"id": "all-nopic",
|
"id": "all-nopic",
|
||||||
"name": "Complete Wikipedia (No Images)",
|
"name": "Complete Wikipedia (No Images)",
|
||||||
"description": "All articles without images. Comprehensive offline reference.",
|
"description": "All articles without images. Comprehensive offline reference.",
|
||||||
"size_mb": 25000,
|
"size_mb": 49000,
|
||||||
"url": "https://download.kiwix.org/zim/wikipedia/wikipedia_en_all_nopic_2025-12.zim",
|
"url": "https://download.kiwix.org/zim/wikipedia/wikipedia_en_all_nopic_2025-12.zim",
|
||||||
"version": "2025-12"
|
"version": "2025-12"
|
||||||
},
|
},
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
"id": "all-maxi",
|
"id": "all-maxi",
|
||||||
"name": "Complete Wikipedia (Full)",
|
"name": "Complete Wikipedia (Full)",
|
||||||
"description": "The complete experience with all images and media.",
|
"description": "The complete experience with all images and media.",
|
||||||
"size_mb": 115000,
|
"size_mb": 118000,
|
||||||
"url": "https://download.kiwix.org/zim/wikipedia/wikipedia_en_all_maxi_2026-02.zim",
|
"url": "https://download.kiwix.org/zim/wikipedia/wikipedia_en_all_maxi_2026-02.zim",
|
||||||
"version": "2026-02"
|
"version": "2026-02"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,6 @@ GREEN='\033[1;32m' # Light Green.
|
||||||
WHIPTAIL_TITLE="Project N.O.M.A.D Installation"
|
WHIPTAIL_TITLE="Project N.O.M.A.D Installation"
|
||||||
NOMAD_DIR="/opt/project-nomad"
|
NOMAD_DIR="/opt/project-nomad"
|
||||||
MANAGEMENT_COMPOSE_FILE_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/management_compose.yaml"
|
MANAGEMENT_COMPOSE_FILE_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/management_compose.yaml"
|
||||||
SIDECAR_UPDATER_DOCKERFILE_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/sidecar-updater/Dockerfile"
|
|
||||||
SIDECAR_UPDATER_SCRIPT_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/sidecar-updater/update-watcher.sh"
|
|
||||||
START_SCRIPT_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/start_nomad.sh"
|
START_SCRIPT_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/start_nomad.sh"
|
||||||
STOP_SCRIPT_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/stop_nomad.sh"
|
STOP_SCRIPT_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/stop_nomad.sh"
|
||||||
UPDATE_SCRIPT_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/update_nomad.sh"
|
UPDATE_SCRIPT_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/update_nomad.sh"
|
||||||
|
|
@ -403,6 +401,15 @@ download_management_compose_file() {
|
||||||
local db_root_password=$(generateRandomPass)
|
local db_root_password=$(generateRandomPass)
|
||||||
local db_user_password=$(generateRandomPass)
|
local db_user_password=$(generateRandomPass)
|
||||||
|
|
||||||
|
# If MySQL data directory exists from a previous install attempt, remove it.
|
||||||
|
# MySQL only initializes credentials on first startup when the data dir is empty.
|
||||||
|
# If stale data exists, MySQL ignores the new passwords above and uses the old ones,
|
||||||
|
# causing "Access denied" errors when the admin container tries to connect.
|
||||||
|
if [[ -d "${NOMAD_DIR}/mysql" ]]; then
|
||||||
|
echo -e "${YELLOW}#${RESET} Removing existing MySQL data directory to ensure credentials match...\\n"
|
||||||
|
sudo rm -rf "${NOMAD_DIR}/mysql"
|
||||||
|
fi
|
||||||
|
|
||||||
# Inject dynamic env values into the compose file
|
# Inject dynamic env values into the compose file
|
||||||
echo -e "${YELLOW}#${RESET} Configuring docker-compose file env variables...\\n"
|
echo -e "${YELLOW}#${RESET} Configuring docker-compose file env variables...\\n"
|
||||||
sed -i "s|URL=replaceme|URL=http://${local_ip_address}:8080|g" "$compose_file_path"
|
sed -i "s|URL=replaceme|URL=http://${local_ip_address}:8080|g" "$compose_file_path"
|
||||||
|
|
@ -415,32 +422,6 @@ download_management_compose_file() {
|
||||||
echo -e "${GREEN}#${RESET} Docker compose file configured successfully.\\n"
|
echo -e "${GREEN}#${RESET} Docker compose file configured successfully.\\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
download_sidecar_files() {
|
|
||||||
# Create sidecar-updater directory if it doesn't exist
|
|
||||||
if [[ ! -d "${NOMAD_DIR}/sidecar-updater" ]]; then
|
|
||||||
sudo mkdir -p "${NOMAD_DIR}/sidecar-updater"
|
|
||||||
sudo chown "$(whoami):$(whoami)" "${NOMAD_DIR}/sidecar-updater"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local sidecar_dockerfile_path="${NOMAD_DIR}/sidecar-updater/Dockerfile"
|
|
||||||
local sidecar_script_path="${NOMAD_DIR}/sidecar-updater/update-watcher.sh"
|
|
||||||
|
|
||||||
echo -e "${YELLOW}#${RESET} Downloading sidecar updater Dockerfile...\\n"
|
|
||||||
if ! curl -fsSL "$SIDECAR_UPDATER_DOCKERFILE_URL" -o "$sidecar_dockerfile_path"; then
|
|
||||||
echo -e "${RED}#${RESET} Failed to download the sidecar updater Dockerfile. Please check the URL and try again."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo -e "${GREEN}#${RESET} Sidecar updater Dockerfile downloaded successfully to $sidecar_dockerfile_path.\\n"
|
|
||||||
|
|
||||||
echo -e "${YELLOW}#${RESET} Downloading sidecar updater script...\\n"
|
|
||||||
if ! curl -fsSL "$SIDECAR_UPDATER_SCRIPT_URL" -o "$sidecar_script_path"; then
|
|
||||||
echo -e "${RED}#${RESET} Failed to download the sidecar updater script. Please check the URL and try again."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
chmod +x "$sidecar_script_path"
|
|
||||||
echo -e "${GREEN}#${RESET} Sidecar updater script downloaded successfully to $sidecar_script_path.\\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
download_helper_scripts() {
|
download_helper_scripts() {
|
||||||
local start_script_path="${NOMAD_DIR}/start_nomad.sh"
|
local start_script_path="${NOMAD_DIR}/start_nomad.sh"
|
||||||
local stop_script_path="${NOMAD_DIR}/stop_nomad.sh"
|
local stop_script_path="${NOMAD_DIR}/stop_nomad.sh"
|
||||||
|
|
@ -510,7 +491,7 @@ verify_gpu_setup() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if Docker has NVIDIA runtime
|
# Check if Docker has NVIDIA runtime
|
||||||
if docker info 2>/dev/null | grep -q \"nvidia\"; then
|
if docker info 2>/dev/null | grep -q "nvidia"; then
|
||||||
echo -e "${GREEN}✓${RESET} Docker NVIDIA runtime configured\\n"
|
echo -e "${GREEN}✓${RESET} Docker NVIDIA runtime configured\\n"
|
||||||
else
|
else
|
||||||
echo -e "${YELLOW}○${RESET} Docker NVIDIA runtime not detected\\n"
|
echo -e "${YELLOW}○${RESET} Docker NVIDIA runtime not detected\\n"
|
||||||
|
|
@ -526,11 +507,11 @@ verify_gpu_setup() {
|
||||||
echo -e "${YELLOW}===========================================${RESET}\\n"
|
echo -e "${YELLOW}===========================================${RESET}\\n"
|
||||||
|
|
||||||
# Summary
|
# Summary
|
||||||
if command -v nvidia-smi &> /dev/null && docker info 2>/dev/null | grep -q \"nvidia\"; then
|
if command -v nvidia-smi &> /dev/null && docker info 2>/dev/null | grep -q "nvidia"; then
|
||||||
echo -e "${GREEN}#${RESET} GPU acceleration is properly configured! The AI Assistant will use your GPU.\\n"
|
echo -e "${GREEN}#${RESET} GPU acceleration is properly configured! The AI Assistant will use your GPU.\\n"
|
||||||
else
|
else
|
||||||
echo -e "${YELLOW}#${RESET} GPU acceleration not detected. The AI Assistant will run in CPU-only mode.\\n"
|
echo -e "${YELLOW}#${RESET} GPU acceleration not detected. The AI Assistant will run in CPU-only mode.\\n"
|
||||||
if command -v nvidia-smi &> /dev/null && ! docker info 2>/dev/null | grep -q \"nvidia\"; then
|
if command -v nvidia-smi &> /dev/null && ! docker info 2>/dev/null | grep -q "nvidia"; then
|
||||||
echo -e "${YELLOW}#${RESET} Tip: Your GPU is detected but Docker runtime is not configured.\\n"
|
echo -e "${YELLOW}#${RESET} Tip: Your GPU is detected but Docker runtime is not configured.\\n"
|
||||||
echo -e "${YELLOW}#${RESET} Try restarting Docker: ${WHITE_R}sudo systemctl restart docker${RESET}\\n"
|
echo -e "${YELLOW}#${RESET} Try restarting Docker: ${WHITE_R}sudo systemctl restart docker${RESET}\\n"
|
||||||
fi
|
fi
|
||||||
|
|
@ -566,7 +547,6 @@ check_docker_compose
|
||||||
setup_nvidia_container_toolkit
|
setup_nvidia_container_toolkit
|
||||||
get_local_ip
|
get_local_ip
|
||||||
create_nomad_directory
|
create_nomad_directory
|
||||||
download_sidecar_files
|
|
||||||
download_helper_scripts
|
download_helper_scripts
|
||||||
download_management_compose_file
|
download_management_compose_file
|
||||||
start_management_containers
|
start_management_containers
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ services:
|
||||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 10
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
container_name: nomad_redis
|
container_name: nomad_redis
|
||||||
|
|
@ -117,4 +117,4 @@ services:
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
nomad-update-shared:
|
nomad-update-shared:
|
||||||
driver: local
|
driver: local
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "project-nomad",
|
"name": "project-nomad",
|
||||||
"version": "1.30.2",
|
"version": "1.30.3",
|
||||||
"description": "\"",
|
"description": "\"",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user