From 7acfd33d5c77e81ef6aa32e12838223cc3ab614c Mon Sep 17 00:00:00 2001 From: Jake Turner Date: Tue, 18 Nov 2025 15:48:43 -0800 Subject: [PATCH] feat: version footer and fix CI version handlng --- .github/workflows/docker.yml | 22 +----- admin/Dockerfile => Dockerfile | 10 ++- README.md | 7 +- admin/app/services/system_service.ts | 90 ++++++++++++++-------- admin/config/inertia.ts | 3 +- admin/inertia/components/Footer.tsx | 14 ++++ admin/inertia/components/StyledSidebar.tsx | 6 ++ admin/inertia/css/app.css | 3 +- admin/inertia/layouts/AppLayout.tsx | 3 + admin/inertia/layouts/SettingsLayout.tsx | 8 +- admin/types/system.ts | 5 ++ 11 files changed, 111 insertions(+), 60 deletions(-) rename admin/Dockerfile => Dockerfile (69%) create mode 100644 admin/inertia/components/Footer.tsx diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 00aac1e..fe9e552 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -9,22 +9,6 @@ on: type: string jobs: - debug: - name: Debugging information - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: List repository root contents - run: | - echo "Repository root contents:" - ls -la - echo "Looking for admin directory:" - ls -la admin/ || echo "admin directory not found" - - name: Print GitHub context - run: echo "${{ toJson(github) }}" - - name: Print workflow inputs - run: echo "${{ toJson(inputs) }}" check_authorization: name: Check authorization to publish new Docker image runs-on: ubuntu-latest @@ -54,9 +38,7 @@ jobs: - name: Build and push uses: docker/build-push-action@v5 with: - context: ./admin - file: ./admin/Dockerfile push: true tags: | - ghcr.io/crosstalk-solutions/project-nomad-admin:${{ inputs.version }} - ghcr.io/crosstalk-solutions/project-nomad-admin:latest + ghcr.io/crosstalk-solutions/project-nomad:${{ inputs.version }} + ghcr.io/crosstalk-solutions/project-nomad:latest diff --git a/admin/Dockerfile b/Dockerfile similarity index 69% rename from admin/Dockerfile rename to Dockerfile index c992904..3e77d9d 100644 --- a/admin/Dockerfile +++ b/Dockerfile @@ -6,20 +6,20 @@ RUN apk add --no-cache bash curl # All deps stage FROM base AS deps WORKDIR /app -ADD package.json package-lock.json ./ +ADD admin/package.json admin/package-lock.json ./ RUN npm ci # Production only deps stage FROM base AS production-deps WORKDIR /app -ADD package.json package-lock.json ./ +ADD admin/package.json admin/package-lock.json ./ RUN npm ci --omit=dev # Build stage FROM base AS build WORKDIR /app COPY --from=deps /app/node_modules /app/node_modules -ADD . . +ADD admin/ ./ RUN node ace build # Production stage @@ -28,6 +28,8 @@ ENV NODE_ENV=production WORKDIR /app COPY --from=production-deps /app/node_modules /app/node_modules COPY --from=build /app/build /app -COPY ./docs /app/docs +# Copy root package.json for version info +COPY package.json /app/version.json +COPY admin/docs /app/docs EXPOSE 8080 CMD ["node", "./bin/server.js"] \ No newline at end of file diff --git a/README.md b/README.md index 0c1e08b..49a08aa 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,12 @@ To test internet connectivity, N.O.M.A.D. attempts to make a request to Cloudfla ## About Security By design, Project N.O.M.A.D. is intended to be open and available without hurdles - it includes no authentication. If you decide to connect your device to a local network after install (e.g. for allowing other devices to access it's resources), you can block/open ports to control which services are exposed. -# Helper Scripts +## Versioning +This project uses semantic versioning. The version is managed in the root `package.json` +and automatically updated by semantic-release. For simplicity's sake, the "project-nomad" container +uses the same version defined there instead of the version in `admin/package.json` (stays at 0.0.0), as it's the only container derived from the code. + +## Helper Scripts Once installed, Project N.O.M.A.D. has a few helper scripts should you ever need to troubleshoot issues or perform maintenance that can't be done through the Command Center. All of these scripts are found in Project N.O.M.A.D.'s install directory, `/opt/project-nomad` ### diff --git a/admin/app/services/system_service.ts b/admin/app/services/system_service.ts index f18bd3f..2cc5e57 100644 --- a/admin/app/services/system_service.ts +++ b/admin/app/services/system_service.ts @@ -1,37 +1,39 @@ -import Service from "#models/service" -import { inject } from "@adonisjs/core"; -import { DockerService } from "#services/docker_service"; -import { ServiceSlim } from "../../types/services.js"; -import logger from "@adonisjs/core/services/logger"; -import si from 'systeminformation'; -import { SystemInformationResponse } from "../../types/system.js"; +import Service from '#models/service' +import { inject } from '@adonisjs/core' +import { DockerService } from '#services/docker_service' +import { ServiceSlim } from '../../types/services.js' +import logger from '@adonisjs/core/services/logger' +import si from 'systeminformation' +import { SystemInformationResponse } from '../../types/system.js' +import { readFileSync } from 'fs' +import { join } from 'path' @inject() export class SystemService { - constructor( - private dockerService: DockerService - ) { } - async getServices({ - installedOnly = true, - }: { - installedOnly?: boolean - }): Promise { - const query = Service.query().orderBy('friendly_name', 'asc').select('id', 'service_name', 'installed', 'ui_location', 'friendly_name', 'description').where('is_dependency_service', false) + private static appVersion: string | null = null + + constructor(private dockerService: DockerService) {} + + async getServices({ installedOnly = true }: { installedOnly?: boolean }): Promise { + 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); + query.where('installed', true) } - const services = await query; + const services = await query if (!services || services.length === 0) { - return []; + return [] } - const statuses = await this.dockerService.getServicesStatus(); + const statuses = await this.dockerService.getServicesStatus() - const toReturn: ServiceSlim[] = []; + const toReturn: ServiceSlim[] = [] for (const service of services) { - const status = statuses.find(s => s.service_name === service.service_name); + const status = statuses.find((s) => s.service_name === service.service_name) toReturn.push({ id: service.id, service_name: service.service_name, @@ -39,12 +41,36 @@ export class SystemService { description: service.description, installed: service.installed, status: status ? status.status : 'unknown', - ui_location: service.ui_location || '' - }); + ui_location: service.ui_location || '', + }) } - return toReturn; + return toReturn + } + static getAppVersion(): string { + try { + if (this.appVersion) { + return this.appVersion + } + + // Return 'dev' for development environment (version.json won't exist) + if (process.env.NODE_ENV === 'development') { + this.appVersion = 'dev' + return 'dev' + } + + const packageJson = readFileSync(join(process.cwd(), 'version.json'), 'utf-8') + const packageData = JSON.parse(packageJson) + + const version = packageData.version || '0.0.0' + + this.appVersion = version + return version + } catch (error) { + logger.error('Error getting app version:', error) + return '0.0.0' + } } async getSystemInfo(): Promise { @@ -53,18 +79,18 @@ export class SystemService { si.cpu(), si.mem(), si.osInfo(), - si.diskLayout() - ]);; + si.diskLayout(), + ]) return { cpu, mem, os, - disk - }; + disk, + } } catch (error) { - logger.error('Error getting system info:', error); - return undefined; + logger.error('Error getting system info:', error) + return undefined } } -} \ No newline at end of file +} diff --git a/admin/config/inertia.ts b/admin/config/inertia.ts index 55ae6c7..2bedb92 100644 --- a/admin/config/inertia.ts +++ b/admin/config/inertia.ts @@ -1,3 +1,4 @@ +import { SystemService } from '#services/system_service' import { defineConfig } from '@adonisjs/inertia' import type { InferSharedProps } from '@adonisjs/inertia/types' @@ -11,7 +12,7 @@ const inertiaConfig = defineConfig({ * Data that should be shared with all rendered pages */ sharedData: { - // user: (ctx) => ctx.inertia.always(() => ctx.auth.user), + appVersion: () => SystemService.getAppVersion(), }, /** diff --git a/admin/inertia/components/Footer.tsx b/admin/inertia/components/Footer.tsx new file mode 100644 index 0000000..0ed8507 --- /dev/null +++ b/admin/inertia/components/Footer.tsx @@ -0,0 +1,14 @@ +import { usePage } from '@inertiajs/react' + +export default function Footer() { + const { appVersion } = usePage().props as unknown as { appVersion: string } + return ( +
+
+

