From f37ff30598ab1aef0dcf4779ae6515a4d995bfc5 Mon Sep 17 00:00:00 2001 From: 9qeklajc Date: Sat, 16 May 2026 14:51:58 +0200 Subject: [PATCH 1/2] display version --- ui/components/add-provider-model-dialog.tsx | 2 +- ui/components/app-page-shell.tsx | 11 +- ui/components/version-status.tsx | 281 ++++++++++++++++++++ ui/lib/utils/version.ts | 72 +++++ 4 files changed, 362 insertions(+), 4 deletions(-) create mode 100644 ui/components/version-status.tsx create mode 100644 ui/lib/utils/version.ts diff --git a/ui/components/add-provider-model-dialog.tsx b/ui/components/add-provider-model-dialog.tsx index 9407d39f..c9f6984d 100644 --- a/ui/components/add-provider-model-dialog.tsx +++ b/ui/components/add-provider-model-dialog.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; diff --git a/ui/components/app-page-shell.tsx b/ui/components/app-page-shell.tsx index 0f2e7026..29b97736 100644 --- a/ui/components/app-page-shell.tsx +++ b/ui/components/app-page-shell.tsx @@ -21,6 +21,7 @@ import { adminLogout } from '@/lib/api/services/auth'; import { Button } from '@/components/ui/button'; import { CurrencyToggle } from '@/components/currency-toggle'; import { ThemeToggle } from '@/components/theme-toggle'; +import { VersionStatus } from '@/components/version-status'; import { Sheet, SheetClose, @@ -110,6 +111,7 @@ export function AppPageShell({

Routstr Node

+ + + +
+
+
+ + Node Version +
+

+ {statusDescription} +

+
+ +
+ +
+
+
+ + Current + + {versionLabel} +
+ {currentVersion?.commit ? ( +
+ + Commit + + + {currentVersion.commit} + +
+ ) : null} +
+ +
+
+ + Latest release + + + {releaseQuery.isLoading + ? 'loading…' + : releaseQuery.isError + ? 'unavailable' + : releaseRateLimited + ? 'rate-limited' + : (releaseQuery.data?.tag_name ?? 'unknown')} + +
+ {releaseQuery.data?.published_at ? ( +
+ + Published + + + {formatReleaseDate(releaseQuery.data.published_at)} + +
+ ) : null} +
+ + {releaseQuery.isError ? ( +

+ Failed to fetch latest release from GitHub. +

+ ) : releaseRateLimited ? ( +

+ GitHub rate limit reached. Try again later. +

+ ) : null} +
+ +
+ + View release changelog + + +
+
+ + ); +} diff --git a/ui/lib/utils/version.ts b/ui/lib/utils/version.ts new file mode 100644 index 00000000..66b8c25d --- /dev/null +++ b/ui/lib/utils/version.ts @@ -0,0 +1,72 @@ +export interface ParsedVersion { + raw: string; + base: string; + parts: readonly number[]; + commit: string | null; +} + +export type StatusKind = + | 'unknown' + | 'outdated' + | 'ahead' + | 'commit-drift' + | 'current'; + +export function parseVersion( + raw: string | undefined | null +): ParsedVersion | null { + if (!raw) return null; + const trimmed = raw.trim().replace(/^v/i, ''); + if (!trimmed) return null; + const [base, commitPart] = trimmed.split('+', 2); + const parts = (base ?? '') + .split('.') + .map((segment) => Number.parseInt(segment, 10)) + .filter((value) => Number.isFinite(value)); + if (parts.length === 0) return null; + return { + raw, + base: base ?? '', + parts, + commit: commitPart ?? null, + }; +} + +export function compareVersionParts( + a: readonly number[], + b: readonly number[] +): number { + const length = Math.max(a.length, b.length); + for (let i = 0; i < length; i += 1) { + const diff = (a[i] ?? 0) - (b[i] ?? 0); + if (diff !== 0) return diff; + } + return 0; +} + +export function deriveStatus( + current: ParsedVersion | null, + latest: ParsedVersion | null +): StatusKind { + if (!current || !latest) return 'unknown'; + const cmp = compareVersionParts(current.parts, latest.parts); + if (cmp < 0) return 'outdated'; + if (cmp > 0) return 'ahead'; + return current.commit ? 'commit-drift' : 'current'; +} + +export function formatReleaseDate(iso: string | undefined): string | null { + if (!iso) return null; + const date = new Date(iso); + if (Number.isNaN(date.getTime())) return null; + return date.toLocaleDateString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + }); +} + +export function formatVersionLabel(version: ParsedVersion | null): string { + if (!version) return 'unknown'; + return version.raw.startsWith('v') ? version.raw : `v${version.raw}`; +} From 89b84392a4a6f79510d1d8ab8d5537922abb0686 Mon Sep 17 00:00:00 2001 From: 9qeklajc Date: Sat, 16 May 2026 15:40:58 +0200 Subject: [PATCH 2/2] lint --- ui/components/version-status.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/ui/components/version-status.tsx b/ui/components/version-status.tsx index 0aee5382..5151c11f 100644 --- a/ui/components/version-status.tsx +++ b/ui/components/version-status.tsx @@ -77,12 +77,14 @@ function pickColorClass(status: StatusKind): string { return 'text-emerald-600 dark:text-emerald-400'; } -function pickIcon(status: StatusKind) { - if (status === 'outdated') return AlertTriangleIcon; +function renderStatusIcon(status: StatusKind, className: string) { + if (status === 'outdated') { + return ; + } if (status === 'commit-drift' || status === 'ahead' || status === 'unknown') { - return InfoIcon; + return ; } - return CheckCircle2Icon; + return ; } function describeStatus(status: StatusKind): string { @@ -138,7 +140,6 @@ export function VersionStatus({ }; const colorClass = pickColorClass(status); - const StatusIcon = pickIcon(status); const versionLabel = currentVersion ? formatVersionLabel(currentVersion) : nodeQuery.isLoading @@ -168,7 +169,7 @@ export function VersionStatus({ title='View version details' aria-label={ariaLabel} > - + {renderStatusIcon(status, 'h-3 w-3 shrink-0')} {versionLabel} @@ -181,7 +182,7 @@ export function VersionStatus({
- + {renderStatusIcon(status, cn('h-4 w-4 shrink-0', colorClass))} Node Version