initial platform gateway service

This commit is contained in:
2026-07-08 14:44:20 +09:00
commit 05d0fd80f0
21 changed files with 1770 additions and 0 deletions
+178
View File
@@ -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()