fix(lms-web): load modules from platform environment profiles

This commit is contained in:
2026-09-01 17:27:54 +09:00
parent 7c0bdf02a2
commit c847e321a7
3 changed files with 33 additions and 18 deletions
+7 -2
View File
@@ -84,6 +84,11 @@ type IdeActivity = WorkspaceActivity
const PUBLIC_GUIDE_COURSE_SLUG = 'autonomy-studio-lms-guide' const PUBLIC_GUIDE_COURSE_SLUG = 'autonomy-studio-lms-guide'
function isRemoteEnvironmentProfile(profile: EnvironmentProfile): boolean {
const target = profile.config?.target
return target === 'remote-jetson' || target === 'platform'
}
function resolveModuleRuntimeImages( function resolveModuleRuntimeImages(
profiles: EnvironmentProfile[], profiles: EnvironmentProfile[],
remoteWorkspace: boolean, remoteWorkspace: boolean,
@@ -92,7 +97,7 @@ function resolveModuleRuntimeImages(
if (!remoteWorkspace) return activeRuntimeImages if (!remoteWorkspace) return activeRuntimeImages
const images = profiles const images = profiles
.filter((profile) => ['real_slam', 'real_slam_3d', 'real_navigation'].includes(profile.profileType.toLowerCase())) .filter(isRemoteEnvironmentProfile)
.flatMap((profile) => Array.isArray(profile.config.runtimeImages) ? profile.config.runtimeImages : []) .flatMap((profile) => Array.isArray(profile.config.runtimeImages) ? profile.config.runtimeImages : [])
.filter((image): image is RuntimeImageArtifact => ( .filter((image): image is RuntimeImageArtifact => (
typeof image === 'object' typeof image === 'object'
@@ -740,7 +745,7 @@ function LocalPcWorkspaceMockup({
const runtimeModules = useMemo(() => resolveRuntimeModules( const runtimeModules = useMemo(() => resolveRuntimeModules(
curriculumWeek.modules, curriculumWeek.modules,
remoteWorkspace remoteWorkspace
? visualizationProfiles.filter((profile) => ['real_slam', 'real_slam_3d', 'real_navigation'].includes(profile.profileType.toLowerCase())) ? visualizationProfiles.filter(isRemoteEnvironmentProfile)
: activeEnvironmentProfile, : activeEnvironmentProfile,
remoteWorkspace ? 'platform' : 'local' remoteWorkspace ? 'platform' : 'local'
), [activeEnvironmentProfile, curriculumWeek.modules, remoteWorkspace, visualizationProfiles]) ), [activeEnvironmentProfile, curriculumWeek.modules, remoteWorkspace, visualizationProfiles])
@@ -50,7 +50,9 @@ export function useWorkspaceModuleList({
const [modulesLoading, setModulesLoading] = useState(false) const [modulesLoading, setModulesLoading] = useState(false)
const [moduleListError, setModuleListError] = useState<string | null>(null) const [moduleListError, setModuleListError] = useState<string | null>(null)
const remoteTarget = useMemo(() => parseRemoteWorkspaceTarget(remoteWorkspaceTargetKey), [remoteWorkspaceTargetKey]) const remoteTarget = useMemo(() => parseRemoteWorkspaceTarget(remoteWorkspaceTargetKey), [remoteWorkspaceTargetKey])
const moduleInputsRef = useRef({ curriculumModules, applyModuleParameterOverrides })
const loadSequenceRef = useRef(0) const loadSequenceRef = useRef(0)
const moduleLoadInFlightRef = useRef(false)
const pendingModuleStatesRef = useRef(new Map<string, { state: ModuleRow['state']; expiresAt: number }>()) const pendingModuleStatesRef = useRef(new Map<string, { state: ModuleRow['state']; expiresAt: number }>())
const moduleStateKey = (module: ModuleRow): string => module.folderName ?? module.name const moduleStateKey = (module: ModuleRow): string => module.folderName ?? module.name
@@ -89,9 +91,13 @@ export function useWorkspaceModuleList({
})) }))
}, []) }, [])
useEffect(() => {
moduleInputsRef.current = { curriculumModules, applyModuleParameterOverrides }
}, [applyModuleParameterOverrides, curriculumModuleKey, curriculumModules])
const loadRemoteModules = useCallback(async (): Promise<{ modules: ModuleRow[] }> => { const loadRemoteModules = useCallback(async (): Promise<{ modules: ModuleRow[] }> => {
if (!remoteTarget) return { modules: [] } if (!remoteTarget) throw new Error('원격 플랫폼 SSH 주소를 확인하지 못했습니다.')
const result = await window.remoteWorkspace.modules.list(remoteTarget, curriculumModules ?? []) const result = await window.remoteWorkspace.modules.list(remoteTarget, moduleInputsRef.current.curriculumModules ?? [])
return { return {
modules: result.modules.map((module) => { modules: result.modules.map((module) => {
const rawModule = module as ModuleRow & { defaultParameters?: unknown } const rawModule = module as ModuleRow & { defaultParameters?: unknown }
@@ -106,38 +112,42 @@ export function useWorkspaceModuleList({
}, [curriculumModuleKey, remoteTarget]) }, [curriculumModuleKey, remoteTarget])
const loadAvailableModules = useCallback(async ({ silent = false }: LoadAvailableModulesOptions = {}): Promise<void> => { const loadAvailableModules = useCallback(async ({ silent = false }: LoadAvailableModulesOptions = {}): Promise<void> => {
// Remote inventory probes open SSH sessions. Keep one request in flight so
// polling cannot continuously replace a slow response with a newer one.
if (moduleLoadInFlightRef.current) return
moduleLoadInFlightRef.current = true
const requestSequence = ++loadSequenceRef.current const requestSequence = ++loadSequenceRef.current
if (!silent) setModulesLoading(true) if (!silent) setModulesLoading(true)
try { try {
const { curriculumModules: currentCurriculumModules, applyModuleParameterOverrides: applyOverrides } = moduleInputsRef.current
if (remoteWorkspace) { if (remoteWorkspace) {
const result = await loadRemoteModules() const result = await loadRemoteModules()
if (requestSequence !== loadSequenceRef.current) return if (requestSequence !== loadSequenceRef.current) return
setModuleListError(null) setModuleListError(null)
setAvailableModules(applyPendingModuleStates(applyModuleParameterOverrides(result.modules).sort(compareModulePriority))) setAvailableModules(applyPendingModuleStates(applyOverrides(result.modules).sort(compareModulePriority)))
return return
} }
const result = await window.localWorkspace.modules.list(clonePathPreview, curriculumModules ?? []) const result = await window.localWorkspace.modules.list(clonePathPreview, currentCurriculumModules ?? [])
if (requestSequence !== loadSequenceRef.current) return if (requestSequence !== loadSequenceRef.current) return
setModuleListError(null) setModuleListError(null)
setAvailableModules(applyPendingModuleStates(applyModuleParameterOverrides(result.modules).sort(compareModulePriority))) setAvailableModules(applyPendingModuleStates(applyOverrides(result.modules).sort(compareModulePriority)))
} catch (error) { } catch (error) {
if (requestSequence !== loadSequenceRef.current) return if (requestSequence !== loadSequenceRef.current) return
console.error('[modules] list failed', error) console.error('[modules] list failed', error)
const message = error instanceof Error ? error.message : '모듈 목록을 불러오지 못했습니다.' const message = error instanceof Error ? error.message : '모듈 목록을 불러오지 못했습니다.'
if (!silent) { if (!silent) {
setModuleListError(message) setModuleListError(message)
setAvailableModules(makeUninstalledModuleRows(curriculumModules)) setAvailableModules(makeUninstalledModuleRows(moduleInputsRef.current.curriculumModules))
} }
} finally { } finally {
moduleLoadInFlightRef.current = false
if (!silent && requestSequence === loadSequenceRef.current) setModulesLoading(false) if (!silent && requestSequence === loadSequenceRef.current) setModulesLoading(false)
} }
}, [ }, [
applyModuleParameterOverrides,
applyPendingModuleStates, applyPendingModuleStates,
clonePathPreview, clonePathPreview,
curriculumModuleKey, curriculumModuleKey,
curriculumModules,
loadRemoteModules, loadRemoteModules,
remoteWorkspace remoteWorkspace
]) ])
@@ -151,10 +161,10 @@ export function useWorkspaceModuleList({
const intervalId = window.setInterval(() => { const intervalId = window.setInterval(() => {
void loadAvailableModules({ silent: true }) void loadAvailableModules({ silent: true })
}, 5000) }, remoteWorkspace ? 10_000 : 5_000)
return () => window.clearInterval(intervalId) return () => window.clearInterval(intervalId)
}, [loadAvailableModules, pollingActive]) }, [loadAvailableModules, pollingActive, remoteWorkspace])
return { return {
availableModules, availableModules,
@@ -36,14 +36,14 @@ function resolveNav2Mode(profile: EnvironmentProfile | null): Nav2VisualizationM
return profile?.profileType.toLowerCase() === 'real_navigation' ? 'navigation' : 'slam' return profile?.profileType.toLowerCase() === 'real_navigation' ? 'navigation' : 'slam'
} }
function isRealPlatformProfile(profile: EnvironmentProfile | null): boolean { function isRemoteEnvironmentProfile(profile: EnvironmentProfile | null): boolean {
const type = profile?.profileType.toLowerCase() const target = profile?.config?.target
return type === 'real_slam' || type === 'real_navigation' return target === 'remote-jetson' || target === 'platform'
} }
function selectRuntimeProfiles(profiles: EnvironmentProfile[], remoteWorkspace: boolean): EnvironmentProfile[] { function selectRuntimeProfiles(profiles: EnvironmentProfile[], remoteWorkspace: boolean): EnvironmentProfile[] {
const matchingProfiles = profiles.filter((profile) => ( const matchingProfiles = profiles.filter((profile) => (
remoteWorkspace ? isRealPlatformProfile(profile) : !isRealPlatformProfile(profile) remoteWorkspace ? isRemoteEnvironmentProfile(profile) : !isRemoteEnvironmentProfile(profile)
)) ))
return matchingProfiles.length ? matchingProfiles : profiles return matchingProfiles.length ? matchingProfiles : profiles
} }
@@ -121,7 +121,7 @@ export function useWorkspaceVisualization({
const activeEnvironmentAgentUrl = typeof activeEnvironmentAgent?.url === 'string' const activeEnvironmentAgentUrl = typeof activeEnvironmentAgent?.url === 'string'
? activeEnvironmentAgent.url.replace(/\/+$/, '') ? activeEnvironmentAgent.url.replace(/\/+$/, '')
: '' : ''
const visualizationRemote = activeEnvironmentConfig.target === 'remote-jetson' && activeEnvironmentAgentUrl const visualizationRemote = isRemoteEnvironmentProfile(activeEnvironmentProfile) && activeEnvironmentAgentUrl
const visualizationRemoteBridgeHttpUrl = (() => { const visualizationRemoteBridgeHttpUrl = (() => {
if (!visualizationRemote) return '' if (!visualizationRemote) return ''
try { try {
@@ -138,7 +138,7 @@ export function useWorkspaceVisualization({
: 'ws://localhost:8765' : 'ws://localhost:8765'
const visualizationBackendUrl = visualizationBridgeHttpUrl const visualizationBackendUrl = visualizationBridgeHttpUrl
const visualizationViewerUrl = visualizationStack === 'slam' || visualizationStack === 'nav2' const visualizationViewerUrl = visualizationStack === 'slam' || visualizationStack === 'nav2'
? `/slam-viewer/index.html?directBridge=1&mode=${nav2VisualizationMode}&runtime=${isRealPlatformProfile(activeEnvironmentProfile) ? 'real' : 'simulation'}&bridgeWs=${encodeURIComponent(`${visualizationBridgeUrl}/ws/state`)}&bridgeHttp=${encodeURIComponent(visualizationBridgeHttpUrl)}&mapKey=${encodeURIComponent(mapKey ?? 'nav2-turtlebot3-world')}&layoutVersion=${worldLayoutVersion ?? 0}` ? `/slam-viewer/index.html?directBridge=1&mode=${nav2VisualizationMode}&runtime=${isRemoteEnvironmentProfile(activeEnvironmentProfile) ? 'real' : 'simulation'}&bridgeWs=${encodeURIComponent(`${visualizationBridgeUrl}/ws/state`)}&bridgeHttp=${encodeURIComponent(visualizationBridgeHttpUrl)}&mapKey=${encodeURIComponent(mapKey ?? 'nav2-turtlebot3-world')}&layoutVersion=${worldLayoutVersion ?? 0}`
: `/autoware-viewer/index.html?source=ros&stack=${visualizationStack}&bridge=${encodeURIComponent(visualizationBridgeUrl)}&backend=${encodeURIComponent(visualizationBackendUrl)}` : `/autoware-viewer/index.html?source=ros&stack=${visualizationStack}&bridge=${encodeURIComponent(visualizationBridgeUrl)}&backend=${encodeURIComponent(visualizationBackendUrl)}`
useEffect(() => { useEffect(() => {