diff --git a/admin/constants/supply_depot_docs.ts b/admin/constants/supply_depot_docs.ts index 4c204e1..21d5f55 100644 --- a/admin/constants/supply_depot_docs.ts +++ b/admin/constants/supply_depot_docs.ts @@ -17,6 +17,7 @@ export const SUPPLY_DEPOT_DOC_ANCHORS: Record = { [SERVICE_NAMES.VAULTWARDEN]: 'vaultwarden', [SERVICE_NAMES.JELLYFIN]: 'jellyfin', [SERVICE_NAMES.MESHTASTIC_WEB]: 'meshtastic-web', + [SERVICE_NAMES.KOLIBRI]: 'kolibri', [SERVICE_NAMES.KOLIBRI_GEN2]: 'kolibri', } diff --git a/admin/docs/supply-depot-apps.md b/admin/docs/supply-depot-apps.md index d666e07..3b09a4f 100644 --- a/admin/docs/supply-depot-apps.md +++ b/admin/docs/supply-depot-apps.md @@ -251,13 +251,14 @@ A complete offline learning platform from Learning Equality. Kolibri pulls toget **First time you open it, you'll go through a quick setup wizard.** Pick your facility type and create the **admin account** (this is the super-user that manages the whole device, so give it a real password and keep track of it). Once you're in, you import learning content as **channels**. -**Getting content in (you re-download it):** Kolibri's content is delivered as channels you import. Open **Device → Channels → Import**, and either pull channels from Kolibri Studio (online) or import from a local drive or another Kolibri device if you already have the content files. There's a lot available, so import just the channels you need; they can be large. +**Importing content:** Kolibri's content is delivered as channels you import. Open **Device → Channels → Import**, and either pull channels from Kolibri Studio (online) or import from a local drive or another Kolibri device if you already have the content files. There's a lot available, so import just the channels you need; they can be large. -**Upgrading from an older NOMAD's Kolibri:** Earlier NOMAD releases shipped a much older Kolibri (the `treehouses/kolibri:0.12.8` image). This Education Platform is a newer, upstream-official Kolibri and installs **fresh** — your old channels and learner data are **not** carried over automatically, because the two versions store data too differently to migrate safely. If you were running the old one: +**Migrating content from Education Platform (Gen 1):** Earlier NOMAD releases shipped a much older Kolibri (the `treehouses/kolibri:0.12.8` image). The Education Platform "Gen 2" is a newer, upstream-official Kolibri and installs **fresh** — your old channels and learner data are **not** carried over automatically, because the two versions store data too differently to migrate safely. If you were running the old one and want to import your existing channels into the new one, here's the process: -1. Install this Education Platform from the catalog (it runs alongside the old one on a different port, so nothing is disrupted while you set it up). -2. Re-import the channels you want, as above. -3. Once you're happy with the new install, uninstall the old Kolibri from its card (it carries a **legacy** badge). Your old data folder stays on disk until you remove it. +1. Install "Education Platform (Gen 2)" from the catalog (it runs alongside the old one on a different port, so nothing is disrupted while you set it up). +2. Launch the new one, walk through the setup wizard, then from the sidebar menu, navigate to **Device > Channels > Import**. Choose the "Local network or internet" option, and then "Add new device". In the dialog that appears, enter the IP address of your NOMAD with the old Education Platform port (8300 by default, so for example `http://192.168.1.36:8300`), give it a name (anything you'd like), and click "Add", and then "Continue". +3. You can now select individual channels from the old Education Platform, or choose "Select entire channels instead" to import everything at once. Click "Import" when ready, and the transfer will start. +3. Once you're happy with the new install and have any content copied over, uninstall the old Education Platform from its card (it carries a **legacy** badge). It's also recommended to choose to remove the old image and data volume when uninstalling to avoid confusion and free up space, but if you want to keep it around for a while just in case, that's totally fine too. **Your data:** Your imported channels, classes, and learner progress live in the `storage/kolibri-gen2` folder on your NOMAD. Backing up that folder backs up your whole Kolibri. diff --git a/admin/inertia/pages/supply-depot.tsx b/admin/inertia/pages/supply-depot.tsx index f73bc2f..3943e99 100644 --- a/admin/inertia/pages/supply-depot.tsx +++ b/admin/inertia/pages/supply-depot.tsx @@ -2,6 +2,7 @@ import { Head, router } from '@inertiajs/react' import { useEffect, useRef, useState } from 'react' import { IconAlertTriangle, + IconArrowRight, IconArrowUp, IconBook, IconBox, @@ -44,6 +45,7 @@ import { getServiceLink } from '~/lib/navigation' import { getSupplyDepotDocLink } from '../../constants/supply_depot_docs' import api from '~/lib/api' import { toTitleCase } from '../../app/utils/misc' +import { SERVICE_NAMES } from '../../constants/service_names' function extractTag(containerImage: string): string { if (!containerImage) return '' @@ -168,7 +170,7 @@ export default function SupplyDepotPage(props: { system: { services: ServiceSlim .then((res) => { if (res) setPreflight(res) }) - .catch(() => {}) // non-fatal; proceed without warnings + .catch(() => { }) // non-fatal; proceed without warnings .finally(() => setPreflightLoading(false)) }, [modal]) @@ -193,6 +195,16 @@ export default function SupplyDepotPage(props: { system: { services: ServiceSlim const installedServices = filteredServices.filter((s) => s.installed) const availableServices = filteredServices.filter((s) => !s.installed) + // Whether the new Kolibri (Gen 2) install exists — gates the "Migrate content to Gen 2" action on + // the legacy Kolibri card. Computed from the full (unfiltered) list so a search filter can't hide it. + const educationGen2Installed = props.system.services.some( + (s) => s.service_name === SERVICE_NAMES.KOLIBRI_GEN2 && s.installed + ) + + useEffect(() => { + console.log("Education Gen 2 installed:", educationGen2Installed) + }, [educationGen2Installed]) + // ── Actions ─────────────────────────────────────────────────────────────── async function handleInstall(service: ServiceSlim) { const hasWarnings = @@ -420,11 +432,10 @@ export default function SupplyDepotPage(props: { system: { services: ServiceSlim @@ -473,6 +484,8 @@ export default function SupplyDepotPage(props: { system: { services: ServiceSlim } autoUpdateMasterEnabled={appAutoUpdateMasterEnabled} onToggleAutoUpdate={(enabled) => handleToggleAutoUpdate(service, enabled)} + migrationInstructionsHref={(service.service_name.startsWith(SERVICE_NAMES.KOLIBRI) && educationGen2Installed) ? getSupplyDepotDocLink(SERVICE_NAMES.KOLIBRI) || undefined : undefined} + migrationInstructionsText={(service.service_name === SERVICE_NAMES.KOLIBRI) ? 'How to migrate content to Gen 2' : "How to migrate content from Gen 1"} /> ))} @@ -795,6 +808,8 @@ interface AppCardProps { // Global master switch (Settings → Updates). When off, per-app toggles are inert. autoUpdateMasterEnabled?: boolean onToggleAutoUpdate?: (enabled: boolean) => void + migrationInstructionsHref?: string + migrationInstructionsText?: string } function AppCard({ @@ -818,6 +833,8 @@ function AppCard({ autoUpdateEnabled, autoUpdateMasterEnabled, onToggleAutoUpdate, + migrationInstructionsHref, + migrationInstructionsText, }: AppCardProps) { const isRunning = service.status === 'running' const isStopped = service.installed && !isRunning @@ -852,11 +869,10 @@ function AppCard({ return (
{/* Installed accent spine (rounded to follow the card corners — the card no longer clips overflow so the Manage dropdown can open above the card without being cut off) */} @@ -975,7 +991,7 @@ function AppCard({ {/* Open button — shown when the app has a default location or a user-set custom URL */} {(service.ui_location || service.custom_url) && ( } label="Stats" onClick={onStats} /> } label="Edit" onClick={onEdit} /> } label="Set custom URL" onClick={onSetUrl} /> + { + migrationInstructionsHref ? ( + e.stopPropagation()} + className="flex items-center gap-2 w-full px-3 py-2 text-xs transition-colors text-left cursor-pointer text-text-primary hover:bg-surface-secondary" + > + + {migrationInstructionsText || 'Migration instructions'} + + ) : (null) + } {!service.is_custom && onToggleAutoUpdate ? ( autoUpdateMasterEnabled ? ( } label="Force Reinstall" onClick={onReinstall} danger /> {service.is_custom ? ( } label="Delete" onClick={onDelete} danger /> - ): ( + ) : ( } label="Uninstall" onClick={onUninstall} danger /> )}
@@ -1087,11 +1117,10 @@ function DropdownItem({ e.stopPropagation() onClick() }} - className={`flex items-center gap-2 w-full px-3 py-2 text-xs transition-colors text-left cursor-pointer ${ - danger + className={`flex items-center gap-2 w-full px-3 py-2 text-xs transition-colors text-left cursor-pointer ${danger ? 'text-desert-red hover:bg-desert-red/10' : 'text-text-primary hover:bg-surface-secondary' - }`} + }`} > {icon} {label}