feat: replace legacy Kolibri image default with latest v19 image

This commit is contained in:
jakeaturner 2026-06-15 21:18:11 +00:00
parent 79d6854464
commit 565bad59da
No known key found for this signature in database
GPG Key ID: B1072EBDEECE328D
10 changed files with 114 additions and 8 deletions

View File

@ -85,6 +85,16 @@ export default class Service extends BaseModel {
@column()
declare category: string | null
// When true the service is sunset: hidden from the install catalog unless it is already
// installed (see SystemService.getServices). Lets a deprecated app stay manageable for users who
// still run it while keeping new users from installing it.
@column({
serialize(value) {
return Boolean(value)
},
})
declare is_deprecated: boolean
@column()
declare source_repo: string | null

View File

@ -333,9 +333,15 @@ export class SystemService {
'auto_update_enabled',
'is_custom',
'is_user_modified',
'is_deprecated',
'category'
)
.where('is_dependency_service', false)
// Deprecated/sunset apps stay visible only while still installed, so the user can manage and
// uninstall them — they never reappear in the install catalog once removed.
.where((q) => {
q.where('is_deprecated', false).orWhere('installed', true)
})
if (installedOnly) {
query.where('installed', true)
}
@ -367,6 +373,7 @@ export class SystemService {
auto_update_enabled: service.auto_update_enabled,
is_custom: service.is_custom,
is_user_modified: service.is_user_modified,
is_deprecated: service.is_deprecated,
category: service.category,
})
}

View File

