feat: version footer and fix CI version handlng

This commit is contained in:
Jake Turner 2025-11-18 15:48:43 -08:00
parent 64b874b1f3
commit 7acfd33d5c
No known key found for this signature in database
GPG Key ID: 694BC38EF2ED4844
11 changed files with 111 additions and 60 deletions

View File

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

View File

@ -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"]

View File

@ -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`
###

View File

@ -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<ServiceSlim[]> {
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<ServiceSlim[]> {
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<SystemInformationResponse | undefined> {
@ -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
}
}
}
}

View File

@ -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(),
},
/**

View File

@ -0,0 +1,14 @@
import { usePage } from '@inertiajs/react'
export default function Footer() {
const { appVersion } = usePage().props as unknown as { appVersion: string }
return (
<footer className="">
<div className="flex justify-center border-t border-gray-900/10 py-4">
<p className="text-sm/6 text-gray-600">
Project N.O.M.A.D. Command Center v{appVersion}
</p>
</div>
</footer>
)
}

View File

@ -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<StyledSidebarProps> = ({ 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<StyledSidebarProps> = ({ title, items }) => {
</li>
</ul>
</nav>
<div className="mb-4 text-center text-sm text-gray-600">
<p>Project N.O.M.A.D. Command Center v{appVersion}</p>
</div>
</div>
)
}

View File

@ -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 {

View File

@ -1,3 +1,5 @@
import Footer from "~/components/Footer";
export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen flex flex-col">
@ -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</p>
</div> */}
<Footer />
</div>
)
}

View File

@ -11,7 +11,13 @@ import { getServiceLink } from '~/lib/navigation'
const navigation = [
{ name: 'Apps', href: '/settings/apps', icon: CommandLineIcon, current: false },
{ name: 'Legal Notices', href: '/settings/legal', icon: IconGavel, current: false },
{ name: 'Service Logs & Metrics', href: getServiceLink('9999'), icon: IconDashboard, current: false, target: '_blank' },
{
name: 'Service Logs & Metrics',
href: getServiceLink('9999'),
icon: IconDashboard,
current: false,
target: '_blank',
},
{ name: 'ZIM Manager', href: '/settings/zim', icon: FolderIcon, current: false },
{
name: 'Zim Remote Explorer',

View File

@ -6,4 +6,9 @@ export type SystemInformationResponse = {
mem: Systeminformation.MemData
os: Systeminformation.OsData
disk: Systeminformation.DiskLayoutData[]
}
// Type inferrence is not working properly with usePage and shared props, so we define this type manually
export type UsePageProps = {
appVersion: string
}