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!
|
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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
## 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
|
## 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,7 +507,7 @@ 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"
|
||||||
|
|
@ -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-rc.2",
|
||||||
"description": "\"",
|
"description": "\"",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user