feat(Services): friendly names and descriptions

This commit is contained in:
Jake Turner 2025-11-18 14:02:22 -08:00
parent c66839655a
commit b8eaaa7ac6
No known key found for this signature in database
GPG Key ID: 694BC38EF2ED4844
8 changed files with 97 additions and 10 deletions

View File

@ -20,6 +20,12 @@ export default class Service extends BaseModel {
@column()
declare container_config: string | null
@column()
declare friendly_name: string | null
@column()
declare description: string | null
@column({
serialize(value) {
return Boolean(value)

View File

@ -16,7 +16,7 @@ export class SystemService {
}: {
installedOnly?: boolean
}): Promise<ServiceSlim[]> {
const query = Service.query().orderBy('service_name', 'asc').select('id', 'service_name', 'installed', 'ui_location').where('is_dependency_service', false)
const query = Service.query().orderBy('friendly_name', 'asc').select('id', 'service_name', 'installed', 'ui_location', 'friendly_name', 'description').where('is_dependency_service', false)
if (installedOnly) {
query.where('installed', true);
}
@ -35,6 +35,8 @@ export class SystemService {
toReturn.push({
id: service.id,
service_name: service.service_name,
friendly_name: service.friendly_name,
description: service.description,
installed: service.installed,
status: status ? status.status : 'unknown',
ui_location: service.ui_location || ''

View File

@ -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('friendly_name').nullable()
table.string('description').nullable()
})
}
async down() {
this.schema.alterTable(this.tableName, (table) => {
table.dropColumn('friendly_name')
table.dropColumn('description')
})
}
}

View File

@ -7,6 +7,8 @@ export default class ServiceSeeder extends BaseSeeder {
private static DEFAULT_SERVICES: Omit<ModelAttributes<Service>, 'created_at' | 'updated_at' | 'metadata' | 'id'>[] = [
{
service_name: DockerService.KIWIX_SERVICE_NAME,
friendly_name: 'Kiwix',
description: 'Offline Wikipedia, eBooks, and more',
container_image: 'ghcr.io/kiwix/kiwix-serve',
container_command: '*.zim --address=0.0.0.0',
container_config: JSON.stringify({
@ -24,6 +26,8 @@ export default class ServiceSeeder extends BaseSeeder {
},
{
service_name: DockerService.OPENSTREETMAP_SERVICE_NAME,
friendly_name: 'OpenStreetMap Tile Server',
description: 'Self-hosted OpenStreetMap tile server',
container_image: 'overv/openstreetmap-tile-server',
container_command: 'run',
container_config: JSON.stringify({
@ -43,6 +47,8 @@ export default class ServiceSeeder extends BaseSeeder {
},
{
service_name: DockerService.OLLAMA_SERVICE_NAME,
friendly_name: 'Ollama',
description: 'Run local LLMs (AI models) with ease on your own hardware',
container_image: 'ollama/ollama:latest',
container_command: 'serve',
container_config: JSON.stringify({
@ -60,6 +66,8 @@ export default class ServiceSeeder extends BaseSeeder {
},
{
service_name: DockerService.OPEN_WEBUI_SERVICE_NAME,
friendly_name: 'Open WebUI',
description: 'A web interface for interacting with local AI models served by Ollama',
container_image: 'ghcr.io/open-webui/open-webui:main',
container_command: null,
container_config: JSON.stringify({
@ -77,6 +85,8 @@ export default class ServiceSeeder extends BaseSeeder {
},
{
service_name: DockerService.CYBERCHEF_SERVICE_NAME,
friendly_name: 'CyberChef',
description: 'The Cyber Swiss Army Knife - a web app for encryption, encoding, and data analysis',
container_image: 'ghcr.io/gchq/cyberchef:latest',
container_command: null,
container_config: JSON.stringify({
@ -93,6 +103,8 @@ export default class ServiceSeeder extends BaseSeeder {
},
{
service_name: DockerService.FLATNOTES_SERVICE_NAME,
friendly_name: 'FlatNotes',
description: 'A simple note-taking app that stores all files locally',
container_image: 'dullage/flatnotes:latest',
container_command: null,
container_config: JSON.stringify({

View File

@ -61,7 +61,7 @@ function StyledTable<T extends { [key: string]: any }>({
<th
key={index}
className={classNames(
'whitespace-nowrap text-left text-sm font-semibold text-gray-900',
'whitespace-nowrap text-left font-semibold text-gray-900',
compact ? `${leftPadding} py-2` : `${leftPadding} py-4 pr-3`
)}
>

View File

@ -14,6 +14,7 @@ import LoadingSpinner from '~/components/LoadingSpinner'
import useErrorNotification from '~/hooks/useErrorNotification'
import useInternetStatus from '~/hooks/useInternetStatus'
import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'
import { IconCheck } from '@tabler/icons-react'
export default function SettingsPage(props: { system: { services: ServiceSlim[] } }) {
const { openModal, closeAllModals } = useModals()
@ -64,12 +65,13 @@ export default function SettingsPage(props: { system: { services: ServiceSlim[]
open={true}
confirmText="Install"
cancelText="Cancel"
confirmVariant='primary'
confirmVariant="primary"
icon={<ArrowDownTrayIcon className="h-12 w-12 text-desert-green" />}
>
<p className="text-gray-700">
Are you sure you want to install {service.service_name}? This will start the service and
make it available in your Project N.O.M.A.D. instance. It may take some time to complete.
Are you sure you want to install {service.friendly_name || service.service_name}? This
will start the service and make it available in your Project N.O.M.A.D. instance. It may
take some time to complete.
</p>
</StyledModal>,
'install-service-modal'
@ -220,10 +222,21 @@ export default function SettingsPage(props: { system: { services: ServiceSlim[]
className="font-semibold"
rowLines={true}
columns={[
{ accessor: 'service_name', title: 'Name' },
{
accessor: 'friendly_name',
title: 'Name',
render(record) {
return (
<div className='flex flex-col'>
<p>{record.friendly_name || record.service_name}</p>
<p className="text-sm text-gray-500">{record.description}</p>
</div>
)
},
},
{
accessor: 'ui_location',
title: 'Location',
title: 'Port',
render: (record) => (
<a
href={getServiceLink(record.ui_location || 'unknown')}
@ -237,8 +250,8 @@ export default function SettingsPage(props: { system: { services: ServiceSlim[]
},
{
accessor: 'installed',
title: 'Installed?',
render: (record) => (record.installed ? 'Yes' : 'No'),
title: 'Installed',
render: (record) => (record.installed ? <IconCheck className="h-6 w-6 text-desert-green" /> : ''),
},
{
accessor: 'actions',

View File

@ -107,6 +107,7 @@
"resolved": "https://registry.npmjs.org/@adonisjs/application/-/application-8.4.1.tgz",
"integrity": "sha512-2vwO/8DoKJ9AR4Vvllz08RcomBoETc3FMf+q+ri1BVVjc76tLGV3KcYZp8+uKOuEreiK6poQ7NwJrR1P5ANA/w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@poppinss/hooks": "^7.2.5",
"@poppinss/macroable": "^1.0.4",
@ -128,6 +129,7 @@
"integrity": "sha512-csLdMW58cwuRjdPEDE0dqwHZCT5snCh+1sQ19HPnQ/BLKPPAvQdDRdw0atoC8LVmouB8ghXVHp3SxnVxlvXYWQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@adonisjs/env": "^6.1.0",
"@antfu/install-pkg": "^0.4.1",
@ -222,6 +224,7 @@
"resolved": "https://registry.npmjs.org/@adonisjs/config/-/config-5.0.3.tgz",
"integrity": "sha512-dO7gkYxZsrsnR8n7d5KUpyi+Q5c6BnV2rmFDqEmEjz5AkOZLLzJJJbeHgMb+M27le7ifEUoa8MRu6RED8NMsJg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@poppinss/utils": "^6.9.4"
},
@ -234,6 +237,7 @@
"resolved": "https://registry.npmjs.org/@adonisjs/core/-/core-6.19.0.tgz",
"integrity": "sha512-qwGuapvMLYPna89Qji/MuD9xx6qqcqc/aLrSGgoFbOzBmd8Ycc9391w7sFrrGuJpHiNLBmf1NJsY3YS2AwyX0A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@adonisjs/ace": "^13.3.0",
"@adonisjs/application": "^8.4.1",
@ -366,6 +370,7 @@
"resolved": "https://registry.npmjs.org/@adonisjs/encryption/-/encryption-6.0.2.tgz",
"integrity": "sha512-37XqVPsZi6zXMbC0Me1/qlcTP0uE+KAtYOFx7D7Tvtz377NL/6gqxqgpW/BopgOSD+CVDXjzO/Wx3M2UrbkJRQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@poppinss/utils": "^6.7.3"
},
@ -458,6 +463,7 @@
"resolved": "https://registry.npmjs.org/@adonisjs/fold/-/fold-10.2.0.tgz",
"integrity": "sha512-VDBGrVz2viaCsmONLKYpMMeP3ds+fw+7kofeF/z9ic6cB3d7BLEB8VcIdGkfY0FCBbLK2Btee1tNPuUF1uMlmQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@poppinss/utils": "^7.0.0-next.1",
"parse-imports": "^2.2.1"
@ -524,6 +530,7 @@
"resolved": "https://registry.npmjs.org/@adonisjs/http-server/-/http-server-7.7.0.tgz",
"integrity": "sha512-qW1wsp7f1BqRO2qmJ8laUaq8vnLjEvhgkMusLEa2ju6RBMMsph5w3cEDTXAwQO8fSSqNXmRTzPRQ1lUm/FXq0A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@paralleldrive/cuid2": "^2.2.2",
"@poppinss/macroable": "^1.0.4",
@ -592,6 +599,7 @@
"resolved": "https://registry.npmjs.org/@adonisjs/logger/-/logger-6.0.6.tgz",
"integrity": "sha512-r5mLmmklSezzu3cu9QaXle2/gPNrgKpiIo+utYlwV3ITsW5JeIX/xcwwMTNM/9f1zU+SwOj5NccPTEFD3feRaw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@poppinss/utils": "^6.9.2",
"abstract-logging": "^2.0.1",
@ -606,6 +614,7 @@
"resolved": "https://registry.npmjs.org/@adonisjs/lucid/-/lucid-21.6.1.tgz",
"integrity": "sha512-0TLCcPm9GHShJlsDAF5SHilafnvTxW25y5nD3bGJBSMEaNfGXcGRBbnyWoeNs7DsnqMCZ6ociT+0XMcKJWzQrQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@adonisjs/presets": "^2.6.4",
"@faker-js/faker": "^9.6.0",
@ -689,6 +698,7 @@
"resolved": "https://registry.npmjs.org/@adonisjs/session/-/session-7.5.1.tgz",
"integrity": "sha512-b1E0W/1nnJfAq3Gv8yPywgsxJ7uzzOBJxxulonXI4t1eSdvJzZGNrFScfVLOcjTwlxwrEFA847tULIQxgR4Spw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@poppinss/macroable": "^1.0.4",
"@poppinss/utils": "^6.9.2"
@ -731,6 +741,7 @@
"resolved": "https://registry.npmjs.org/@adonisjs/shield/-/shield-8.2.0.tgz",
"integrity": "sha512-RddRbs92y87GGFUgDSWD/Pg7qYHh8+MctUphFZwtbTblvDckrjZxuYyp+vmVATPuvDvK7sOlatuZHT4HQSz9zQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@poppinss/utils": "^6.9.2",
"csrf": "^3.1.0",
@ -809,6 +820,7 @@
"resolved": "https://registry.npmjs.org/@adonisjs/vite/-/vite-4.0.0.tgz",
"integrity": "sha512-5kdE0qLIm2dj+XO0HiCohmh3tfZ+X468wkNXErQ15VS0fkDjZWns2VuiYpoToTKd4tdX/oGlpnpd8aIwAGB4ow==",
"license": "MIT",
"peer": true,
"dependencies": {
"@poppinss/utils": "^6.8.3",
"@vavite/multibuild": "^5.1.0",
@ -907,6 +919,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.7.tgz",
"integrity": "sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@ -2198,6 +2211,7 @@
"integrity": "sha512-M2LUtHhKr4KgBfX73tDHNCD1IOmcXp9dvC+AinmRxsggIFnarsClcfjT/sXc3uNzjZW7Lk31LvcH76AxJHBmJQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.16.0"
},
@ -2226,6 +2240,7 @@
"integrity": "sha512-e3BFn1rca/OTiagilkmRTrLVhl00iC/LrY5j4Ns/VZDONYHs9BKAbHaImxjD1zoHMEhwQEF+ce7fgMO/BK+lfg==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@japa/core": "^10.3.0",
"@japa/errors-printer": "^4.1.2",
@ -3136,6 +3151,7 @@
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.21"
@ -3694,6 +3710,7 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz",
"integrity": "sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/query-core": "5.83.0"
},
@ -3963,6 +3980,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.33.tgz",
"integrity": "sha512-wzoocdnnpSxZ+6CjW4ADCK1jVmd1S/J3ArNWfn8FDDQtRm8dkDg7TA+mvek2wNrfCgwuZxqEOiB9B1XCJ6+dbw==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@ -3992,6 +4010,7 @@
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@ -4085,6 +4104,7 @@
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.35.0",
"@typescript-eslint/types": "8.35.0",
@ -4313,6 +4333,7 @@
"resolved": "https://registry.npmjs.org/@vinejs/vine/-/vine-3.0.1.tgz",
"integrity": "sha512-ZtvYkYpZOYdvbws3uaOAvTFuvFXoQGAtmzeiXu+XSMGxi5GVsODpoI9Xu9TplEMuD/5fmAtBbKb9cQHkWkLXDQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@poppinss/macroable": "^1.0.4",
"@types/validator": "^13.12.2",
@ -4392,6 +4413,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -4749,6 +4771,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001726",
"electron-to-chromium": "^1.5.173",
@ -5717,6 +5740,7 @@
"resolved": "https://registry.npmjs.org/edge.js/-/edge.js-6.2.1.tgz",
"integrity": "sha512-me875zh6YA0V429hywgQIpHgMvQkondv5XHaP6EsL2yIBpLcBWCl7Ba1cai0SwYhp8iD0IyV3KjpxLrnW7S2Ag==",
"license": "MIT",
"peer": true,
"dependencies": {
"@poppinss/inspect": "^1.0.1",
"@poppinss/macroable": "^1.0.4",
@ -5987,6 +6011,7 @@
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@ -6048,6 +6073,7 @@
"integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@ -8165,6 +8191,7 @@
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz",
"integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
}
@ -9062,6 +9089,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@ -9119,6 +9147,7 @@
"integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@ -9424,6 +9453,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -9448,6 +9478,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@ -9496,6 +9527,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@ -10892,6 +10924,7 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -11084,6 +11117,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@ -11282,6 +11316,7 @@
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
"license": "ISC",
"peer": true,
"bin": {
"yaml": "bin.mjs"
},

View File

@ -2,4 +2,4 @@ import Service from "#models/service";
export type ServiceStatus = 'unknown' | 'running' | 'stopped';
export type ServiceSlim = Pick<Service, 'id' | 'service_name' | 'installed' | 'ui_location'> & { status?: ServiceStatus };
export type ServiceSlim = Pick<Service, 'id' | 'service_name' | 'installed' | 'ui_location' | 'friendly_name' | 'description'> & { status?: ServiceStatus };