+ Project N.O.M.A.D. Command Center v{appVersion} +

+
+
+ ) +} diff --git a/admin/inertia/components/StyledSidebar.tsx b/admin/inertia/components/StyledSidebar.tsx index f15ed9d..23bae9f 100644 --- a/admin/inertia/components/StyledSidebar.tsx +++ b/admin/inertia/components/StyledSidebar.tsx @@ -3,6 +3,8 @@ import { Dialog, DialogBackdrop, DialogPanel, TransitionChild } from '@headlessu import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline' import classNames from '~/lib/classNames' import { IconArrowLeft } from '@tabler/icons-react' +import { usePage } from '@inertiajs/react' +import { UsePageProps } from '../../types/system' type SidebarItem = { name: string @@ -19,6 +21,7 @@ interface StyledSidebarProps { const StyledSidebar: React.FC = ({ title, items }) => { const [sidebarOpen, setSidebarOpen] = useState(false) + const { appVersion } = usePage().props as unknown as UsePageProps const currentPath = useMemo(() => { if (typeof window === 'undefined') return '' @@ -72,6 +75,9 @@ const StyledSidebar: React.FC = ({ title, items }) => { +
+

Project N.O.M.A.D. Command Center v{appVersion}

+
) } diff --git a/admin/inertia/css/app.css b/admin/inertia/css/app.css index 9907589..ff456f0 100644 --- a/admin/inertia/css/app.css +++ b/admin/inertia/css/app.css @@ -5,7 +5,8 @@ --color-desert-green-light: #babaaa; --color-desert-green: #424420; --color-desert-orange: #a84a12; - --color-desert-sand: #f7eedc + --color-desert-sand: #f7eedc; + /* --color-desert-sand: #E2DAC2; */ } body { diff --git a/admin/inertia/layouts/AppLayout.tsx b/admin/inertia/layouts/AppLayout.tsx index ecb6da3..86117d3 100644 --- a/admin/inertia/layouts/AppLayout.tsx +++ b/admin/inertia/layouts/AppLayout.tsx @@ -1,3 +1,5 @@ +import Footer from "~/components/Footer"; + export default function AppLayout({ children }: { children: React.ReactNode }) { return (
@@ -17,6 +19,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) { className="text-desert-orange font-semibold text-sm italic" >A project by Crosstalk Solutions

*/} +