mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-04-05 16:26:15 +02:00
feat: Use friendly app names on Dashboard with open source attribution
Updates the Dashboard to use the same user-friendly names as the Easy Setup Wizard, giving credit to the open source projects powering each capability: - Kiwix → Information Library (Powered by Kiwix) - Kolibri → Education Platform (Powered by Kolibri) - Open WebUI → AI Assistant (Powered by Open WebUI + Ollama) - FlatNotes → Notes (Powered by FlatNotes) - CyberChef → Data Tools (Powered by CyberChef) Also reorders Dashboard cards to prioritize Core Capabilities first, with Maps promoted to Core Capability status, followed by Additional Tools, then system items (Easy Setup, Install Apps, Docs, Settings). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
33f04728c5
commit
a7ba9c0219
|
|
@ -26,6 +26,12 @@ export default class Service extends BaseModel {
|
||||||
@column()
|
@column()
|
||||||
declare description: string | null
|
declare description: string | null
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare powered_by: string | null
|
||||||
|
|
||||||
|
@column()
|
||||||
|
declare display_order: number | null
|
||||||
|
|
||||||
@column()
|
@column()
|
||||||
declare icon: string | null // must be a TablerIcons name to be properly rendered in the UI (e.g. "IconBrandDocker")
|
declare icon: string | null // must be a TablerIcons name to be properly rendered in the UI (e.g. "IconBrandDocker")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ export class SystemService {
|
||||||
await this._syncContainersWithDatabase() // Sync up before fetching to ensure we have the latest status
|
await this._syncContainersWithDatabase() // Sync up before fetching to ensure we have the latest status
|
||||||
|
|
||||||
const query = Service.query()
|
const query = Service.query()
|
||||||
|
.orderBy('display_order', 'asc')
|
||||||
.orderBy('friendly_name', 'asc')
|
.orderBy('friendly_name', 'asc')
|
||||||
.select(
|
.select(
|
||||||
'id',
|
'id',
|
||||||
|
|
@ -70,7 +71,9 @@ export class SystemService {
|
||||||
'ui_location',
|
'ui_location',
|
||||||
'friendly_name',
|
'friendly_name',
|
||||||
'description',
|
'description',
|
||||||
'icon'
|
'icon',
|
||||||
|
'powered_by',
|
||||||
|
'display_order'
|
||||||
)
|
)
|
||||||
.where('is_dependency_service', false)
|
.where('is_dependency_service', false)
|
||||||
if (installedOnly) {
|
if (installedOnly) {
|
||||||
|
|
@ -98,6 +101,8 @@ export class SystemService {
|
||||||
installation_status: service.installation_status,
|
installation_status: service.installation_status,
|
||||||
status: status ? status.status : 'unknown',
|
status: status ? status.status : 'unknown',
|
||||||
ui_location: service.ui_location || '',
|
ui_location: service.ui_location || '',
|
||||||
|
powered_by: service.powered_by,
|
||||||
|
display_order: service.display_order,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||||
|
|
||||||
|
export default class extends BaseSchema {
|
||||||
|
protected tableName = 'services'
|
||||||
|
|
||||||
|
async up() {
|
||||||
|
this.schema.alterTable(this.tableName, (table) => {
|
||||||
|
table.string('powered_by').nullable()
|
||||||
|
table.integer('display_order').nullable().defaultTo(100)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async down() {
|
||||||
|
this.schema.alterTable(this.tableName, (table) => {
|
||||||
|
table.dropColumn('powered_by')
|
||||||
|
table.dropColumn('display_order')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||||
|
|
||||||
|
export default class extends BaseSchema {
|
||||||
|
protected tableName = 'services'
|
||||||
|
|
||||||
|
async up() {
|
||||||
|
// Update existing services with new friendly names and powered_by values
|
||||||
|
await this.db.rawQuery(`
|
||||||
|
UPDATE services SET
|
||||||
|
friendly_name = 'Information Library',
|
||||||
|
powered_by = 'Kiwix',
|
||||||
|
display_order = 1,
|
||||||
|
description = 'Offline access to Wikipedia, medical references, how-to guides, and encyclopedias'
|
||||||
|
WHERE service_name = 'nomad_kiwix_serve'
|
||||||
|
`)
|
||||||
|
|
||||||
|
await this.db.rawQuery(`
|
||||||
|
UPDATE services SET
|
||||||
|
friendly_name = 'Education Platform',
|
||||||
|
powered_by = 'Kolibri',
|
||||||
|
display_order = 2,
|
||||||
|
description = 'Interactive learning platform with video courses and exercises'
|
||||||
|
WHERE service_name = 'nomad_kolibri'
|
||||||
|
`)
|
||||||
|
|
||||||
|
await this.db.rawQuery(`
|
||||||
|
UPDATE services SET
|
||||||
|
friendly_name = 'AI Assistant',
|
||||||
|
powered_by = 'Open WebUI + Ollama',
|
||||||
|
display_order = 3,
|
||||||
|
description = 'Local AI chat that runs entirely on your hardware - no internet required'
|
||||||
|
WHERE service_name = 'nomad_open_webui'
|
||||||
|
`)
|
||||||
|
|
||||||
|
await this.db.rawQuery(`
|
||||||
|
UPDATE services SET
|
||||||
|
friendly_name = 'Notes',
|
||||||
|
powered_by = 'FlatNotes',
|
||||||
|
display_order = 10,
|
||||||
|
description = 'Simple note-taking app with local storage'
|
||||||
|
WHERE service_name = 'nomad_flatnotes'
|
||||||
|
`)
|
||||||
|
|
||||||
|
await this.db.rawQuery(`
|
||||||
|
UPDATE services SET
|
||||||
|
friendly_name = 'Data Tools',
|
||||||
|
powered_by = 'CyberChef',
|
||||||
|
display_order = 11,
|
||||||
|
description = 'Swiss Army knife for data encoding, encryption, and analysis'
|
||||||
|
WHERE service_name = 'nomad_cyberchef'
|
||||||
|
`)
|
||||||
|
|
||||||
|
await this.db.rawQuery(`
|
||||||
|
UPDATE services SET
|
||||||
|
display_order = 100
|
||||||
|
WHERE service_name = 'nomad_ollama'
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async down() {
|
||||||
|
// Revert to original names
|
||||||
|
await this.db.rawQuery(`
|
||||||
|
UPDATE services SET
|
||||||
|
friendly_name = 'Kiwix',
|
||||||
|
powered_by = NULL,
|
||||||
|
display_order = NULL,
|
||||||
|
description = 'Offline Wikipedia, eBooks, and more'
|
||||||
|
WHERE service_name = 'nomad_kiwix_serve'
|
||||||
|
`)
|
||||||
|
|
||||||
|
await this.db.rawQuery(`
|
||||||
|
UPDATE services SET
|
||||||
|
friendly_name = 'Kolibri',
|
||||||
|
powered_by = NULL,
|
||||||
|
display_order = NULL,
|
||||||
|
description = 'An offline-first education platform for schools and learners'
|
||||||
|
WHERE service_name = 'nomad_kolibri'
|
||||||
|
`)
|
||||||
|
|
||||||
|
await this.db.rawQuery(`
|
||||||
|
UPDATE services SET
|
||||||
|
friendly_name = 'Open WebUI',
|
||||||
|
powered_by = NULL,
|
||||||
|
display_order = NULL,
|
||||||
|
description = 'A web interface for interacting with local AI models served by Ollama'
|
||||||
|
WHERE service_name = 'nomad_open_webui'
|
||||||
|
`)
|
||||||
|
|
||||||
|
await this.db.rawQuery(`
|
||||||
|
UPDATE services SET
|
||||||
|
friendly_name = 'FlatNotes',
|
||||||
|
powered_by = NULL,
|
||||||
|
display_order = NULL,
|
||||||
|
description = 'A simple note-taking app that stores all files locally'
|
||||||
|
WHERE service_name = 'nomad_flatnotes'
|
||||||
|
`)
|
||||||
|
|
||||||
|
await this.db.rawQuery(`
|
||||||
|
UPDATE services SET
|
||||||
|
friendly_name = 'CyberChef',
|
||||||
|
powered_by = NULL,
|
||||||
|
display_order = NULL,
|
||||||
|
description = 'The Cyber Swiss Army Knife - a web app for encryption, encoding, and data analysis'
|
||||||
|
WHERE service_name = 'nomad_cyberchef'
|
||||||
|
`)
|
||||||
|
|
||||||
|
await this.db.rawQuery(`
|
||||||
|
UPDATE services SET
|
||||||
|
display_order = NULL
|
||||||
|
WHERE service_name = 'nomad_ollama'
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,8 +10,10 @@ export default class ServiceSeeder extends BaseSeeder {
|
||||||
private static DEFAULT_SERVICES: Omit<ModelAttributes<Service>, 'created_at' | 'updated_at' | 'metadata' | 'id'>[] = [
|
private static DEFAULT_SERVICES: Omit<ModelAttributes<Service>, 'created_at' | 'updated_at' | 'metadata' | 'id'>[] = [
|
||||||
{
|
{
|
||||||
service_name: DockerService.KIWIX_SERVICE_NAME,
|
service_name: DockerService.KIWIX_SERVICE_NAME,
|
||||||
friendly_name: 'Kiwix',
|
friendly_name: 'Information Library',
|
||||||
description: 'Offline Wikipedia, eBooks, and more',
|
powered_by: 'Kiwix',
|
||||||
|
display_order: 1,
|
||||||
|
description: 'Offline access to Wikipedia, medical references, how-to guides, and encyclopedias',
|
||||||
icon: 'IconBooks',
|
icon: 'IconBooks',
|
||||||
container_image: 'ghcr.io/kiwix/kiwix-serve:3.8.1',
|
container_image: 'ghcr.io/kiwix/kiwix-serve:3.8.1',
|
||||||
container_command: '*.zim --address=all',
|
container_command: '*.zim --address=all',
|
||||||
|
|
@ -32,6 +34,8 @@ export default class ServiceSeeder extends BaseSeeder {
|
||||||
{
|
{
|
||||||
service_name: DockerService.OLLAMA_SERVICE_NAME,
|
service_name: DockerService.OLLAMA_SERVICE_NAME,
|
||||||
friendly_name: 'Ollama',
|
friendly_name: 'Ollama',
|
||||||
|
powered_by: null,
|
||||||
|
display_order: 100, // Dependency service, not shown directly
|
||||||
description: 'Run local LLMs (AI models) with ease on your own hardware',
|
description: 'Run local LLMs (AI models) with ease on your own hardware',
|
||||||
icon: 'IconRobot',
|
icon: 'IconRobot',
|
||||||
container_image: 'ollama/ollama:latest',
|
container_image: 'ollama/ollama:latest',
|
||||||
|
|
@ -52,8 +56,10 @@ export default class ServiceSeeder extends BaseSeeder {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
service_name: DockerService.OPEN_WEBUI_SERVICE_NAME,
|
service_name: DockerService.OPEN_WEBUI_SERVICE_NAME,
|
||||||
friendly_name: 'Open WebUI',
|
friendly_name: 'AI Assistant',
|
||||||
description: 'A web interface for interacting with local AI models served by Ollama',
|
powered_by: 'Open WebUI + Ollama',
|
||||||
|
display_order: 3,
|
||||||
|
description: 'Local AI chat that runs entirely on your hardware - no internet required',
|
||||||
icon: 'IconWand',
|
icon: 'IconWand',
|
||||||
container_image: 'ghcr.io/open-webui/open-webui:main',
|
container_image: 'ghcr.io/open-webui/open-webui:main',
|
||||||
container_command: null,
|
container_command: null,
|
||||||
|
|
@ -74,8 +80,10 @@ export default class ServiceSeeder extends BaseSeeder {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
service_name: DockerService.CYBERCHEF_SERVICE_NAME,
|
service_name: DockerService.CYBERCHEF_SERVICE_NAME,
|
||||||
friendly_name: 'CyberChef',
|
friendly_name: 'Data Tools',
|
||||||
description: 'The Cyber Swiss Army Knife - a web app for encryption, encoding, and data analysis',
|
powered_by: 'CyberChef',
|
||||||
|
display_order: 11,
|
||||||
|
description: 'Swiss Army knife for data encoding, encryption, and analysis',
|
||||||
icon: 'IconChefHat',
|
icon: 'IconChefHat',
|
||||||
container_image: 'ghcr.io/gchq/cyberchef:latest',
|
container_image: 'ghcr.io/gchq/cyberchef:latest',
|
||||||
container_command: null,
|
container_command: null,
|
||||||
|
|
@ -94,8 +102,10 @@ export default class ServiceSeeder extends BaseSeeder {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
service_name: DockerService.FLATNOTES_SERVICE_NAME,
|
service_name: DockerService.FLATNOTES_SERVICE_NAME,
|
||||||
friendly_name: 'FlatNotes',
|
friendly_name: 'Notes',
|
||||||
description: 'A simple note-taking app that stores all files locally',
|
powered_by: 'FlatNotes',
|
||||||
|
display_order: 10,
|
||||||
|
description: 'Simple note-taking app with local storage',
|
||||||
icon: 'IconNotes',
|
icon: 'IconNotes',
|
||||||
container_image: 'dullage/flatnotes:latest',
|
container_image: 'dullage/flatnotes:latest',
|
||||||
container_command: null,
|
container_command: null,
|
||||||
|
|
@ -116,8 +126,10 @@ export default class ServiceSeeder extends BaseSeeder {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
service_name: DockerService.KOLIBRI_SERVICE_NAME,
|
service_name: DockerService.KOLIBRI_SERVICE_NAME,
|
||||||
friendly_name: 'Kolibri',
|
friendly_name: 'Education Platform',
|
||||||
description: 'An offline-first education platform for schools and learners',
|
powered_by: 'Kolibri',
|
||||||
|
display_order: 2,
|
||||||
|
description: 'Interactive learning platform with video courses and exercises',
|
||||||
icon: 'IconSchool',
|
icon: 'IconSchool',
|
||||||
container_image: 'treehouses/kolibri:latest',
|
container_image: 'treehouses/kolibri:latest',
|
||||||
container_command: null,
|
container_command: null,
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,20 @@ import { getServiceLink } from '~/lib/navigation'
|
||||||
import { ServiceSlim } from '../../types/services'
|
import { ServiceSlim } from '../../types/services'
|
||||||
import DynamicIcon, { DynamicIconName } from '~/components/DynamicIcon'
|
import DynamicIcon, { DynamicIconName } from '~/components/DynamicIcon'
|
||||||
|
|
||||||
const STATIC_ITEMS = [
|
// Maps is a Core Capability (display_order: 4)
|
||||||
|
const MAPS_ITEM = {
|
||||||
|
label: 'Maps',
|
||||||
|
to: '/maps',
|
||||||
|
target: '',
|
||||||
|
description: 'View offline maps',
|
||||||
|
icon: <IconMapRoute size={48} />,
|
||||||
|
installed: true,
|
||||||
|
displayOrder: 4,
|
||||||
|
poweredBy: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
// System items shown after all apps
|
||||||
|
const SYSTEM_ITEMS = [
|
||||||
{
|
{
|
||||||
label: 'Easy Setup',
|
label: 'Easy Setup',
|
||||||
to: '/easy-setup',
|
to: '/easy-setup',
|
||||||
|
|
@ -22,6 +35,8 @@ const STATIC_ITEMS = [
|
||||||
'Not sure where to start? Use the setup wizard to quickly configure your N.O.M.A.D.!',
|
'Not sure where to start? Use the setup wizard to quickly configure your N.O.M.A.D.!',
|
||||||
icon: <IconBolt size={48} />,
|
icon: <IconBolt size={48} />,
|
||||||
installed: true,
|
installed: true,
|
||||||
|
displayOrder: 50,
|
||||||
|
poweredBy: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Install Apps',
|
label: 'Install Apps',
|
||||||
|
|
@ -30,6 +45,8 @@ const STATIC_ITEMS = [
|
||||||
description: 'Not seeing your favorite app? Install it here!',
|
description: 'Not seeing your favorite app? Install it here!',
|
||||||
icon: <IconPlus size={48} />,
|
icon: <IconPlus size={48} />,
|
||||||
installed: true,
|
installed: true,
|
||||||
|
displayOrder: 51,
|
||||||
|
poweredBy: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Docs',
|
label: 'Docs',
|
||||||
|
|
@ -38,14 +55,8 @@ const STATIC_ITEMS = [
|
||||||
description: 'Read Project N.O.M.A.D. manuals and guides',
|
description: 'Read Project N.O.M.A.D. manuals and guides',
|
||||||
icon: <IconHelp size={48} />,
|
icon: <IconHelp size={48} />,
|
||||||
installed: true,
|
installed: true,
|
||||||
},
|
displayOrder: 52,
|
||||||
{
|
poweredBy: null,
|
||||||
label: 'Maps',
|
|
||||||
to: '/maps',
|
|
||||||
target: '',
|
|
||||||
description: 'View offline maps',
|
|
||||||
icon: <IconMapRoute size={48} />,
|
|
||||||
installed: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
|
|
@ -54,33 +65,59 @@ const STATIC_ITEMS = [
|
||||||
description: 'Configure your N.O.M.A.D. settings',
|
description: 'Configure your N.O.M.A.D. settings',
|
||||||
icon: <IconSettings size={48} />,
|
icon: <IconSettings size={48} />,
|
||||||
installed: true,
|
installed: true,
|
||||||
|
displayOrder: 53,
|
||||||
|
poweredBy: null,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
interface DashboardItem {
|
||||||
|
label: string
|
||||||
|
to: string
|
||||||
|
target: string
|
||||||
|
description: string
|
||||||
|
icon: React.ReactNode
|
||||||
|
installed: boolean
|
||||||
|
displayOrder: number
|
||||||
|
poweredBy: string | null
|
||||||
|
}
|
||||||
|
|
||||||
export default function Home(props: {
|
export default function Home(props: {
|
||||||
system: {
|
system: {
|
||||||
services: ServiceSlim[]
|
services: ServiceSlim[]
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
const items = []
|
const items: DashboardItem[] = []
|
||||||
props.system.services.map((service) => {
|
|
||||||
items.push({
|
|
||||||
label: service.friendly_name || service.service_name,
|
|
||||||
to: service.ui_location ? getServiceLink(service.ui_location) : '#',
|
|
||||||
target: '_blank',
|
|
||||||
description:
|
|
||||||
service.description ||
|
|
||||||
`Access the ${service.friendly_name || service.service_name} application`,
|
|
||||||
icon: service.icon ? (
|
|
||||||
<DynamicIcon icon={service.icon as DynamicIconName} className="!size-12" />
|
|
||||||
) : (
|
|
||||||
<IconWifiOff size={48} />
|
|
||||||
),
|
|
||||||
installed: service.installed,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
items.push(...STATIC_ITEMS)
|
// Add installed services (non-dependency services only)
|
||||||
|
props.system.services
|
||||||
|
.filter((service) => service.installed && service.ui_location)
|
||||||
|
.forEach((service) => {
|
||||||
|
items.push({
|
||||||
|
label: service.friendly_name || service.service_name,
|
||||||
|
to: service.ui_location ? getServiceLink(service.ui_location) : '#',
|
||||||
|
target: '_blank',
|
||||||
|
description:
|
||||||
|
service.description ||
|
||||||
|
`Access the ${service.friendly_name || service.service_name} application`,
|
||||||
|
icon: service.icon ? (
|
||||||
|
<DynamicIcon icon={service.icon as DynamicIconName} className="!size-12" />
|
||||||
|
) : (
|
||||||
|
<IconWifiOff size={48} />
|
||||||
|
),
|
||||||
|
installed: service.installed,
|
||||||
|
displayOrder: service.display_order ?? 100,
|
||||||
|
poweredBy: service.powered_by ?? null,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add Maps as a Core Capability
|
||||||
|
items.push(MAPS_ITEM)
|
||||||
|
|
||||||
|
// Add system items
|
||||||
|
items.push(...SYSTEM_ITEMS)
|
||||||
|
|
||||||
|
// Sort all items by display order
|
||||||
|
items.sort((a, b) => a.displayOrder - b.displayOrder)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
|
|
@ -94,6 +131,9 @@ export default function Home(props: {
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center mb-2">{item.icon}</div>
|
<div className="flex items-center justify-center mb-2">{item.icon}</div>
|
||||||
<h3 className="font-bold text-2xl">{item.label}</h3>
|
<h3 className="font-bold text-2xl">{item.label}</h3>
|
||||||
|
{item.poweredBy && (
|
||||||
|
<p className="text-sm opacity-80">Powered by {item.poweredBy}</p>
|
||||||
|
)}
|
||||||
<p className="xl:text-lg mt-2">{item.description}</p>
|
<p className="xl:text-lg mt-2">{item.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -10,4 +10,6 @@ export type ServiceSlim = Pick<
|
||||||
| 'friendly_name'
|
| 'friendly_name'
|
||||||
| 'description'
|
| 'description'
|
||||||
| 'icon'
|
| 'icon'
|
||||||
|
| 'powered_by'
|
||||||
|
| 'display_order'
|
||||||
> & { status?: string }
|
> & { status?: string }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user