Merge branch 'master' into feature/easy-setup-wizard-ux

This commit is contained in:
Jake Turner 2026-01-19 10:21:57 -08:00 committed by GitHub
commit e0dcd129e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1790 additions and 93 deletions

195
admin/docs/faq.md Normal file
View File

@ -0,0 +1,195 @@
# Frequently Asked Questions
## General Questions
### What is N.O.M.A.D.?
N.O.M.A.D. (Node for Offline Media, Archives, and Data) is a personal server that gives you access to knowledge, education, and AI assistance without requiring an internet connection. It runs on your own hardware, keeping your data private and accessible anytime.
### Do I need internet to use N.O.M.A.D.?
No — that's the whole point. Once your content is downloaded, everything works offline. You only need internet to:
- Download new content
- Update the software
- Sync the latest versions of Wikipedia, maps, etc.
### What hardware do I need?
N.O.M.A.D. is designed for capable hardware, especially if you want to use the AI features. Recommended:
- Modern multi-core CPU
- 16GB+ RAM (32GB+ for best AI performance)
- SSD storage (size depends on content — 500GB minimum, 2TB+ recommended)
- GPU recommended for faster AI responses
### How much storage do I need?
It depends on what you download:
- Full Wikipedia: ~95GB
- Khan Academy courses: ~50GB
- Medical references: ~500MB
- US state maps: ~2-3GB each
- AI models: 10-40GB depending on model
Start with essentials and add more as needed.
---
## Content Questions
### How do I add more Wikipedia content?
1. Go to **Settings** (hamburger menu → Settings)
2. Click **ZIM Manager**
3. Browse available content
4. Click Download on items you want
### How do I add more educational courses?
1. Open **Kolibri**
2. Sign in as an admin
3. Go to **Device → Channels**
4. Browse and import available channels
### How current is the content?
Content is as current as when it was last downloaded. Wikipedia snapshots are typically updated monthly. Check the file names or descriptions for dates.
### Can I add my own files?
Currently, N.O.M.A.D. uses standard content formats (ZIM files for Kiwix, Kolibri channels for education). Custom content support may be added in future versions.
---
## Troubleshooting
### A feature isn't loading or shows a blank page
**Try these steps:**
1. Wait 30 seconds — some features take time to start
2. Refresh the page (Ctrl+R or Cmd+R)
3. Go back to the Command Center and try again
4. Check Settings → System to see if the service is running
5. Try restarting the service (Stop, then Start in Apps manager)
### Maps show a gray/blank area
The Maps feature requires downloaded map data. If you see a blank area:
1. Go to **Settings → Maps Manager**
2. Download map regions for your area
3. Wait for downloads to complete
4. Return to Maps and refresh
### AI responses are slow
Local AI requires significant computing power. To improve speed:
- Close other applications on the server
- Ensure adequate cooling (overheating causes throttling)
- Consider using a smaller/faster AI model if available
- Add a GPU if your hardware supports it
### "Service unavailable" or connection errors
The service might still be starting up. Wait 1-2 minutes and try again.
If the problem persists:
1. Go to **Settings → Apps**
2. Find the problematic service
3. Click **Restart**
4. Wait 30 seconds, then try again
### Downloads are stuck or failing
1. Check your internet connection
2. Go to **Settings** and check available storage
3. If storage is full, delete unused content
4. Cancel the stuck download and try again
### The server won't start
If you can't access the Command Center at all:
1. Verify the server hardware is powered on
2. Check network connectivity
3. Try accessing directly via the server's IP address
4. Check server logs if you have console access
### I forgot my Kolibri password
Kolibri passwords are managed separately:
1. If you're an admin, you can reset user passwords in Kolibri's user management
2. If you forgot the admin password, you may need to reset it via command line (contact your administrator)
---
## Updates and Maintenance
### How do I update N.O.M.A.D.?
1. Go to **Settings → Check for Updates**
2. If an update is available, click to install
3. The system will download updates and restart automatically
4. This typically takes 2-5 minutes
### Should I update regularly?
Yes, while you have internet access. Updates include:
- Bug fixes
- New features
- Security improvements
- Performance enhancements
### How do I update content (Wikipedia, etc.)?
Content updates are separate from software updates:
1. Go to **Settings → ZIM Manager**
2. Check for newer versions of your installed content
3. Download updated versions as needed
Tip: New Wikipedia snapshots are released approximately monthly.
### What happens if an update fails?
The system is designed to recover gracefully. If an update fails:
1. The previous version should continue working
2. Try the update again later
3. Check Settings → System for error messages
### Command-Line Maintenance
For advanced troubleshooting or when you can't access the web interface, N.O.M.A.D. includes helper scripts in `/opt/project-nomad`:
**Start all services:**
```bash
sudo bash /opt/project-nomad/start_nomad.sh
```
**Stop all services:**
```bash
sudo bash /opt/project-nomad/stop_nomad.sh
```
**Update Command Center:**
```bash
sudo bash /opt/project-nomad/update_nomad.sh
```
*Note: This updates the Command Center only, not individual apps. Update apps through the web interface.*
**Uninstall N.O.M.A.D.:**
```bash
curl -fsSL https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/master/install/uninstall_nomad.sh -o uninstall_nomad.sh
sudo bash uninstall_nomad.sh
```
*Warning: This cannot be undone. All data will be deleted.*
---
## Privacy and Security
### Is my data private?
Yes. N.O.M.A.D. runs entirely on your hardware. Your searches, AI conversations, and usage data never leave your server.
### Can others access my server?
By default, N.O.M.A.D. is accessible on your local network. Anyone on the same network can access it. For public networks, consider additional security measures.
### Does the AI send data anywhere?
No. The AI runs completely locally. Your conversations are not sent to any external service.
---
## Getting More Help
### The AI can help
Try asking Open WebUI for help. The local AI can answer questions about many topics, including technical troubleshooting.
### Check the documentation
You're in the docs now. Use the menu to find specific topics.
### Release Notes
See what's changed in each version: **[Release Notes](/docs/release-notes)**

View File

