mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-28 03:29:25 +01:00
Compare commits
17 Commits
main
...
v1.30.3-rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99aeff6df1 | ||
|
|
caa915fc13 | ||
|
|
6a7c9f3736 | ||
|
|
551fca90fb | ||
|
|
10397be914 | ||
|
|
149967f48b | ||
|
|
b2c9252342 | ||
|
|
e7ae6bba8e | ||
|
|
9480b2ec9f | ||
|
|
1e66d3c2e4 | ||
|
|
22126835e0 | ||
|
|
1ce4347ace | ||
|
|
96e3a0cecd | ||
|
|
7b482d6123 | ||
|
|
f7b715ad56 | ||
|
|
a68d6ab593 | ||
|
|
09c12e7574 |
|
|
@ -28,6 +28,8 @@ sudo apt-get update && sudo apt-get install -y curl && curl -fsSL https://raw.gi
|
|||
|
||||
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
|
||||
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export default class SystemController {
|
|||
if (result.success) {
|
||||
response.send({ success: true, message: result.message });
|
||||
} 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 { createHash } from 'crypto'
|
||||
import logger from '@adonisjs/core/services/logger'
|
||||
|
|
@ -63,6 +63,10 @@ export class DownloadModelJob {
|
|||
logger.error(
|
||||
`[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}`)
|
||||
}
|
||||
|
||||
|
|
@ -85,6 +89,15 @@ export class DownloadModelJob {
|
|||
const queue = queueService.getQueue(this.queue)
|
||||
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 {
|
||||
const job = await queue.add(this.key, params, {
|
||||
jobId,
|
||||
|
|
@ -104,9 +117,9 @@ export class DownloadModelJob {
|
|||
}
|
||||
} catch (error) {
|
||||
if (error.message.includes('job already exists')) {
|
||||
const existing = await queue.getJob(jobId)
|
||||
const active = await queue.getJob(jobId)
|
||||
return {
|
||||
job: existing,
|
||||
job: active,
|
||||
created: false,
|
||||
message: `Job already exists for model ${params.modelName}`,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -571,10 +571,10 @@ export class BenchmarkService {
|
|||
*/
|
||||
private _normalizeScore(value: number, reference: number): number {
|
||||
if (value <= 0) return 0
|
||||
// Log scale: score = 50 * (1 + log2(value/reference))
|
||||
// This gives 50 at reference value, scales logarithmically
|
||||
// Log scale with widened range: dividing log2 by 3 prevents scores from
|
||||
// clamping to 0% for below-average hardware. Gives 50% at reference value.
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -583,9 +583,9 @@ export class BenchmarkService {
|
|||
*/
|
||||
private _normalizeScoreInverse(value: number, reference: number): number {
|
||||
if (value <= 0) return 1
|
||||
// Inverse: lower values = higher scores
|
||||
// Inverse: lower values = higher scores, with widened log range
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -619,6 +619,7 @@ export class BenchmarkService {
|
|||
const eventsMatch = output.match(/events per second:\s*([\d.]+)/i)
|
||||
const totalTimeMatch = output.match(/total time:\s*([\d.]+)s/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 {
|
||||
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.
|
||||
**/
|
||||
const WIKIPEDIA_ZIM_URL =
|
||||
'https://github.com/Crosstalk-Solutions/project-nomad/raw/refs/heads/main/install/wikipedia_en_100_mini_2025-06.zim'
|
||||
const filename = '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_2026-01.zim'
|
||||
const filepath = join(process.cwd(), ZIM_STORAGE_PATH, filename)
|
||||
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
|
||||
* @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 {
|
||||
await this._ensureDependencies()
|
||||
if (!this.ollama) {
|
||||
|
|
@ -86,11 +86,21 @@ export class OllamaService {
|
|||
logger.info(`[OllamaService] Model "${model}" downloaded successfully.`)
|
||||
return { success: true, message: 'Model downloaded successfully.' }
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(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
|
||||
}
|
||||
|
||||
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) {
|
||||
transmit.broadcast(BROADCAST_CHANNELS.OLLAMA_MODEL_DOWNLOAD, {
|
||||
model,
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export default class ServiceSeeder extends BaseSeeder {
|
|||
display_order: 3,
|
||||
description: 'Local AI chat that runs entirely on your hardware - no internet required',
|
||||
icon: 'IconWand',
|
||||
container_image: 'ollama/ollama:0.15.2',
|
||||
container_image: 'ollama/ollama:0.18.1',
|
||||
source_repo: 'https://github.com/ollama/ollama',
|
||||
container_command: 'serve',
|
||||
container_config: JSON.stringify({
|
||||
|
|
@ -94,7 +94,7 @@ export default class ServiceSeeder extends BaseSeeder {
|
|||
display_order: 11,
|
||||
description: 'Swiss Army knife for data encoding, encryption, and analysis',
|
||||
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',
|
||||
container_command: null,
|
||||
container_config: JSON.stringify({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,25 @@
|
|||
# Release Notes
|
||||
|
||||
## Unreleased
|
||||
|
||||
### 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
|
||||
|
||||
### Features
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import useOllamaModelDownloads from '~/hooks/useOllamaModelDownloads'
|
||||
import HorizontalBarChart from './HorizontalBarChart'
|
||||
import StyledSectionHeader from './StyledSectionHeader'
|
||||
import { IconAlertTriangle } from '@tabler/icons-react'
|
||||
|
||||
interface ActiveModelDownloadsProps {
|
||||
withHeader?: boolean
|
||||
|
|
@ -17,19 +18,31 @@ const ActiveModelDownloads = ({ withHeader = false }: ActiveModelDownloadsProps)
|
|||
downloads.map((download) => (
|
||||
<div
|
||||
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
|
||||
items={[
|
||||
{
|
||||
label: download.model,
|
||||
value: download.percent,
|
||||
total: '100%',
|
||||
used: `${download.percent.toFixed(1)}%`,
|
||||
type: 'ollama-model',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{download.error ? (
|
||||
<div className="flex items-start gap-3">
|
||||
<IconAlertTriangle className="text-red-500 flex-shrink-0 mt-0.5" size={20} />
|
||||
<div>
|
||||
<p className="font-medium text-text-primary">{download.model}</p>
|
||||
<p className="text-sm text-red-600 mt-1">{download.error}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<HorizontalBarChart
|
||||
items={[
|
||||
{
|
||||
label: download.model,
|
||||
value: download.percent,
|
||||
total: '100%',
|
||||
used: `${download.percent.toFixed(1)}%`,
|
||||
type: 'ollama-model',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -78,11 +78,11 @@ const StyledSidebar: React.FC<StyledSidebarProps> = ({ title, items }) => {
|
|||
</li>
|
||||
</ul>
|
||||
</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>
|
||||
<button
|
||||
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" />
|
||||
Debug Info
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export default function ThemeToggle({ compact = false }: ThemeToggleProps) {
|
|||
<button
|
||||
onClick={toggleTheme}
|
||||
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'}
|
||||
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 classNames from 'classnames'
|
||||
import DynamicIcon, { DynamicIconName } from './DynamicIcon'
|
||||
import StyledButton from './StyledButton'
|
||||
|
||||
interface TierSelectionModalProps {
|
||||
isOpen: boolean
|
||||
|
|
@ -213,18 +214,14 @@ const TierSelectionModal: React.FC<TierSelectionModalProps> = ({
|
|||
|
||||
{/* Footer */}
|
||||
<div className="bg-surface-secondary px-6 py-4 flex justify-end gap-3">
|
||||
<button
|
||||
<StyledButton
|
||||
variant='primary'
|
||||
size='lg'
|
||||
onClick={handleSubmit}
|
||||
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
|
||||
</button>
|
||||
</StyledButton>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export type OllamaModelDownload = {
|
|||
model: string
|
||||
percent: number
|
||||
timestamp: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export default function useOllamaModelDownloads() {
|
||||
|
|
@ -17,7 +18,19 @@ export default function useOllamaModelDownloads() {
|
|||
setDownloads((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
|
||||
updated.set(data.model, data)
|
||||
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 { ServiceSlim } from '../../types/services'
|
||||
import { FileEntry } from '../../types/files'
|
||||
|
|
@ -25,13 +25,19 @@ class API {
|
|||
}
|
||||
|
||||
async affectService(service_name: string, action: 'start' | 'stop' | 'restart') {
|
||||
return catchInternal(async () => {
|
||||
try {
|
||||
const response = await this.client.post<{ success: boolean; message: string }>(
|
||||
'/system/services/affect',
|
||||
{ service_name, action }
|
||||
)
|
||||
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) {
|
||||
|
|
@ -192,13 +198,19 @@ class API {
|
|||
}
|
||||
|
||||
async forceReinstallService(service_name: string) {
|
||||
return catchInternal(async () => {
|
||||
try {
|
||||
const response = await this.client.post<{ success: boolean; message: string }>(
|
||||
`/system/services/force-reinstall`,
|
||||
{ service_name }
|
||||
)
|
||||
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) {
|
||||
|
|
@ -459,13 +471,19 @@ class API {
|
|||
}
|
||||
|
||||
async installService(service_name: string) {
|
||||
return catchInternal(async () => {
|
||||
try {
|
||||
const response = await this.client.post<{ success: boolean; message: string }>(
|
||||
'/system/services/install',
|
||||
{ service_name }
|
||||
)
|
||||
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() {
|
||||
|
|
@ -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() {
|
||||
return catchInternal(async () => {
|
||||
return await this.client.get<ListZimFilesResponse>('/zim/list')
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export default function SupportPage() {
|
|||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-semibold mb-3">Need Help With Your Home Network?</h2>
|
||||
<a
|
||||
href="https://roguesupport.com"
|
||||
href="https://rogue.support"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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.
|
||||
</p>
|
||||
<a
|
||||
href="https://roguesupport.com"
|
||||
href="https://rogue.support"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 text-blue-600 hover:underline font-medium"
|
||||
>
|
||||
Visit RogueSupport.com
|
||||
Visit Rogue.Support
|
||||
<IconExternalLink size={16} />
|
||||
</a>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"spec_version": "2026-02-11",
|
||||
"spec_version": "2026-03-15",
|
||||
"categories": [
|
||||
{
|
||||
"name": "Medicine",
|
||||
|
|
@ -113,10 +113,10 @@
|
|||
"resources": [
|
||||
{
|
||||
"id": "canadian_prepper_winterprepping_en",
|
||||
"version": "2025-11",
|
||||
"version": "2026-02",
|
||||
"title": "Canadian Prepper: Winter Prepping",
|
||||
"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
|
||||
},
|
||||
{
|
||||
|
|
@ -137,18 +137,18 @@
|
|||
"resources": [
|
||||
{
|
||||
"id": "canadian_prepper_bugoutconcepts_en",
|
||||
"version": "2025-11",
|
||||
"version": "2026-02",
|
||||
"title": "Canadian Prepper: Bug Out Concepts",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"id": "urban-prepper_en_all",
|
||||
"version": "2025-11",
|
||||
"version": "2026-02",
|
||||
"title": "Urban Prepper",
|
||||
"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
|
||||
}
|
||||
]
|
||||
|
|
@ -194,10 +194,10 @@
|
|||
"resources": [
|
||||
{
|
||||
"id": "wikibooks_en_all_nopic",
|
||||
"version": "2025-10",
|
||||
"version": "2026-01",
|
||||
"title": "Wikibooks",
|
||||
"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
|
||||
}
|
||||
]
|
||||
|
|
@ -210,35 +210,35 @@
|
|||
"resources": [
|
||||
{
|
||||
"id": "ted_mul_ted-ed",
|
||||
"version": "2025-07",
|
||||
"version": "2026-01",
|
||||
"title": "TED-Ed",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"id": "wikiversity_en_all_maxi",
|
||||
"version": "2025-11",
|
||||
"version": "2026-02",
|
||||
"title": "Wikiversity",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"id": "libretexts.org_en_math",
|
||||
"version": "2025-01",
|
||||
"version": "2026-01",
|
||||
"title": "LibreTexts Mathematics",
|
||||
"description": "Open-source math textbooks from algebra to calculus",
|
||||
"url": "https://download.kiwix.org/zim/libretexts/libretexts.org_en_math_2025-01.zim",
|
||||
"size_mb": 831
|
||||
"url": "https://download.kiwix.org/zim/libretexts/libretexts.org_en_math_2026-01.zim",
|
||||
"size_mb": 792
|
||||
},
|
||||
{
|
||||
"id": "libretexts.org_en_phys",
|
||||
"version": "2025-01",
|
||||
"version": "2026-01",
|
||||
"title": "LibreTexts Physics",
|
||||
"description": "Physics courses and textbooks",
|
||||
"url": "https://download.kiwix.org/zim/libretexts/libretexts.org_en_phys_2025-01.zim",
|
||||
"size_mb": 560
|
||||
"url": "https://download.kiwix.org/zim/libretexts/libretexts.org_en_phys_2026-01.zim",
|
||||
"size_mb": 534
|
||||
},
|
||||
{
|
||||
"id": "libretexts.org_en_chem",
|
||||
|
|
@ -266,18 +266,18 @@
|
|||
"resources": [
|
||||
{
|
||||
"id": "wikibooks_en_all_maxi",
|
||||
"version": "2025-10",
|
||||
"version": "2026-01",
|
||||
"title": "Wikibooks (With Images)",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"id": "ted_mul_ted-conference",
|
||||
"version": "2025-08",
|
||||
"version": "2026-02",
|
||||
"title": "TED Conference",
|
||||
"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
|
||||
},
|
||||
{
|
||||
|
|
@ -290,11 +290,11 @@
|
|||
},
|
||||
{
|
||||
"id": "libretexts.org_en_geo",
|
||||
"version": "2025-01",
|
||||
"version": "2026-01",
|
||||
"title": "LibreTexts Geosciences",
|
||||
"description": "Earth science, geology, and environmental studies",
|
||||
"url": "https://download.kiwix.org/zim/libretexts/libretexts.org_en_geo_2025-01.zim",
|
||||
"size_mb": 1190
|
||||
"url": "https://download.kiwix.org/zim/libretexts/libretexts.org_en_geo_2026-01.zim",
|
||||
"size_mb": 1127
|
||||
},
|
||||
{
|
||||
"id": "libretexts.org_en_eng",
|
||||
|
|
@ -306,11 +306,11 @@
|
|||
},
|
||||
{
|
||||
"id": "libretexts.org_en_biz",
|
||||
"version": "2025-01",
|
||||
"version": "2026-01",
|
||||
"title": "LibreTexts Business",
|
||||
"description": "Business, economics, and management textbooks",
|
||||
"url": "https://download.kiwix.org/zim/libretexts/libretexts.org_en_biz_2025-01.zim",
|
||||
"size_mb": 840
|
||||
"url": "https://download.kiwix.org/zim/libretexts/libretexts.org_en_biz_2026-01.zim",
|
||||
"size_mb": 801
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -331,18 +331,18 @@
|
|||
"resources": [
|
||||
{
|
||||
"id": "woodworking.stackexchange.com_en_all",
|
||||
"version": "2025-12",
|
||||
"version": "2026-02",
|
||||
"title": "Woodworking Q&A",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"id": "mechanics.stackexchange.com_en_all",
|
||||
"version": "2025-12",
|
||||
"version": "2026-02",
|
||||
"title": "Motor Vehicle Maintenance Q&A",
|
||||
"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
|
||||
}
|
||||
]
|
||||
|
|
@ -355,10 +355,10 @@
|
|||
"resources": [
|
||||
{
|
||||
"id": "diy.stackexchange.com_en_all",
|
||||
"version": "2025-12",
|
||||
"version": "2026-02",
|
||||
"title": "DIY & Home Improvement Q&A",
|
||||
"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
|
||||
}
|
||||
]
|
||||
|
|
@ -375,7 +375,7 @@
|
|||
"title": "iFixit Repair Guides",
|
||||
"description": "Step-by-step repair guides for electronics, appliances, and vehicles",
|
||||
"url": "https://download.kiwix.org/zim/ifixit/ifixit_en_all_2025-12.zim",
|
||||
"size_mb": 3570
|
||||
"size_mb": 3380
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -396,18 +396,18 @@
|
|||
"resources": [
|
||||
{
|
||||
"id": "foss.cooking_en_all",
|
||||
"version": "2025-11",
|
||||
"version": "2026-02",
|
||||
"title": "FOSS Cooking",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"id": "based.cooking_en_all",
|
||||
"version": "2025-11",
|
||||
"version": "2026-02",
|
||||
"title": "Based.Cooking",
|
||||
"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
|
||||
}
|
||||
]
|
||||
|
|
@ -420,18 +420,18 @@
|
|||
"resources": [
|
||||
{
|
||||
"id": "gardening.stackexchange.com_en_all",
|
||||
"version": "2025-12",
|
||||
"version": "2026-02",
|
||||
"title": "Gardening Q&A",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"id": "cooking.stackexchange.com_en_all",
|
||||
"version": "2025-12",
|
||||
"version": "2026-02",
|
||||
"title": "Cooking Q&A",
|
||||
"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
|
||||
},
|
||||
{
|
||||
|
|
@ -485,18 +485,18 @@
|
|||
"resources": [
|
||||
{
|
||||
"id": "freecodecamp_en_all",
|
||||
"version": "2025-11",
|
||||
"version": "2026-02",
|
||||
"title": "freeCodeCamp",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"id": "devdocs_en_python",
|
||||
"version": "2026-01",
|
||||
"version": "2026-02",
|
||||
"title": "Python Documentation",
|
||||
"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
|
||||
},
|
||||
{
|
||||
|
|
@ -533,26 +533,26 @@
|
|||
"resources": [
|
||||
{
|
||||
"id": "arduino.stackexchange.com_en_all",
|
||||
"version": "2025-12",
|
||||
"version": "2026-02",
|
||||
"title": "Arduino Q&A",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"id": "raspberrypi.stackexchange.com_en_all",
|
||||
"version": "2025-12",
|
||||
"version": "2026-02",
|
||||
"title": "Raspberry Pi Q&A",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"id": "devdocs_en_node",
|
||||
"version": "2026-01",
|
||||
"version": "2026-02",
|
||||
"title": "Node.js Documentation",
|
||||
"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
|
||||
},
|
||||
{
|
||||
|
|
@ -581,18 +581,18 @@
|
|||
"resources": [
|
||||
{
|
||||
"id": "electronics.stackexchange.com_en_all",
|
||||
"version": "2025-12",
|
||||
"version": "2026-02",
|
||||
"title": "Electronics Q&A",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"id": "robotics.stackexchange.com_en_all",
|
||||
"version": "2025-12",
|
||||
"version": "2026-02",
|
||||
"title": "Robotics Q&A",
|
||||
"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
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
"id": "all-nopic",
|
||||
"name": "Complete Wikipedia (No Images)",
|
||||
"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",
|
||||
"version": "2025-12"
|
||||
},
|
||||
|
|
@ -45,7 +45,7 @@
|
|||
"id": "all-maxi",
|
||||
"name": "Complete Wikipedia (Full)",
|
||||
"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",
|
||||
"version": "2026-02"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,8 +31,6 @@ GREEN='\033[1;32m' # Light Green.
|
|||
WHIPTAIL_TITLE="Project N.O.M.A.D 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"
|
||||
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"
|
||||
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"
|
||||
|
|
@ -403,6 +401,15 @@ download_management_compose_file() {
|
|||
local db_root_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
|
||||
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"
|
||||
|
|
@ -415,32 +422,6 @@ download_management_compose_file() {
|
|||
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() {
|
||||
local start_script_path="${NOMAD_DIR}/start_nomad.sh"
|
||||
local stop_script_path="${NOMAD_DIR}/stop_nomad.sh"
|
||||
|
|
@ -510,7 +491,7 @@ verify_gpu_setup() {
|
|||
fi
|
||||
|
||||
# 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"
|
||||
else
|
||||
echo -e "${YELLOW}○${RESET} Docker NVIDIA runtime not detected\\n"
|
||||
|
|
@ -526,7 +507,7 @@ verify_gpu_setup() {
|
|||
echo -e "${YELLOW}===========================================${RESET}\\n"
|
||||
|
||||
# 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"
|
||||
else
|
||||
echo -e "${YELLOW}#${RESET} GPU acceleration not detected. The AI Assistant will run in CPU-only mode.\\n"
|
||||
|
|
@ -566,7 +547,6 @@ check_docker_compose
|
|||
setup_nvidia_container_toolkit
|
||||
get_local_ip
|
||||
create_nomad_directory
|
||||
download_sidecar_files
|
||||
download_helper_scripts
|
||||
download_management_compose_file
|
||||
start_management_containers
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ services:
|
|||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
retries: 10
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: nomad_redis
|
||||
|
|
@ -117,4 +117,4 @@ services:
|
|||
|
||||
volumes:
|
||||
nomad-update-shared:
|
||||
driver: local
|
||||
driver: local
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "project-nomad",
|
||||
"version": "1.30.2",
|
||||
"version": "1.30.3-rc.2",
|
||||
"description": "\"",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user