@ -5,6 +5,7 @@ export const SERVICE_NAMES = {
CYBERCHEF: 'nomad_cyberchef',
FLATNOTES: 'nomad_flatnotes',
KOLIBRI: 'nomad_kolibri',
KOLIBRI_GEN2: 'nomad_kolibri_2',
// Supply Depot — curated catalog (ports 84008499)
STIRLING_PDF: 'nomad_stirling_pdf',
FILEBROWSER: 'nomad_filebrowser',

View File

@ -17,6 +17,7 @@ export const SUPPLY_DEPOT_DOC_ANCHORS: Record<string, string> = {
[SERVICE_NAMES.VAULTWARDEN]: 'vaultwarden',
[SERVICE_NAMES.JELLYFIN]: 'jellyfin',
[SERVICE_NAMES.MESHTASTIC_WEB]: 'meshtastic-web',
[SERVICE_NAMES.KOLIBRI_GEN2]: 'kolibri',
}
// Returns the in-app docs link for a service, or null if it has no documentation section.

View File

@ -0,0 +1,43 @@
import { BaseSchema } from '@adonisjs/lucid/schema'
import { SERVICE_NAMES } from '../../constants/service_names.js'
export default class extends BaseSchema {
protected tableName = 'services'
async up() {
// Generic deprecation flag (reusable for future sunsets): a deprecated service is hidden from
// the install catalog unless it is already installed — see SystemService.getServices().
this.schema.alterTable(this.tableName, (table) => {
table.boolean('is_deprecated').notNullable().defaultTo(false)
})
// Sunset the legacy treehouses/kolibri:0.12.8 entry, replaced by the learningequality Gen 2
// entry seeded as `nomad_kolibri_2`. The seeder is additive + sync-existing and never deletes,
// so without this step every existing deployment keeps an orphaned `nomad_kolibri` row and can
// still install the dead 6-year-old image. Conditional handling keeps it data-safe:
this.defer(async (db) => {
// Never installed → just an orphaned catalog row; drop it outright.
await db
.from(this.tableName)
.where('service_name', SERVICE_NAMES.KOLIBRI)
.where('installed', false)
.delete()
// Currently installed → a running 0.12.8 container holds port 8300 + a bind mount. Keep the
// row (it's Nomad's only handle to open/stop/uninstall that container) but flag it deprecated
// so it shows a "Legacy" badge and drops out of the catalog once the user uninstalls it.
await db
.from(this.tableName)
.where('service_name', SERVICE_NAMES.KOLIBRI)
.where('installed', true)
.update({ is_deprecated: true })
})
}
async down() {
// Note: the legacy-row deletion in up() is a one-way data change and is not restored here.
this.schema.alterTable(this.tableName, (table) => {
table.dropColumn('is_deprecated')
})
}
}

View File

@ -14,6 +14,7 @@ type ServiceSeedRecord = Omit<
| 'update_checked_at'
| 'metadata'
| 'is_user_modified'
| 'is_deprecated'
| 'custom_url'
| 'auto_update_enabled'
| 'available_update_first_seen_at'
@ -163,24 +164,38 @@ export default class ServiceSeeder extends BaseSeeder {
depends_on: null,
},
{
service_name: SERVICE_NAMES.KOLIBRI,
friendly_name: 'Education Platform',
// "Kolibri Gen 2" — the upstream-official learningequality image replacing the ~6-year-old
// community treehouses/kolibri:0.12.8. This is a distinct catalog entry (own service_name,
// volume, and ports), not an in-place upgrade: the new image uses a different repo, mounts at
// /kolibri instead of /root/.kolibri, and crosses 7 minor versions of Kolibri's own data
// schema. Existing 0.12.8 installs are sunset via the deprecate-legacy-kolibri migration and
// keep running on 8300 until uninstalled; content is re-imported into the fresh Gen 2 install.
service_name: SERVICE_NAMES.KOLIBRI_GEN2,
friendly_name: 'Education Platform (Gen 2)',
powered_by: 'Kolibri',
display_order: 2,
description: 'Interactive learning platform with video courses and exercises',
icon: 'IconSchool',
container_image: 'treehouses/kolibri:0.12.8',
container_image: 'learningequality/kolibri:0.19.4',
source_repo: 'https://github.com/learningequality/kolibri',
container_command: null,
container_config: JSON.stringify({
HostConfig: {
RestartPolicy: { Name: 'unless-stopped' },
PortBindings: { '8080/tcp': [{ HostPort: '8300' }] },
Binds: [`${ServiceSeeder.NOMAD_STORAGE_ABS_PATH}/kolibri:/root/.kolibri`],
// 8080 = web UI. 8311 = zip-content server (interactive exercises / HTML5 apps), served
// from a separate "alternate origin" the browser connects to DIRECTLY. KOLIBRI_ZIP_CONTENT_PORT
// sets the port Kolibri both LISTENS on inside the container AND advertises in content URLs,
// so the internal port, the published host port, and that env value must all be identical
// (8311) — otherwise content URLs point at a host port that doesn't route to the listener
// and every content page fails with ERR_CONNECTION_REFUSED. The image's default 8081 is
// unused here. The image refuses to start without /kolibri mounted (KOLIBRI_HOME = /kolibri).
PortBindings: { '8080/tcp': [{ HostPort: '8310' }], '8311/tcp': [{ HostPort: '8311' }] },
Binds: [`${ServiceSeeder.NOMAD_STORAGE_ABS_PATH}/kolibri-gen2:/kolibri`],
},
ExposedPorts: { '8080/tcp': {} },
ExposedPorts: { '8080/tcp': {}, '8311/tcp': {} },
Env: ['KOLIBRI_ZIP_CONTENT_PORT=8311'],
}),
ui_location: '8300',
ui_location: '8310',
installed: false,
installation_status: 'idle',
is_dependency_service: false,

View File

@ -242,3 +242,23 @@ A browser-based control panel for [Meshtastic](https://meshtastic.org) devices.
**Your data:** There's nothing to set up or store on your NOMAD for this app. Your radio's settings live on the radio itself, and this app's preferences live in your browser. There's no NOMAD folder to manage.
**Works offline:** Fully offline, which is the entire point of Meshtastic. The app is served from your NOMAD, and talking to your radios happens over your local network or radio, never the internet. The only online bits are the links in the footer (Vercel, legal), which don't matter for using your mesh.
## Education Platform (Kolibri) {% #kolibri %}
A complete offline learning platform from Learning Equality. Kolibri pulls together video lessons, exercises, and readings into structured channels, organizes them into classes and lessons, tracks learner progress, and works entirely on your NOMAD with no internet. It's built for schools and learners in places with little or no connectivity.
**Official site:** [learningequality.org/kolibri](https://learningequality.org/kolibri) · **Source:** [github.com/learningequality/kolibri](https://github.com/learningequality/kolibri)
**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.
**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:
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.
**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.
**Works offline:** Fully offline once content is imported, that's what Kolibri is for. The only step that uses the internet is importing channels from Kolibri Studio; everything after that, browsing lessons, doing exercises, tracking progress, runs entirely on your NOMAD.

View File

@ -61,7 +61,7 @@ function buildCoreCapabilities(aiAssistantName: string): Capability[] {
'Interactive exercises and quizzes',
'Progress tracking for learners',
],
services: [SERVICE_NAMES.KOLIBRI],
services: [SERVICE_NAMES.KOLIBRI_GEN2],
icon: 'IconSchool',
},
{

View File

@ -930,6 +930,14 @@ function AppCard({
modified
</span>
) : null}
{service.is_deprecated ? (
<span
className="text-xs px-2 py-0.5 rounded-full font-medium bg-desert-orange-lighter text-desert-orange-dark border border-desert-orange-light"
title="This is a legacy version that's no longer maintained. Install the current Education Platform from the catalog, then uninstall this one."
>
legacy
</span>
) : null}
{uiPort && (
<span className="text-xs px-2 py-0.5 rounded-full bg-surface-secondary text-text-muted font-mono">
{uiIsHttps ? '🔒 ' : ''}:{uiPort}

View File

@ -18,5 +18,6 @@ export type ServiceSlim = Pick<
| 'auto_update_enabled'
| 'is_custom'
| 'is_user_modified'
| 'is_deprecated'
| 'category'
> & { status?: string }