@ -0,0 +1,238 @@
# Getting Started with N.O.M.A.D.
This guide will help you install and set up your N.O.M.A.D. server.
---
## Installation
### System Requirements
N.O.M.A.D. runs on any **Debian-based Linux** system (Ubuntu recommended). The installation is terminal-based, and everything is accessed through a web browser — no desktop environment needed.
**Minimum Specs** (Command Center only):
- 2 GHz dual-core processor
- 4 GB RAM
- 5 GB free storage
- Internet connection (for initial install)
**Recommended Specs** (with AI features):
- AMD Ryzen 7 / Intel Core i7 or better
- 32 GB RAM
- NVIDIA RTX 3060 or better (more VRAM = larger AI models)
- 250 GB+ free storage (SSD preferred)
The Command Center itself is lightweight — your hardware requirements depend on which tools and content you choose to install.
### Install N.O.M.A.D.
Open a terminal and run these two commands:
```bash
curl -fsSL https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/master/install/install_nomad.sh -o install_nomad.sh
```
```bash
sudo bash install_nomad.sh
```
That's it. Once the install finishes, open a browser and go to:
- **Same machine:** `http://localhost:8080`
- **Other devices on your network:** `http://YOUR_SERVER_IP:8080`
### About Internet & Privacy
N.O.M.A.D. is designed for offline use. Internet is only needed:
- During initial installation
- When downloading additional content
There is **zero telemetry** — your data stays on your device.
### About Security
N.O.M.A.D. has no built-in authentication — it's designed to be open and accessible. If you expose it on a network, consider using firewall rules to control which ports are accessible.
---
## After Installation
### 1. Run the Easy Setup Wizard
If this is your first time using N.O.M.A.D., the Easy Setup wizard will help you:
- Choose which apps to enable
- Download map regions for your area
- Select knowledge collections (Wikipedia, medical references, etc.)
**[Launch Easy Setup →](/easy-setup)**
The wizard walks you through four simple steps:
1. **Apps** — Choose additional tools like CyberChef or FlatNotes
2. **Maps** — Select geographic regions for offline maps
3. **ZIM Files** — Choose reference collections (Wikipedia, medical, survival guides)
4. **Review** — Confirm your selections and start downloading
### 2. Wait for Downloads to Complete
Depending on what you selected, downloads may take a while. You can:
- Monitor progress in the Settings area
- Continue using features that are already installed
- Leave your server running overnight for large downloads
### 3. Explore Your Content
Once downloads complete, you're ready to go. Your content works offline whenever you need it.
---
## Understanding Your Tools
### Kiwix — Your Offline Library
Kiwix stores compressed versions of websites and references that work without internet.
**What's included:**
- Full Wikipedia (millions of articles)
- Medical references and first aid guides
- How-to guides and survival information
- Classic books from Project Gutenberg
**How to use it:**
1. Click **Kiwix** from the Command Center
2. Choose a collection (like Wikipedia)
3. Search or browse just like the regular website
**[Open Kiwix →](/kiwix)**
---
### Kolibri — Offline Education
Kolibri provides complete educational courses that work offline.
**What's included:**
- Khan Academy video courses
- Math, science, reading, and more
- Progress tracking for learners
- Works for all ages
**How to use it:**
1. Click **Kolibri** from the Command Center
2. Sign in or create a learner account
3. Browse courses and start learning
**Tip:** Kolibri supports multiple users. Create accounts for each family member to track individual progress.
**[Open Kolibri →](/kolibri)**
---
### Open WebUI — Your AI Assistant
Chat with a local AI that runs entirely on your server — no internet needed.
**What can it do:**
- Answer questions on any topic
- Explain complex concepts simply
- Help with writing and editing
- Brainstorm ideas
- Assist with problem-solving
**How to use it:**
1. Click **Open WebUI** from the Command Center
2. Type your question or request
3. The AI responds in conversational style
**Tip:** Be specific in your questions. Instead of "tell me about plants," try "what vegetables grow well in shade?"
**[Open AI Chat →](/openwebui)**
---
### Maps — Offline Navigation
View maps without internet. Download the regions you need before going offline.
**How to use it:**
1. Click **Maps** from the Command Center
2. Navigate by dragging and zooming
3. Search for locations using the search bar
**To add more map regions:**
1. Go to **Settings → Maps Manager**
2. Select the regions you need
3. Click Download
**Tip:** Download maps for areas you travel to frequently, plus neighboring regions just in case.
**[Open Maps →](/maps)**
---
## Managing Your Server
### Adding More Content
As your needs change, you can add more content anytime:
- **More apps:** Settings → Apps
- **More references:** Settings → ZIM Manager
- **More map regions:** Settings → Maps Manager
- **More educational content:** Through Kolibri's built-in content browser
### Keeping Things Updated
While you have internet, periodically check for updates:
1. Go to **Settings → Check for Updates**
2. If updates are available, click to install
3. Wait for the update to complete (your server will restart)
Content updates (Wikipedia, maps, etc.) can be managed separately from software updates.
### Monitoring System Health
Check on your server anytime:
1. Go to **Settings → System**
2. View CPU, memory, and storage usage
3. Check system uptime and status
---
## Tips for Best Results
### Before Going Offline
- **Update everything** — Run software and content updates
- **Download what you need** — Maps, references, educational content
- **Test it** — Make sure features work while you still have internet to troubleshoot
### Storage Management
Your server has limited storage. Prioritize:
- Content you'll actually use
- Critical references (medical, survival)
- Maps for your region
- Educational content matching your needs
Check storage usage in **Settings → System**.
### Getting Help
- **In-app docs:** You're reading them now
- **AI assistant:** Ask Open WebUI for help with almost anything
- **Release notes:** See what's new in each version
---
## Next Steps
You're ready to use N.O.M.A.D. Here are some things to try:
1. **Look something up** — Search for a topic in Kiwix
2. **Learn something** — Start a Khan Academy course in Kolibri
3. **Ask a question** — Chat with the AI in Open WebUI
4. **Explore maps** — Find your neighborhood in the Maps viewer
Enjoy your offline knowledge server!

View File

