initial platform gateway service
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user