179 lines
5.1 KiB
TypeScript
179 lines
5.1 KiB
TypeScript
import type { AccessGrant, ConnectionView, Platform, PlatformSession, PlatformStatus } from '../types'
|
|
import { headscaleNodeDiscovery } from './headscale-node-discovery'
|
|
|
|
const nowIso = () => new Date().toISOString()
|
|
|
|
function platformListFromEnv(): Platform[] | null {
|
|
try {
|
|
const raw = process.env.PLATFORM_REGISTRY_JSON;
|
|
if (!raw?.trim()) return null;
|
|
const parsed = JSON.parse(raw) as Platform[];
|
|
return Array.isArray(parsed) ? parsed : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const defaultPlatforms: Platform[] = [
|
|
{
|
|
id: 'futuredrive-f1tenth-01',
|
|
name: 'F1Tenth',
|
|
nodeKey: 'futuredrive-f1tenth-01',
|
|
agentUrl: '',
|
|
headscaleUser: 'mobility',
|
|
tags: ['jetson', 'ros', 'f1tenth'],
|
|
state: 'offline',
|
|
lastSeenAt: null
|
|
},
|
|
{
|
|
id: 'futuredrive-mkmini-02',
|
|
name: 'MK-mini',
|
|
nodeKey: 'futuredrive-mkmini-02',
|
|
agentUrl: '',
|
|
headscaleUser: 'mobility',
|
|
tags: ['jetson', 'ros', 'mkmini'],
|
|
state: 'offline',
|
|
lastSeenAt: null
|
|
},
|
|
{
|
|
id: 'futuredrive-simulator-01',
|
|
name: 'Simulator',
|
|
nodeKey: 'futuredrive-simulator-01',
|
|
agentUrl: '',
|
|
headscaleUser: 'mobility',
|
|
tags: ['simulator', 'ros'],
|
|
state: 'offline',
|
|
lastSeenAt: null
|
|
}
|
|
];
|
|
|
|
const platforms = new Map<string, Platform>((platformListFromEnv() ?? defaultPlatforms).map((platform) => [platform.id, platform]));
|
|
|
|
/*
|
|
const platforms = new Map<string, Platform>([
|
|
[
|
|
'jetson-lab-01',
|
|
{
|
|
id: 'jetson-lab-01',
|
|
name: 'Jetson Lab 01',
|
|
nodeKey: 'headscale-node-jetson-lab-01',
|
|
agentUrl: 'http://100.64.0.11:4600',
|
|
headscaleUser: 'mobility',
|
|
tags: ['jetson', 'ros', 'lab'],
|
|
state: 'ready',
|
|
lastSeenAt: nowIso()
|
|
}
|
|
],
|
|
[
|
|
'sim-lab-01',
|
|
{
|
|
id: 'sim-lab-01',
|
|
name: 'Simulator Lab 01',
|
|
nodeKey: 'headscale-node-sim-lab-01',
|
|
agentUrl: 'http://100.64.0.21:4600',
|
|
headscaleUser: 'mobility',
|
|
tags: ['simulator', 'ros'],
|
|
state: 'ready',
|
|
lastSeenAt: nowIso()
|
|
}
|
|
]
|
|
])
|
|
*/
|
|
|
|
const statusByPlatformId = new Map<string, PlatformStatus>(
|
|
Array.from(platforms.values()).map((platform) => [
|
|
platform.id,
|
|
{
|
|
platformId: platform.id,
|
|
batteryPercent: platform.id.startsWith('jetson') ? 86 : null,
|
|
network: 'online',
|
|
ros: 'ok',
|
|
processes: [
|
|
{ name: 'mobility-agent', status: 'running' },
|
|
{ name: 'roscore', status: 'running' }
|
|
],
|
|
updatedAt: nowIso()
|
|
}
|
|
])
|
|
)
|
|
|
|
export class PlatformRegistry {
|
|
listPlatforms(): Platform[] {
|
|
return Array.from(platforms.values())
|
|
}
|
|
|
|
async resolvePlatform(platformId: string, accountEmail?: string): Promise<Platform | null> {
|
|
const platform = this.getPlatform(platformId) ?? {
|
|
id: platformId,
|
|
name: platformId,
|
|
nodeKey: platformId,
|
|
agentUrl: '',
|
|
headscaleUser: 'tagged-devices',
|
|
tags: ['teaching-device'],
|
|
state: 'offline' as const,
|
|
lastSeenAt: null,
|
|
sshUser: null,
|
|
workspaceRoot: null,
|
|
}
|
|
if (!platform) return null
|
|
if (!accountEmail && platform.agentUrl) return platform
|
|
|
|
const resolvedAgent = await headscaleNodeDiscovery.resolveAgent(platform, accountEmail)
|
|
if (!resolvedAgent) {
|
|
const next = { ...platform, agentUrl: '', state: 'offline' as const, lastSeenAt: platform.lastSeenAt }
|
|
platforms.set(platformId, next)
|
|
this.updateNetworkStatus(platformId, 'offline')
|
|
return next
|
|
}
|
|
|
|
const next = {
|
|
...platform,
|
|
agentUrl: resolvedAgent.agentUrl,
|
|
state: platform.state === 'maintenance' ? platform.state : 'ready' as const,
|
|
lastSeenAt: resolvedAgent.lastSeenAt ?? nowIso(),
|
|
sshUser: resolvedAgent.sshUser ?? platform.sshUser ?? null,
|
|
workspaceRoot: resolvedAgent.workspaceRoot ?? platform.workspaceRoot ?? null,
|
|
}
|
|
platforms.set(platformId, next)
|
|
this.updateNetworkStatus(platformId, 'online')
|
|
return next
|
|
}
|
|
|
|
getPlatform(platformId: string): Platform | null {
|
|
return platforms.get(platformId) ?? null
|
|
}
|
|
|
|
getStatus(platformId: string): PlatformStatus | null {
|
|
return statusByPlatformId.get(platformId) ?? null
|
|
}
|
|
|
|
setPlatformState(platformId: string, state: Platform['state']): Platform | null {
|
|
const platform = platforms.get(platformId)
|
|
if (!platform) return null
|
|
const next = { ...platform, state, lastSeenAt: nowIso() }
|
|
platforms.set(platformId, next)
|
|
return next
|
|
}
|
|
|
|
listConnections(activeSessions: PlatformSession[], activeGrants: AccessGrant[]): ConnectionView[] {
|
|
return this.listPlatforms().map((platform) => ({
|
|
platform,
|
|
activeSession: activeSessions.find((session) => session.platformId === platform.id) ?? null,
|
|
activeGrants: activeGrants.filter((grant) => grant.platformId === platform.id),
|
|
status: this.getStatus(platform.id)!
|
|
}))
|
|
}
|
|
|
|
private updateNetworkStatus(platformId: string, network: PlatformStatus['network']) {
|
|
const status = statusByPlatformId.get(platformId)
|
|
if (!status) return
|
|
statusByPlatformId.set(platformId, {
|
|
...status,
|
|
network,
|
|
updatedAt: nowIso()
|
|
})
|
|
}
|
|
}
|
|
|
|
export const platformRegistry = new PlatformRegistry()
|