diff --git a/admin/app/models/service.ts b/admin/app/models/service.ts index 35c08f5..8d9a619 100644 --- a/admin/app/models/service.ts +++ b/admin/app/models/service.ts @@ -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) diff --git a/admin/app/services/system_service.ts b/admin/app/services/system_service.ts index 7e5305c..f18bd3f 100644 --- a/admin/app/services/system_service.ts +++ b/admin/app/services/system_service.ts @@ -16,7 +16,7 @@ export class SystemService { }: { installedOnly?: boolean }): Promise { - 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 || '' diff --git a/admin/database/migrations/1763499145832_update_services_table.ts b/admin/database/migrations/1763499145832_update_services_table.ts new file mode 100644 index 0000000..edcb622 --- /dev/null +++ b/admin/database/migrations/1763499145832_update_services_table.ts @@ -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') + }) + } +} \ No newline at end of file diff --git a/admin/database/seeders/service_seeder.ts b/admin/database/seeders/service_seeder.ts index 5a64104..623932a 100644 --- a/admin/database/seeders/service_seeder.ts +++ b/admin/database/seeders/service_seeder.ts @@ -7,6 +7,8 @@ export default class ServiceSeeder extends BaseSeeder { private static DEFAULT_SERVICES: Omit, '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({ diff --git a/admin/inertia/components/StyledTable.tsx b/admin/inertia/components/StyledTable.tsx index 2c918aa..e24c661 100644 --- a/admin/inertia/components/StyledTable.tsx +++ b/admin/inertia/components/StyledTable.tsx @@ -61,7 +61,7 @@ function StyledTable({ diff --git a/admin/inertia/pages/settings/apps.tsx b/admin/inertia/pages/settings/apps.tsx index 048a87c..0d5c73d 100644 --- a/admin/inertia/pages/settings/apps.tsx +++ b/admin/inertia/pages/settings/apps.tsx @@ -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={} >

- 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.

, '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 ( +
+

{record.friendly_name || record.service_name}

+

{record.description}

+
+ ) + }, + }, { accessor: 'ui_location', - title: 'Location', + title: 'Port', render: (record) => ( (record.installed ? 'Yes' : 'No'), + title: 'Installed', + render: (record) => (record.installed ? : ''), }, { accessor: 'actions', diff --git a/admin/package-lock.json b/admin/package-lock.json index 3dc258d..fb8bdae 100644 --- a/admin/package-lock.json +++ b/admin/package-lock.json @@ -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" }, diff --git a/admin/types/services.ts b/admin/types/services.ts index d9df282..4b4bb70 100644 --- a/admin/types/services.ts +++ b/admin/types/services.ts @@ -2,4 +2,4 @@ import Service from "#models/service"; export type ServiceStatus = 'unknown' | 'running' | 'stopped'; -export type ServiceSlim = Pick & { status?: ServiceStatus }; \ No newline at end of file +export type ServiceSlim = Pick & { status?: ServiceStatus }; \ No newline at end of file