@ -1,71 +1,67 @@
# Lorem Ipsum Markdown Showcase # Welcome to Project N.O.M.A.D.
Your personal offline knowledge server is ready to use.
## What is N.O.M.A.D.?
**N.O.M.A.D.** stands for **Node for Offline Media, Archives, and Data**. It's your personal server for accessing knowledge, education, and AI assistance — even when you have no internet connection.
Think of it as having Wikipedia, Khan Academy, an AI assistant, and offline maps all in one place, running on hardware you control.
## What Can You Do?
### Browse Offline Knowledge
Access millions of Wikipedia articles, medical references, how-to guides, and ebooks — all stored locally on your server. No internet required.
**[Open Kiwix →](/kiwix)**
### Learn Something New
Khan Academy courses covering math, science, economics, and more. Complete with videos and exercises, all available offline.
**[Open Kolibri →](/kolibri)**
### Chat with AI
Ask questions, get explanations, brainstorm ideas, or get help with writing. Your local AI assistant works completely offline.
**[Open AI Chat →](/openwebui)**
### View Offline Maps
Navigate and explore maps without an internet connection. Download regions you need before going offline.
**[Open Maps →](/maps)**
--- ---
## Introduction ## Getting Started
This document serves as a comprehensive example of **Markdown's various formatting possibilities**, using the classic *Lorem Ipsum* text as its content. From basic text styling to lists, code blocks, and tables, you'll find a demonstration of common Markdown features here. **New to N.O.M.A.D.?** Use the Easy Setup wizard to configure your server and download content collections.
**[Run Easy Setup →](/easy-setup)**
Or explore the **[Getting Started Guide](/docs/getting-started)** for a walkthrough of all features.
--- ---
## Basic Text Formatting ## Quick Links
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. | I want to... | Go here |
|--------------|---------|
* This text is **bold**. | Download more content | [Install Apps](/apps) |
* This text is *italic*. | Add Wikipedia/reference content | [ZIM Manager](/settings/zim-manager) |
* This text is ***bold and italic***. | Download map regions | [Maps Manager](/settings/maps-manager) |
* This text is ~~struck through~~. | Check for updates | [System Update](/settings/updates) |
* You can also use `backticks` for `inline code`. | View system status | [Settings](/settings) |
--- ---
## Headers ## Keeping Your Server Updated
Markdown supports up to six levels of headers. N.O.M.A.D. works best when kept up to date while you have internet access. This ensures you have the latest:
- Software features and bug fixes
- Wikipedia and reference content
- Educational materials
- AI model improvements
# Header 1 When you go offline, you'll have everything you need — the last synced versions of all your content.
## Header 2
### Header 3
#### Header 4
##### Header 5
###### Header 6
--- **[Check for Updates →](/settings/updates)**
## Lists
### Unordered List
* Lorem ipsum dolor sit amet.
* Consectetur adipiscing elit.
* Sed do eiusmod tempor.
* Incididunt ut labore et dolore magna.
* Aliqua ut enim ad minim veniam.
### Ordered List
1. Quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
2. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
3. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
---
## Blockquotes
> "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
>
> — John Doe, *Lorem Ipsum Anthology*
---
## Code Blocks
```python
def fibonacci(n):
a, b = 0, 1
for i in range(n):
print(a, end=" ")
a, b = b, a + b
fibonacci(10)

190
admin/docs/use-cases.md Normal file
View File

@ -0,0 +1,190 @@
# What Can You Do With N.O.M.A.D.?
N.O.M.A.D. is designed to be your information lifeline when internet isn't available. Here's how different people use it.
---
## Emergency Preparedness
When disasters strike, internet and cell service often go down first. N.O.M.A.D. keeps critical information at your fingertips.
**What you can do:**
- Look up first aid and emergency medical procedures
- Access survival guides and emergency protocols
- Find information about water purification, food storage, shelter building
- Use offline maps to navigate when GPS services are degraded
- Research plant identification, weather patterns, radio frequencies
**Recommended content:**
- Medical Library ZIM collection
- Survival/Prepper reference guides
- Maps for your region and evacuation routes
- Wikipedia (searchable for almost any topic)
---
## Homeschooling and Education
Teach your children anywhere, with or without internet. Complete curriculum available offline.
**What you can do:**
- Access Khan Academy's full course library (math, science, reading, history)
- Track progress for multiple students
- Supplement with Wikipedia for research projects
- Use the AI as a patient tutor for any subject
- Access classic literature through Project Gutenberg
**Recommended content:**
- Khan Academy courses via Kolibri
- Wikipedia for Schools (curated for younger learners)
- Project Gutenberg (classic books)
- Educational ZIM collections
**Tip:** Create separate Kolibri accounts for each child to track their individual progress.
---
## Off-Grid Living
Living away from reliable internet doesn't mean living without information.
**What you can do:**
- Research DIY projects and repairs
- Look up gardening, animal husbandry, food preservation
- Access medical references for remote healthcare
- Learn new skills through educational videos
- Get AI help with planning and problem-solving
**Recommended content:**
- How-to and DIY reference collections
- Medical and first aid guides
- Agricultural and homesteading references
- Maps for your rural area
- Practical skills courses in Kolibri
---
## Remote Work Sites
Construction sites, research stations, ships, and remote facilities often lack reliable internet.
**What you can do:**
- Access technical references and documentation
- Use AI for writing assistance and analysis
- Look up regulations, standards, and procedures
- Provide educational resources for workers
- Maintain communication records with note-taking apps
**Recommended content:**
- Industry-specific technical references
- Relevant Wikipedia categories
- Maps of work areas
- Documentation and compliance guides
---
## Travel and Expeditions
International travel, cruises, camping trips — stay informed anywhere.
**What you can do:**
- Access maps without expensive roaming data
- Research destinations, history, and culture
- Translate concepts with AI assistance
- Identify plants, animals, and geological features
- Access travel health information
**Recommended content:**
- Maps for destination countries/regions
- Wikipedia in relevant languages
- Medical/health references
- Cultural and historical content
---
## Privacy-Conscious Users
Some people simply prefer to keep their searches and questions private.
**What you can do:**
- Search Wikipedia without being tracked
- Ask AI questions that stay on your own hardware
- Learn about sensitive topics privately
- Keep your intellectual curiosity to yourself
**How it works:**
- All data stays on your server
- No search history sent to companies
- No AI conversations leave your network
- You control your own information
---
## Medical Reference
When you can't reach a doctor, having reliable medical information can be critical.
**What you can access:**
- NHS Medicines A-Z (drug information and interactions)
- Medical Library (field medicine, emergency procedures)
- First aid guides
- Anatomy and physiology references
- Disease and symptom information
**Important:** Medical references are for information only. They don't replace professional medical care. In emergencies, always seek professional help when possible.
**Recommended content:**
- Medical Essentials ZIM collection
- NHS Medicines reference
- First aid and emergency medicine guides
---
## Academic Research
Students and researchers can work without depending on university networks.
**What you can do:**
- Access Wikipedia's extensive article database
- Use AI for research assistance and summarization
- Work on papers and projects offline
- Cross-reference multiple sources
- Take notes with built-in tools
**Recommended content:**
- Full Wikipedia
- Academic and educational references
- Subject-specific ZIM collections
- Note-taking apps (FlatNotes)
---
## Setting Up for Your Use Case
### Step 1: Identify Your Needs
What situations might you face without internet? What information would you need?
### Step 2: Prioritize Content
Storage is limited. Focus on:
1. Critical safety information (medical, emergency)
2. Content matching your primary use case
3. General reference (Wikipedia)
4. Nice-to-have additions
### Step 3: Download While You Can
Keep your server updated while you have internet. You never know when you'll need to go offline.
### Step 4: Practice
Try using N.O.M.A.D. before you need it. Familiarity with the tools makes them more useful in a crisis.
---
## Need Something Specific?
N.O.M.A.D. content is customizable. If you don't see what you need:
1. **Check ZIM Remote Explorer** — Thousands of ZIM files are available
2. **Browse Kolibri channels** — Educational content for many subjects
3. **Request features** — Let us know what content would help you
Your offline server, your content choices.

View File

@ -0,0 +1,88 @@
import { formatBytes } from '~/lib/util'
import DynamicIcon, { DynamicIconName } from './DynamicIcon'
import { CuratedCategory, CategoryTier } from '../../types/downloads'
import classNames from 'classnames'
import { IconChevronRight, IconCircleCheck } from '@tabler/icons-react'
export interface CategoryCardProps {
category: CuratedCategory
selectedTier?: CategoryTier | null
onClick?: (category: CuratedCategory) => void
}
const CategoryCard: React.FC<CategoryCardProps> = ({ category, selectedTier, onClick }) => {
// Calculate total size range across all tiers
const getTierTotalSize = (tier: CategoryTier, allTiers: CategoryTier[]): number => {
let total = tier.resources.reduce((acc, r) => acc + r.size_mb * 1024 * 1024, 0)
// Add included tier sizes recursively
if (tier.includesTier) {
const includedTier = allTiers.find(t => t.slug === tier.includesTier)
if (includedTier) {
total += getTierTotalSize(includedTier, allTiers)
}
}
return total
}
const minSize = getTierTotalSize(category.tiers[0], category.tiers)
const maxSize = getTierTotalSize(category.tiers[category.tiers.length - 1], category.tiers)
return (
<div
className={classNames(
'flex flex-col bg-desert-green rounded-lg p-6 text-white border shadow-sm hover:shadow-lg transition-shadow cursor-pointer h-80',
selectedTier ? 'border-lime-400 border-2' : 'border-desert-green'
)}
onClick={() => onClick?.(category)}
>
<div className="flex items-center mb-4">
<div className="flex justify-between w-full items-center">
<div className="flex items-center">
<DynamicIcon icon={category.icon as DynamicIconName} className="w-6 h-6 mr-2" />
<h3 className="text-lg font-semibold">{category.name}</h3>
</div>
{selectedTier ? (
<div className="flex items-center">
<IconCircleCheck className="w-5 h-5 text-lime-400" />
<span className="text-lime-400 text-sm ml-1">{selectedTier.name}</span>
</div>
) : (
<IconChevronRight className="w-5 h-5 text-white opacity-70" />
)}
</div>
</div>
<p className="text-gray-200 grow">{category.description}</p>
<div className="mt-4 pt-4 border-t border-white/20">
<p className="text-sm text-gray-300 mb-2">
{category.tiers.length} tiers available
</p>
<div className="flex flex-wrap gap-2">
{category.tiers.map((tier) => (
<span
key={tier.slug}
className={classNames(
'text-xs px-2 py-1 rounded',
tier.recommended
? 'bg-lime-500/30 text-lime-200'
: 'bg-white/10 text-gray-300',
selectedTier?.slug === tier.slug && 'ring-2 ring-lime-400'
)}
>
{tier.name}
{tier.recommended && ' *'}
</span>
))}
</div>
<p className="text-gray-300 text-xs mt-3">
Size: {formatBytes(minSize, 1)} - {formatBytes(maxSize, 1)}
</p>
</div>
</div>
)
}
export default CategoryCard

View File

@ -0,0 +1,122 @@
import classNames from '~/lib/classNames'
import { formatBytes } from '~/lib/util'
import { IconAlertTriangle, IconServer } from '@tabler/icons-react'
interface StorageProjectionBarProps {
totalSize: number // Total disk size in bytes
currentUsed: number // Currently used space in bytes
projectedAddition: number // Additional space that will be used in bytes
}
export default function StorageProjectionBar({
totalSize,
currentUsed,
projectedAddition,
}: StorageProjectionBarProps) {
const projectedTotal = currentUsed + projectedAddition
const currentPercent = (currentUsed / totalSize) * 100
const projectedPercent = (projectedAddition / totalSize) * 100
const projectedTotalPercent = (projectedTotal / totalSize) * 100
const remainingAfter = totalSize - projectedTotal
const willExceed = projectedTotal > totalSize
// Determine warning level based on projected total
const getProjectedColor = () => {
if (willExceed) return 'bg-desert-red'
if (projectedTotalPercent >= 90) return 'bg-desert-orange'
if (projectedTotalPercent >= 75) return 'bg-desert-tan'
return 'bg-desert-olive'
}
const getProjectedGlow = () => {
if (willExceed) return 'shadow-desert-red/50'
if (projectedTotalPercent >= 90) return 'shadow-desert-orange/50'
if (projectedTotalPercent >= 75) return 'shadow-desert-tan/50'
return 'shadow-desert-olive/50'
}
return (
<div className="bg-desert-stone-lighter/30 rounded-lg p-4 border border-desert-stone-light">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<IconServer size={20} className="text-desert-green" />
<span className="font-semibold text-desert-green">Storage</span>
</div>
<div className="text-sm text-desert-stone-dark font-mono">
{formatBytes(projectedTotal, 1)} / {formatBytes(totalSize, 1)}
{projectedAddition > 0 && (
<span className="text-desert-stone ml-2">
(+{formatBytes(projectedAddition, 1)} selected)
</span>
)}
</div>
</div>
{/* Progress bar */}
<div className="relative">
<div className="h-8 bg-desert-green-lighter/20 rounded-lg border border-desert-stone-light overflow-hidden">
{/* Current usage - darker/subdued */}
<div
className="absolute h-full bg-desert-stone transition-all duration-300"
style={{ width: `${Math.min(currentPercent, 100)}%` }}
/>
{/* Projected addition - highlighted */}
{projectedAddition > 0 && (
<div
className={classNames(
'absolute h-full transition-all duration-300 shadow-lg',
getProjectedColor(),
getProjectedGlow()
)}
style={{
left: `${Math.min(currentPercent, 100)}%`,
width: `${Math.min(projectedPercent, 100 - currentPercent)}%`,
}}
/>
)}
</div>
{/* Percentage label */}
<div
className={classNames(
'absolute top-1/2 -translate-y-1/2 font-bold text-sm',
projectedTotalPercent > 15
? 'left-3 text-desert-white drop-shadow-md'
: 'right-3 text-desert-green'
)}
>
{Math.round(projectedTotalPercent)}%
</div>
</div>
{/* Legend and warnings */}
<div className="flex items-center justify-between mt-3">
<div className="flex items-center gap-4 text-xs">
<div className="flex items-center gap-1.5">
<div className="w-3 h-3 rounded bg-desert-stone" />
<span className="text-desert-stone-dark">Current ({formatBytes(currentUsed, 1)})</span>
</div>
{projectedAddition > 0 && (
<div className="flex items-center gap-1.5">
<div className={classNames('w-3 h-3 rounded', getProjectedColor())} />
<span className="text-desert-stone-dark">
Selected (+{formatBytes(projectedAddition, 1)})
</span>
</div>
)}
</div>
{willExceed ? (
<div className="flex items-center gap-1.5 text-desert-red text-xs font-medium">
<IconAlertTriangle size={14} />
<span>Exceeds available space by {formatBytes(projectedTotal - totalSize, 1)}</span>
</div>
) : (
<div className="text-xs text-desert-stone">
{formatBytes(remainingAfter, 1)} will remain free
</div>
)}
</div>
</div>
)
}

View File

@ -0,0 +1,205 @@
import { Fragment } from 'react'
import { Dialog, Transition } from '@headlessui/react'
import { IconX, IconCheck, IconInfoCircle } from '@tabler/icons-react'
import { CuratedCategory, CategoryTier, CategoryResource } from '../../types/downloads'
import { formatBytes } from '~/lib/util'
import classNames from 'classnames'
import DynamicIcon, { DynamicIconName } from './DynamicIcon'
interface TierSelectionModalProps {
isOpen: boolean
onClose: () => void
category: CuratedCategory | null
selectedTierSlug?: string | null
onSelectTier: (category: CuratedCategory, tier: CategoryTier) => void
}
const TierSelectionModal: React.FC<TierSelectionModalProps> = ({
isOpen,
onClose,
category,
selectedTierSlug,
onSelectTier,
}) => {
if (!category) return null
// Get all resources for a tier (including inherited resources)
const getAllResourcesForTier = (tier: CategoryTier): CategoryResource[] => {
const resources = [...tier.resources]
if (tier.includesTier) {
const includedTier = category.tiers.find(t => t.slug === tier.includesTier)
if (includedTier) {
resources.unshift(...getAllResourcesForTier(includedTier))
}
}
return resources
}
const getTierTotalSize = (tier: CategoryTier): number => {
return getAllResourcesForTier(tier).reduce((acc, r) => acc + r.size_mb * 1024 * 1024, 0)
}
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-50" onClose={onClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black/50" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-4xl transform overflow-hidden rounded-lg bg-white shadow-xl transition-all">
{/* Header */}
<div className="bg-desert-green px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center">
<DynamicIcon
icon={category.icon as DynamicIconName}
className="w-8 h-8 text-white mr-3"
/>
<div>
<Dialog.Title className="text-xl font-semibold text-white">
{category.name}
</Dialog.Title>
<p className="text-sm text-gray-200">{category.description}</p>
</div>
</div>
<button
onClick={onClose}
className="text-white/70 hover:text-white transition-colors"
>
<IconX size={24} />
</button>
</div>
</div>
{/* Content */}
<div className="p-6">
<p className="text-gray-600 mb-6">
Select a tier based on your storage capacity and needs. Higher tiers include all content from lower tiers.
</p>
<div className="space-y-4">
{category.tiers.map((tier, index) => {
const allResources = getAllResourcesForTier(tier)
const totalSize = getTierTotalSize(tier)
const isSelected = selectedTierSlug === tier.slug
return (
<div
key={tier.slug}
onClick={() => onSelectTier(category, tier)}
className={classNames(
'border-2 rounded-lg p-5 cursor-pointer transition-all',
isSelected
? 'border-desert-green bg-desert-green/5 shadow-md'
: 'border-gray-200 hover:border-desert-green/50 hover:shadow-sm',
tier.recommended && !isSelected && 'border-lime-500/50'
)}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h3 className="text-lg font-semibold text-gray-900">
{tier.name}
</h3>
{tier.recommended && (
<span className="text-xs bg-lime-500 text-white px-2 py-0.5 rounded">
Recommended
</span>
)}
{tier.includesTier && (
<span className="text-xs text-gray-500">
(includes {category.tiers.find(t => t.slug === tier.includesTier)?.name})
</span>
)}
</div>
<p className="text-gray-600 text-sm mb-3">{tier.description}</p>
{/* Resources preview */}
<div className="bg-gray-50 rounded p-3">
<p className="text-xs text-gray-500 mb-2 font-medium">
{allResources.length} resources included:
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{allResources.map((resource, idx) => (
<div key={idx} className="flex items-start text-sm">
<IconCheck size={14} className="text-desert-green mr-1.5 mt-0.5 flex-shrink-0" />
<div>
<span className="text-gray-700">{resource.title}</span>
<span className="text-gray-400 text-xs ml-1">
({formatBytes(resource.size_mb * 1024 * 1024, 0)})
</span>
</div>
</div>
))}
</div>
</div>
</div>
<div className="ml-4 text-right flex-shrink-0">
<div className="text-lg font-semibold text-gray-900">
{formatBytes(totalSize, 1)}
</div>
<div className={classNames(
'w-6 h-6 rounded-full border-2 flex items-center justify-center mt-2 ml-auto',
isSelected
? 'border-desert-green bg-desert-green'
: 'border-gray-300'
)}>
{isSelected && <IconCheck size={16} className="text-white" />}
</div>
</div>
</div>
</div>
)
})}
</div>
{/* Info note */}
<div className="mt-6 flex items-start gap-2 text-sm text-gray-500 bg-blue-50 p-3 rounded">
<IconInfoCircle size={18} className="text-blue-500 flex-shrink-0 mt-0.5" />
<p>
You can change your selection at any time. Downloads will begin when you complete the setup wizard.
</p>
</div>
</div>
{/* Footer */}
<div className="bg-gray-50 px-6 py-4 flex justify-end gap-3">
<button
onClick={onClose}
className="px-4 py-2 text-gray-700 hover:text-gray-900 transition-colors"
>
Close
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
)
}
export default TierSelectionModal

View File

@ -1,17 +1,22 @@
import { Head, router } from '@inertiajs/react' import { Head, router } from '@inertiajs/react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useEffect, useState } from 'react' import { useEffect, useState, useMemo } from 'react'
import AppLayout from '~/layouts/AppLayout' import AppLayout from '~/layouts/AppLayout'
import StyledButton from '~/components/StyledButton' import StyledButton from '~/components/StyledButton'
import api from '~/lib/api' import api from '~/lib/api'
import { ServiceSlim } from '../../../types/services' import { ServiceSlim } from '../../../types/services'
import CuratedCollectionCard from '~/components/CuratedCollectionCard' import CuratedCollectionCard from '~/components/CuratedCollectionCard'
import CategoryCard from '~/components/CategoryCard'
import TierSelectionModal from '~/components/TierSelectionModal'
import LoadingSpinner from '~/components/LoadingSpinner' import LoadingSpinner from '~/components/LoadingSpinner'
import Alert from '~/components/Alert' import Alert from '~/components/Alert'
import { IconCheck, IconChevronDown, IconChevronUp } from '@tabler/icons-react' import { IconCheck, IconChevronDown, IconChevronUp } from '@tabler/icons-react'
import StorageProjectionBar from '~/components/StorageProjectionBar'
import { useNotifications } from '~/context/NotificationContext' import { useNotifications } from '~/context/NotificationContext'
import useInternetStatus from '~/hooks/useInternetStatus' import useInternetStatus from '~/hooks/useInternetStatus'
import { useSystemInfo } from '~/hooks/useSystemInfo'
import classNames from 'classnames' import classNames from 'classnames'
import { CuratedCategory, CategoryTier, CategoryResource } from '../../../types/downloads'
// Capability definitions - maps user-friendly categories to services // Capability definitions - maps user-friendly categories to services
interface Capability { interface Capability {
@ -102,6 +107,21 @@ type WizardStep = 1 | 2 | 3 | 4
const CURATED_MAP_COLLECTIONS_KEY = 'curated-map-collections' const CURATED_MAP_COLLECTIONS_KEY = 'curated-map-collections'
const CURATED_ZIM_COLLECTIONS_KEY = 'curated-zim-collections' const CURATED_ZIM_COLLECTIONS_KEY = 'curated-zim-collections'
const CURATED_CATEGORIES_KEY = 'curated-categories'
const CATEGORIES_URL =
'https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/feature/tiered-collections/collections/kiwix-categories.json'
// Helper to get all resources for a tier (including inherited resources)
const getAllResourcesForTier = (tier: CategoryTier, allTiers: CategoryTier[]): CategoryResource[] => {
const resources = [...tier.resources]
if (tier.includesTier) {
const includedTier = allTiers.find((t) => t.slug === tier.includesTier)
if (includedTier) {
resources.unshift(...getAllResourcesForTier(includedTier, allTiers))
}
}
return resources
}
export default function EasySetupWizard(props: { system: { services: ServiceSlim[] } }) { export default function EasySetupWizard(props: { system: { services: ServiceSlim[] } }) {
const [currentStep, setCurrentStep] = useState<WizardStep>(1) const [currentStep, setCurrentStep] = useState<WizardStep>(1)
@ -111,14 +131,21 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim
const [isProcessing, setIsProcessing] = useState(false) const [isProcessing, setIsProcessing] = useState(false)
const [showAdditionalTools, setShowAdditionalTools] = useState(false) const [showAdditionalTools, setShowAdditionalTools] = useState(false)
// Category/tier selection state
const [selectedTiers, setSelectedTiers] = useState<Map<string, CategoryTier>>(new Map())
const [tierModalOpen, setTierModalOpen] = useState(false)
const [activeCategory, setActiveCategory] = useState<CuratedCategory | null>(null)
const { addNotification } = useNotifications() const { addNotification } = useNotifications()
const { isOnline } = useInternetStatus() const { isOnline } = useInternetStatus()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const { data: systemInfo } = useSystemInfo({ enabled: true })
const anySelectionMade = const anySelectionMade =
selectedServices.length > 0 || selectedServices.length > 0 ||
selectedMapCollections.length > 0 || selectedMapCollections.length > 0 ||
selectedZimCollections.length > 0 selectedZimCollections.length > 0 ||
selectedTiers.size > 0
const { data: mapCollections, isLoading: isLoadingMaps } = useQuery({ const { data: mapCollections, isLoading: isLoadingMaps } = useQuery({
queryKey: [CURATED_MAP_COLLECTIONS_KEY], queryKey: [CURATED_MAP_COLLECTIONS_KEY],
@ -136,6 +163,20 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim
const allServices = props.system.services const allServices = props.system.services
// Services that can still be installed (not already installed) // Services that can still be installed (not already installed)
// Fetch curated categories with tiers
const { data: categories, isLoading: isLoadingCategories } = useQuery({
queryKey: [CURATED_CATEGORIES_KEY],
queryFn: async () => {
const response = await fetch(CATEGORIES_URL)
if (!response.ok) {
throw new Error('Failed to fetch categories')
}
const data = await response.json()
return data.categories as CuratedCategory[]
},
refetchOnWindowFocus: false,
})
const availableServices = props.system.services.filter( const availableServices = props.system.services.filter(
(service) => !service.installed && service.installation_status !== 'installing' (service) => !service.installed && service.installation_status !== 'installing'
) )
@ -163,6 +204,85 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim
) )
} }
// Category/tier handlers
const handleCategoryClick = (category: CuratedCategory) => {
if (!isOnline) return
setActiveCategory(category)
setTierModalOpen(true)
}
const handleTierSelect = (category: CuratedCategory, tier: CategoryTier) => {
setSelectedTiers((prev) => {
const newMap = new Map(prev)
// If same tier is selected, deselect it
if (prev.get(category.slug)?.slug === tier.slug) {
newMap.delete(category.slug)
} else {
newMap.set(category.slug, tier)
}
return newMap
})
}
const closeTierModal = () => {
setTierModalOpen(false)
setActiveCategory(null)
}
// Get all resources from selected tiers for downloading
const getSelectedTierResources = (): CategoryResource[] => {
if (!categories) return []
const resources: CategoryResource[] = []
selectedTiers.forEach((tier, categorySlug) => {
const category = categories.find((c) => c.slug === categorySlug)
if (category) {
resources.push(...getAllResourcesForTier(tier, category.tiers))
}
})
return resources
}
// Calculate total projected storage from all selections
const projectedStorageBytes = useMemo(() => {
let totalBytes = 0
// Add tier resources
const tierResources = getSelectedTierResources()
totalBytes += tierResources.reduce((sum, r) => sum + r.size_mb * 1024 * 1024, 0)
// Add map collections
if (mapCollections) {
selectedMapCollections.forEach((slug) => {
const collection = mapCollections.find((c) => c.slug === slug)
if (collection) {
totalBytes += collection.resources.reduce((sum, r) => sum + r.size_mb * 1024 * 1024, 0)
}
})
}
// Add ZIM collections
if (zimCollections) {
selectedZimCollections.forEach((slug) => {
const collection = zimCollections.find((c) => c.slug === slug)
if (collection) {
totalBytes += collection.resources.reduce((sum, r) => sum + r.size_mb * 1024 * 1024, 0)
}
})
}
return totalBytes
}, [selectedTiers, selectedMapCollections, selectedZimCollections, categories, mapCollections, zimCollections])
// Get primary disk/filesystem info for storage projection
// Try disk array first (Linux/production), fall back to fsSize (Windows/dev)
const primaryDisk = systemInfo?.disk?.[0]
const primaryFs = systemInfo?.fsSize?.[0]
const storageInfo = primaryDisk
? { totalSize: primaryDisk.totalSize, totalUsed: primaryDisk.totalUsed }
: primaryFs
? { totalSize: primaryFs.size, totalUsed: primaryFs.used }
: null
const canProceedToNextStep = () => { const canProceedToNextStep = () => {
if (!isOnline) return false // Must be online to proceed if (!isOnline) return false // Must be online to proceed
if (currentStep === 1) return true // Can skip app installation if (currentStep === 1) return true // Can skip app installation
@ -200,9 +320,12 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim
await Promise.all(installPromises) await Promise.all(installPromises)
// Download collections and individual tier resources
const tierResources = getSelectedTierResources()
const downloadPromises = [ const downloadPromises = [
...selectedMapCollections.map((slug) => api.downloadMapCollection(slug)), ...selectedMapCollections.map((slug) => api.downloadMapCollection(slug)),
...selectedZimCollections.map((slug) => api.downloadZimCollection(slug)), ...selectedZimCollections.map((slug) => api.downloadZimCollection(slug)),
...tierResources.map((resource) => api.downloadRemoteZimFile(resource.url)),
] ]
await Promise.all(downloadPromises) await Promise.all(downloadPromises)
@ -263,7 +386,7 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim
const steps = [ const steps = [
{ number: 1, label: 'Apps' }, { number: 1, label: 'Apps' },
{ number: 2, label: 'Maps' }, { number: 2, label: 'Maps' },
{ number: 3, label: 'ZIM Files' }, { number: 3, label: 'Content' },
{ number: 4, label: 'Review' }, { number: 4, label: 'Review' },
] ]
@ -577,44 +700,79 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim
const renderStep3 = () => ( const renderStep3 = () => (
<div className="space-y-6"> <div className="space-y-6">
<div className="text-center mb-6"> <div className="text-center mb-6">
<h2 className="text-3xl font-bold text-gray-900 mb-2">Choose ZIM Files</h2> <h2 className="text-3xl font-bold text-gray-900 mb-2">Choose Content Collections</h2>
<p className="text-gray-600"> <p className="text-gray-600">
Select ZIM file collections for offline knowledge. You can always download more later. Select content categories for offline knowledge. Click a category to choose your preferred tier based on storage capacity.
</p> </p>
</div> </div>
{isLoadingZims ? (
{/* Curated Categories with Tiers */}
{isLoadingCategories ? (
<div className="flex justify-center py-12"> <div className="flex justify-center py-12">
<LoadingSpinner /> <LoadingSpinner />
</div> </div>
) : zimCollections && zimCollections.length > 0 ? ( ) : categories && categories.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <>
{zimCollections.map((collection) => ( <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div {categories.map((category) => (
key={collection.slug} <CategoryCard
onClick={() => key={category.slug}
isOnline && !collection.all_downloaded && toggleZimCollection(collection.slug) category={category}
} selectedTier={selectedTiers.get(category.slug) || null}
className={classNames( onClick={handleCategoryClick}
'relative', />
selectedZimCollections.includes(collection.slug) && ))}
'ring-4 ring-desert-green rounded-lg', </div>
collection.all_downloaded && 'opacity-75',
!isOnline && 'opacity-50 cursor-not-allowed' {/* Tier Selection Modal */}
)} <TierSelectionModal
> isOpen={tierModalOpen}
<CuratedCollectionCard collection={collection} size="large" /> onClose={closeTierModal}
{selectedZimCollections.includes(collection.slug) && ( category={activeCategory}
<div className="absolute top-2 right-2 bg-desert-green rounded-full p-1"> selectedTierSlug={activeCategory ? selectedTiers.get(activeCategory.slug)?.slug : null}
<IconCheck size={32} className="text-white" /> onSelectTier={handleTierSelect}
</div> />
)} </>
) : null}
{/* Legacy flat collections - show if available and no categories */}
{(!categories || categories.length === 0) && (
<>
{isLoadingZims ? (
<div className="flex justify-center py-12">
<LoadingSpinner />
</div> </div>
))} ) : zimCollections && zimCollections.length > 0 ? (
</div> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
) : ( {zimCollections.map((collection) => (
<div className="text-center py-12"> <div
<p className="text-gray-600 text-lg">No ZIM collections available at this time.</p> key={collection.slug}
</div> onClick={() =>
isOnline && !collection.all_downloaded && toggleZimCollection(collection.slug)
}
className={classNames(
'relative',
selectedZimCollections.includes(collection.slug) &&
'ring-4 ring-desert-green rounded-lg',
collection.all_downloaded && 'opacity-75',
!isOnline && 'opacity-50 cursor-not-allowed'
)}
>
<CuratedCollectionCard collection={collection} size="large" />
{selectedZimCollections.includes(collection.slug) && (
<div className="absolute top-2 right-2 bg-desert-green rounded-full p-1">
<IconCheck size={32} className="text-white" />
</div>
)}
</div>
))}
</div>
) : (
<div className="text-center py-12">
<p className="text-gray-600 text-lg">No content collections available at this time.</p>
</div>
)}
</>
)} )}
</div> </div>
) )
@ -623,7 +781,8 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim
const hasSelections = const hasSelections =
selectedServices.length > 0 || selectedServices.length > 0 ||
selectedMapCollections.length > 0 || selectedMapCollections.length > 0 ||
selectedZimCollections.length > 0 selectedZimCollections.length > 0 ||
selectedTiers.size > 0
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@ -700,6 +859,39 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim
</div> </div>
)} )}
{selectedTiers.size > 0 && (
<div className="bg-white rounded-lg border-2 border-desert-stone-light p-6">
<h3 className="text-xl font-semibold text-gray-900 mb-4">
Content Categories ({selectedTiers.size})
</h3>
{Array.from(selectedTiers.entries()).map(([categorySlug, tier]) => {
const category = categories?.find((c) => c.slug === categorySlug)
if (!category) return null
const resources = getAllResourcesForTier(tier, category.tiers)
return (
<div key={categorySlug} className="mb-4 last:mb-0">
<div className="flex items-center mb-2">
<IconCheck size={20} className="text-desert-green mr-2" />
<span className="text-gray-900 font-medium">
{category.name} - {tier.name}
</span>
<span className="text-gray-500 text-sm ml-2">
({resources.length} files)
</span>
</div>
<ul className="ml-7 space-y-1">
{resources.map((resource, idx) => (
<li key={idx} className="text-sm text-gray-600">
{resource.title}
</li>
))}
</ul>
</div>
)
})}
</div>
)}
<Alert <Alert
title="Ready to Start" title="Ready to Start"
message="Click 'Complete Setup' to begin installing apps and downloading content. This may take some time depending on your internet connection and the size of the downloads." message="Click 'Complete Setup' to begin installing apps and downloading content. This may take some time depending on your internet connection and the size of the downloads."
@ -727,6 +919,15 @@ export default function EasySetupWizard(props: { system: { services: ServiceSlim
<div className="max-w-7xl mx-auto px-4 py-8"> <div className="max-w-7xl mx-auto px-4 py-8">
<div className="bg-white rounded-md shadow-md"> <div className="bg-white rounded-md shadow-md">
{renderStepIndicator()} {renderStepIndicator()}
{storageInfo && (
<div className="px-6 pt-4">
<StorageProjectionBar
totalSize={storageInfo.totalSize}
currentUsed={storageInfo.totalUsed}
projectedAddition={projectedStorageBytes}
/>
</div>
)}
<div className="p-6 min-h-fit"> <div className="p-6 min-h-fit">
{currentStep === 1 && renderStep1()} {currentStep === 1 && renderStep1()}
{currentStep === 2 && renderStep2()} {currentStep === 2 && renderStep2()}

View File

@ -22,7 +22,7 @@ const NotificationsProvider = ({ children }: { children: React.ReactNode }) => {
} }
const removeNotification = (id: string) => { const removeNotification = (id: string) => {
setNotifications(notifications.filter((n) => n.id !== id)) setNotifications((prev) => prev.filter((n) => n.id !== id))
} }
const removeAllNotifications = () => { const removeAllNotifications = () => {

View File

@ -59,3 +59,33 @@ export type DownloadJobWithProgress = {
filepath: string filepath: string
filetype: string filetype: string
} }
// Tiered category types for curated collections UI
export type CategoryResource = {
title: string
description: string
size_mb: number
url: string
}
export type CategoryTier = {
name: string
slug: string
description: string
recommended?: boolean
includesTier?: string
resources: CategoryResource[]
}
export type CuratedCategory = {
name: string
slug: string
icon: string
description: string
language: string
tiers: CategoryTier[]
}
export type CuratedCategoriesFile = {
categories: CuratedCategory[]
}

View File

@ -0,0 +1,107 @@
# Kiwix Categories To-Do List
Potential categories to add to the tiered collections system in `kiwix-categories.json`.
## Current Categories (Completed)
- [x] Medicine - Medical references, first aid, emergency care
- [x] Survival & Preparedness - Food prep, prepper videos, repair guides
- [x] Education & Reference - Wikipedia, textbooks, TED talks
---
## High Priority
### Technology & Programming
Stack Overflow, developer documentation, coding tutorials
- Stack Overflow (multiple tags available)
- DevDocs documentation
- freeCodeCamp
- Programming language references
### Children & Family
Age-appropriate educational content for kids
- Wikipedia for Schools
- Wikibooks Children's Bookshelf
- Khan Academy Kids (via Kolibri - separate system)
- Storybooks, fairy tales
### Trades & Vocational
Practical skills for building, fixing, and maintaining
- Electrical wiring guides
- Plumbing basics
- Automotive repair
- Woodworking
- Welding fundamentals
### Agriculture & Gardening
Food production and farming (expand beyond what's in Survival)
- Practical Plants database
- Permaculture guides
- Seed saving
- Animal husbandry
- Composting and soil management
---
## Medium Priority
### Languages & Reference
Dictionaries, language learning, translation
- Wiktionary (multiple languages)
- Language learning resources
- Translation dictionaries
- Grammar guides
### History & Culture
Historical knowledge and cultural encyclopedias
- Wikipedia History portal content
- Historical documents
- Cultural archives
- Biographies
### Legal & Civic
Laws, rights, and civic procedures
- Legal references
- Constitutional documents
- Civic procedures
- Rights and responsibilities
### Communications
Emergency and amateur radio, networking
- Ham radio guides
- Emergency communication protocols
- Basic networking/IT
- Signal procedures
---
## Nice To Have
### Entertainment
Recreational reading and activities
- Project Gutenberg (fiction categories)
- Chess tutorials
- Puzzles and games
- Music theory
### Religion & Philosophy
Spiritual and philosophical texts
- Religious texts (various traditions)
- Philosophy references
- Ethics guides
### Regional/Non-English Bundles
Content in other languages
- Spanish language bundle
- French language bundle
- Other major languages
---
## Notes
- Each category should have 3 tiers: Essential, Standard, Comprehensive
- Higher tiers include all content from lower tiers via `includesTier`
- Check Kiwix catalog for available ZIM files: https://download.kiwix.org/zim/
- Consider storage constraints - Essential tiers should be <500MB ideally
- Mark one tier as `recommended: true` (usually Essential)

View File

@ -0,0 +1,325 @@
{
"categories": [
{
"name": "Medicine",
"slug": "medicine",
"icon": "IconStethoscope",
"description": "Medical references, guides, and encyclopedias for healthcare information and emergency preparedness.",
"language": "en",
"tiers": [
{
"name": "Essential",
"slug": "medicine-essential",
"description": "Core medical references for first aid, medications, and emergency care. Start here.",
"recommended": true,
"resources": [
{
"title": "Medical Library",
"description": "Field and emergency medicine books and guides",
"url": "https://download.kiwix.org/zim/other/zimgit-medicine_en_2024-08.zim",
"size_mb": 67
},
{
"title": "NHS Medicines A to Z",
"description": "How medicines work, dosages, side effects, and interactions",
"url": "https://download.kiwix.org/zim/zimit/nhs.uk_en_medicines_2025-12.zim",
"size_mb": 16
},
{
"title": "Military Medicine",
"description": "Tactical and field medicine manuals",
"url": "https://download.kiwix.org/zim/zimit/fas-military-medicine_en_2025-06.zim",
"size_mb": 78
},
{
"title": "CDC Health Information",
"description": "Disease prevention, travel health, and outbreak information",
"url": "https://download.kiwix.org/zim/zimit/wwwnc.cdc.gov_en_all_2024-11.zim",
"size_mb": 170
}
]
},
{
"name": "Standard",
"slug": "medicine-standard",
"description": "Comprehensive medical encyclopedia with detailed health information. Includes everything in Essential.",
"includesTier": "medicine-essential",
"resources": [
{
"title": "MedlinePlus",
"description": "NIH's consumer health encyclopedia - diseases, conditions, drugs, supplements",
"url": "https://download.kiwix.org/zim/zimit/medlineplus.gov_en_all_2025-01.zim",
"size_mb": 1800
}
]
},
{
"name": "Comprehensive",
"slug": "medicine-comprehensive",
"description": "Professional-level medical references and textbooks. Includes everything in Standard.",
"includesTier": "medicine-standard",
"resources": [
{
"title": "Wikipedia Medicine",
"description": "Curated medical articles from Wikipedia with images",
"url": "https://download.kiwix.org/zim/wikipedia/wikipedia_en_medicine_maxi_2026-01.zim",
"size_mb": 2000
},
{
"title": "LibreTexts Medicine",
"description": "Open-source medical textbooks and educational content",
"url": "https://download.kiwix.org/zim/libretexts/libretexts.org_en_med_2025-01.zim",
"size_mb": 1100
},
{
"title": "LibrePathology",
"description": "Pathology reference for disease identification",
"url": "https://download.kiwix.org/zim/other/librepathology_en_all_maxi_2025-09.zim",
"size_mb": 76
}
]
}
]
},
{
"name": "Survival & Preparedness",
"slug": "survival",
"icon": "IconShieldCheck",
"description": "Emergency preparedness, self-sufficiency, food storage, and practical survival skills.",
"language": "en",
"tiers": [
{
"name": "Essential",
"slug": "survival-essential",
"description": "Core food preparation and cooking guides. Lightweight text-based resources to get started.",
"recommended": true,
"resources": [
{
"title": "Food for Preppers",
"description": "Recipes and techniques for food preservation and preparation",
"url": "https://download.kiwix.org/zim/other/zimgit-food-preparation_en_2025-04.zim",
"size_mb": 98
},
{
"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",
"size_mb": 24
},
{
"title": "Based.Cooking",
"description": "Simple, practical recipes from the community",
"url": "https://download.kiwix.org/zim/zimit/based.cooking_en_all_2025-11.zim",
"size_mb": 16
}
]
},
{
"name": "Standard",
"slug": "survival-standard",
"description": "Video guides for winter survival, bug-out gear, plus gardening and cooking Q&A. Includes Essential.",
"includesTier": "survival-essential",
"resources": [
{
"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",
"size_mb": 1340
},
{
"title": "Canadian Prepper: Bug Out Roll",
"description": "Essential gear selection for your bug-out bag",
"url": "https://download.kiwix.org/zim/videos/canadian_prepper_bugoutroll_en_2025-08.zim",
"size_mb": 975
},
{
"title": "Gardening Q&A",
"description": "Stack Exchange Q&A for growing your own food",
"url": "https://download.kiwix.org/zim/stack_exchange/gardening.stackexchange.com_en_all_2025-12.zim",
"size_mb": 923
},
{
"title": "Cooking Q&A",
"description": "Stack Exchange Q&A for cooking techniques and food safety",
"url": "https://download.kiwix.org/zim/stack_exchange/cooking.stackexchange.com_en_all_2025-12.zim",
"size_mb": 236
}
]
},
{
"name": "Comprehensive",
"slug": "survival-comprehensive",
"description": "Full prepper video library plus repair guides and DIY skills. Includes Standard.",
"includesTier": "survival-standard",
"resources": [
{
"title": "Urban Prepper",
"description": "Comprehensive urban emergency preparedness video series",
"url": "https://download.kiwix.org/zim/videos/urban-prepper_en_all_2025-11.zim",
"size_mb": 2240
},
{
"title": "Canadian Prepper: Prepping Food",
"description": "Long-term food storage and survival meal preparation",
"url": "https://download.kiwix.org/zim/videos/canadian_prepper_preppingfood_en_2025-09.zim",
"size_mb": 2160
},
{
"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",
"size_mb": 2890
},
{
"title": "Learning Self-Reliance",
"description": "Prepping, survival skills, beekeeping, and homesteading",
"url": "https://download.kiwix.org/zim/videos/lrnselfreliance_en_all_2025-12.zim",
"size_mb": 3970
},
{
"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
},
{
"title": "DIY & Home Improvement Q&A",
"description": "Stack Exchange Q&A for home repairs and construction",
"url": "https://download.kiwix.org/zim/stack_exchange/diy.stackexchange.com_en_all_2025-12.zim",
"size_mb": 1900
}
]
}
]
},
{
"name": "Education & Reference",
"slug": "education",
"icon": "IconSchool",
"description": "Encyclopedias, textbooks, tutorials, and educational videos for self-directed learning.",
"language": "en",
"tiers": [
{
"name": "Essential",
"slug": "education-essential",
"description": "Core reference materials - Wikipedia's best articles and open textbooks. Lightweight, text-focused.",
"recommended": true,
"resources": [
{
"title": "Wikipedia Top 45,000 Articles",
"description": "The 45,000 best Wikipedia articles, optimized for size (no images)",
"url": "https://download.kiwix.org/zim/wikipedia/wikipedia_en_top_nopic_2025-12.zim",
"size_mb": 1880
},
{
"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",
"size_mb": 3100
}
]
},
{
"name": "Standard",
"slug": "education-standard",
"description": "Adds educational videos, university-level tutorials, and STEM textbooks. Includes Essential.",
"includesTier": "education-essential",
"resources": [
{
"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",
"size_mb": 5610
},
{
"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",
"size_mb": 2370
},
{
"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
},
{
"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
},
{
"title": "LibreTexts Chemistry",
"description": "Chemistry courses and textbooks",
"url": "https://download.kiwix.org/zim/libretexts/libretexts.org_en_chem_2025-01.zim",
"size_mb": 2180
},
{
"title": "LibreTexts Biology",
"description": "Biology courses and textbooks",
"url": "https://download.kiwix.org/zim/libretexts/libretexts.org_en_bio_2025-01.zim",
"size_mb": 2240
},
{
"title": "Project Gutenberg: Education",
"description": "Classic educational texts and resources",
"url": "https://download.kiwix.org/zim/gutenberg/gutenberg_en_education_2025-12.zim",
"size_mb": 606
}
]
},
{
"name": "Comprehensive",
"slug": "education-comprehensive",
"description": "Complete educational library with full Wikipedia, enhanced textbooks, and TED talks. Includes Standard.",
"includesTier": "education-standard",
"resources": [
{
"title": "Wikipedia (Full, No Images)",
"description": "Complete English Wikipedia - over 6 million articles",
"url": "https://download.kiwix.org/zim/wikipedia/wikipedia_en_all_mini_2025-12.zim",
"size_mb": 11400
},
{
"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",
"size_mb": 5400
},
{
"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",
"size_mb": 16500
},
{
"title": "LibreTexts Humanities",
"description": "Literature, philosophy, history, and social sciences",
"url": "https://download.kiwix.org/zim/libretexts/libretexts.org_en_human_2025-01.zim",
"size_mb": 3730
},
{
"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
},
{
"title": "LibreTexts Engineering",
"description": "Engineering courses and technical references",
"url": "https://download.kiwix.org/zim/libretexts/libretexts.org_en_eng_2025-01.zim",
"size_mb": 678
},
{
"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
}
]
}
]
}
]
}