mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-18 22:36:19 +00:00
fix(fleet): resolve version detection pipeline for Docker builds (#402)
* fix(fleet): resolve version detection pipeline for Docker builds The Dockerfile backend-builder stage was missing a COPY of the root package.json, causing generate-version.js to fall back to "0.0.0-dev" at build time. At runtime, the filesystem walk also failed (root package.json not in the final image), producing the string "unknown" which the frontend rendered as "vunknown". Changes: - Dockerfile: copy root package.json into backend-builder stage - CapabilityRegistry: return null (not "unknown") for unresolvable versions; add isValidVersion() type guard; normalize remote meta responses to strip "unknown"/"0.0.0-dev" sentinel values - Fleet endpoints: hoist gateway version validation outside per-node loops; treat unresolvable remote versions as "potentially outdated" instead of silently marking them up to date - FleetView: guard all version display points (card badge, update button, gateway label, modal columns) via shared formatVersion() - EditorLayout, CapabilityGate: use shared isValidVersion utility - New frontend/src/lib/version.ts shared utility - Docs: add troubleshooting section for version display edge cases - Screenshots: updated Fleet Overview and Node Updates modal * docs: update fleet node updates screenshot with live remote node
This commit is contained in:
@@ -2,6 +2,7 @@ import { type ReactNode } from 'react';
|
||||
import { Unplug } from 'lucide-react';
|
||||
import { useNodes } from '@/context/NodeContext';
|
||||
import type { Capability } from '@/lib/capabilities';
|
||||
import { isValidVersion } from '@/lib/version';
|
||||
|
||||
interface CapabilityGateProps {
|
||||
capability: Capability;
|
||||
@@ -15,8 +16,7 @@ export function CapabilityGate({ capability, featureName = 'This feature', child
|
||||
if (hasCapability(capability)) return <>{children}</>;
|
||||
|
||||
const nodeName = activeNode?.name ?? 'this node';
|
||||
const hasValidVersion = activeNodeMeta?.version && activeNodeMeta.version !== 'unknown' && activeNodeMeta.version !== '0.0.0-dev';
|
||||
const versionHint = hasValidVersion
|
||||
const versionHint = isValidVersion(activeNodeMeta?.version)
|
||||
? `${nodeName} is running v${activeNodeMeta.version}`
|
||||
: `${nodeName} does not support this capability`;
|
||||
|
||||
@@ -28,7 +28,7 @@ export function CapabilityGate({ capability, featureName = 'This feature', child
|
||||
<div className="absolute inset-0 flex items-start justify-center pt-8">
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-muted/80 border border-border text-muted-foreground text-xs">
|
||||
<Unplug className="w-3 h-3" strokeWidth={1.5} />
|
||||
{featureName} is not available — {versionHint}
|
||||
{featureName} is not available: {versionHint}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,7 @@ import { LabelPill, LabelDot, type Label as StackLabel } from './LabelPill';
|
||||
import { LabelAssignPopover } from './LabelAssignPopover';
|
||||
import { UserProfileDropdown } from './UserProfileDropdown';
|
||||
import { apiFetch, fetchForNode } from '@/lib/api';
|
||||
import { isValidVersion } from '@/lib/version';
|
||||
import { toast } from '@/components/ui/toast-store';
|
||||
import { Label } from './ui/label';
|
||||
import { Command, CommandInput, CommandList, CommandItem } from './ui/command';
|
||||
@@ -1350,7 +1351,7 @@ export default function EditorLayout() {
|
||||
node.status === 'offline' ? 'bg-red-500' : 'bg-gray-400'
|
||||
}`} />
|
||||
<span>{node.name}</span>
|
||||
{meta?.version && meta.version !== 'unknown' && meta.version !== '0.0.0-dev' && (
|
||||
{isValidVersion(meta?.version) && (
|
||||
<span className="font-mono text-[10px] tabular-nums text-muted-foreground/60 ml-auto">
|
||||
v{meta.version}
|
||||
</span>
|
||||
|
||||
@@ -28,6 +28,7 @@ import FleetSnapshots from './FleetSnapshots';
|
||||
import { toast } from '@/components/ui/toast-store';
|
||||
import { LabelDot, type Label as StackLabel } from './LabelPill';
|
||||
import { MultiSelectCombobox } from '@/components/ui/multi-select-combobox';
|
||||
import { formatVersion } from '@/lib/version';
|
||||
|
||||
// --- Types ---
|
||||
|
||||
@@ -68,7 +69,7 @@ interface NodeUpdateStatus {
|
||||
name: string;
|
||||
type: 'local' | 'remote';
|
||||
version: string | null;
|
||||
latestVersion: string;
|
||||
latestVersion: string | null;
|
||||
updateAvailable: boolean;
|
||||
updateStatus: 'updating' | 'completed' | 'timeout' | 'failed' | null;
|
||||
}
|
||||
@@ -363,6 +364,8 @@ function NodeCard({ node, onNavigate, labelMap, updateStatus, onUpdate, updating
|
||||
const [loadingStacks, setLoadingStacks] = useState(false);
|
||||
|
||||
const isOnline = node.status === 'online';
|
||||
const formattedVersion = formatVersion(updateStatus?.version);
|
||||
const formattedLatest = formatVersion(updateStatus?.latestVersion);
|
||||
const cpuPercent = getNodeCpu(node);
|
||||
const memPercent = getNodeMem(node);
|
||||
const diskPercent = getNodeDisk(node);
|
||||
@@ -411,9 +414,9 @@ function NodeCard({ node, onNavigate, labelMap, updateStatus, onUpdate, updating
|
||||
<Badge variant="outline" className="text-[10px] px-1.5 py-0 h-4 shrink-0">
|
||||
{node.type}
|
||||
</Badge>
|
||||
{updateStatus?.version && (
|
||||
{formattedVersion && (
|
||||
<Badge variant="outline" className="text-[10px] px-1.5 py-0 h-4 font-mono tabular-nums shrink-0">
|
||||
v{updateStatus.version}
|
||||
{formattedVersion}
|
||||
</Badge>
|
||||
)}
|
||||
{updateStatus?.updateStatus && <UpdateStatusBadge status={updateStatus.updateStatus} />}
|
||||
@@ -498,7 +501,7 @@ function NodeCard({ node, onNavigate, labelMap, updateStatus, onUpdate, updating
|
||||
{updatingNodeId === node.id ? (
|
||||
<><Loader2 className="w-3 h-3 mr-1.5 animate-spin" />Triggering...</>
|
||||
) : (
|
||||
<><Download className="w-3 h-3 mr-1.5" strokeWidth={1.5} />Update to v{updateStatus.latestVersion}</>
|
||||
<><Download className="w-3 h-3 mr-1.5" strokeWidth={1.5} />{formattedLatest ? `Update to ${formattedLatest}` : 'Update'}</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -1140,6 +1143,7 @@ export function FleetView({ onNavigateToNode }: FleetViewProps) {
|
||||
const failed = updateStatuses.filter(s => s.updateStatus === 'failed' || s.updateStatus === 'timeout').length;
|
||||
const q = modalSearch.toLowerCase();
|
||||
const filtered = q ? updateStatuses.filter(s => s.name.toLowerCase().includes(q) || s.type.includes(q)) : updateStatuses;
|
||||
const gatewayLabel = formatVersion(updateStatuses[0]?.latestVersion);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -1182,9 +1186,11 @@ export function FleetView({ onNavigateToNode }: FleetViewProps) {
|
||||
className="h-8 pl-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-[11px] text-muted-foreground shrink-0">
|
||||
Gateway: <span className="font-mono tabular-nums text-foreground">v{updateStatuses[0]?.latestVersion}</span>
|
||||
</div>
|
||||
{gatewayLabel && (
|
||||
<div className="text-[11px] text-muted-foreground shrink-0">
|
||||
Gateway: <span className="font-mono tabular-nums text-foreground">{gatewayLabel}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Table header */}
|
||||
@@ -1218,12 +1224,12 @@ export function FleetView({ onNavigateToNode }: FleetViewProps) {
|
||||
|
||||
{/* Current version */}
|
||||
<span className="text-xs font-mono tabular-nums text-muted-foreground">
|
||||
{s.version && s.version !== 'unknown' && s.version !== '0.0.0-dev' ? `v${s.version}` : <span className="text-muted-foreground/50 italic text-[10px]">unknown</span>}
|
||||
{formatVersion(s.version) ?? <span className="text-muted-foreground/50 italic text-[10px]">unknown</span>}
|
||||
</span>
|
||||
|
||||
{/* Latest version */}
|
||||
<span className="text-xs font-mono tabular-nums">
|
||||
v{s.latestVersion}
|
||||
{formatVersion(s.latestVersion) ?? <span className="text-muted-foreground/50 italic text-[10px]">unknown</span>}
|
||||
</span>
|
||||
|
||||
{/* Status / Action */}
|
||||
|
||||
Reference in New Issue
Block a user