Compare commits

...

2 Commits

Author SHA1 Message Date
chyoo 38ab528d24 add commits.. 2026-08-12 14:46:57 +09:00
chyoo 324e644ef6 fix: restrict NAV2 prebuilt artifacts to amd64 2026-08-11 14:21:21 +09:00
19 changed files with 550 additions and 44 deletions
@@ -0,0 +1,13 @@
UPDATE "course_week_modules"
SET "prebuilt_artifacts" = jsonb_build_array(
jsonb_build_object(
'type', 'build-install',
'fileName', 'lms-nav2-mkmini-build-install-amd64.tar.zst',
'url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-mkmini/1.0.0/prebuilt/amd64/lms-nav2-mkmini-build-install-amd64.tar.zst',
'checksumSha256', 'a11ac9e920fcbe5c5a54e086b72d453932ece32e0c7976858435c0a0ba73a8f9',
'architecture', 'amd64',
'required', false,
'extractPath', '.'
)
)
WHERE "folder_name" = '[lms-nav2-mkmini]nav2-mkmini_v1.0.0_ws';
@@ -0,0 +1,18 @@
UPDATE course_week_modules AS module
SET prebuilt_artifacts = (
SELECT jsonb_agg(
CASE
WHEN artifact->>'url' LIKE '%/studio-artifacts/modules/lms-nav2-%/1.0.0/prebuilt/%'
AND COALESCE(NULLIF(artifact->>'architecture', ''), '') = ''
THEN jsonb_set(artifact, '{architecture}', '"amd64"'::jsonb, true)
ELSE artifact
END
)
FROM jsonb_array_elements(module.prebuilt_artifacts) AS artifact
)
WHERE EXISTS (
SELECT 1
FROM jsonb_array_elements(module.prebuilt_artifacts) AS artifact
WHERE artifact->>'url' LIKE '%/studio-artifacts/modules/lms-nav2-%/1.0.0/prebuilt/%'
AND COALESCE(NULLIF(artifact->>'architecture', ''), '') = ''
);
@@ -0,0 +1,16 @@
UPDATE "course_environment_profiles"
SET "config" = replace(
replace(
replace(
"config"::text,
'nav2-slam:humble',
'future-nav2-gazebo:humble'
),
'runtime-images/nav2/humble/latest/nav2-slam-humble.tar.zst',
'runtime-images/nav2/gazebo/latest/future-nav2-gazebo-humble.tar.zst'
),
'nav2-slam-humble.tar.zst',
'future-nav2-gazebo-humble.tar.zst'
)::jsonb
WHERE "config"::text LIKE '%nav2-slam:humble%'
OR "config"::text LIKE '%nav2-slam-humble.tar.zst%';
@@ -0,0 +1,17 @@
UPDATE "course_week_modules" AS module
SET "prebuilt_artifacts" = (
SELECT jsonb_agg(
CASE
WHEN artifact->>'fileName' = 'lms-nav2-gazebo-simulator-build-install.tar.zst'
OR artifact->>'url' LIKE '%/lms-nav2-gazebo-simulator-build-install.tar.zst'
THEN jsonb_set(
artifact,
'{checksumSha256}',
to_jsonb('8432100a17d85047ab4ac79445d5eacf236ccc79a785db61d14270104af270b7'::text)
)
ELSE artifact
END
)
FROM jsonb_array_elements(module."prebuilt_artifacts") AS artifact
)
WHERE module."folder_name" = '[lms-nav2-gazebo-simulator]nav2-simulator_v1.0.0_ws';
@@ -0,0 +1,18 @@
-- The NAV2 viewer expects the unified slam-ros-bridge API
-- (/state, /ws/state and /ws/control). The older nav2-ros-bridge context
-- does not expose those endpoints, so remove that override from every
-- local NAV2 course environment profile.
UPDATE "course_environment_profiles"
SET
"config" = jsonb_set(
("config"::jsonb #- '{bridge,buildContext}'),
'{bridge,image}',
'"slam-ros-bridge:latest"'::jsonb,
true
),
"updated_at" = CURRENT_TIMESTAMP
WHERE "platform_key" = 'NAV2'
AND (
"config"::jsonb #>> '{bridge,buildContext}' = 'resources/nav2-ros-bridge'
OR "config"::jsonb #>> '{bridge,image}' = 'nav2-ros-bridge'
);
@@ -0,0 +1,27 @@
UPDATE "course_week_modules" AS module
SET "prebuilt_artifacts" = (
SELECT jsonb_agg(
CASE
WHEN artifact->>'fileName' = 'lms-nav2-gazebo-planning-build-install.tar.zst'
OR artifact->>'url' LIKE '%/lms-nav2-gazebo-planning-build-install.tar.zst'
THEN jsonb_set(
artifact,
'{checksumSha256}',
to_jsonb('6087b12b35a4c2ea3073c6b9fad5e43175d7425c2f4a042fa654e8c42b2bf29d'::text)
)
WHEN artifact->>'fileName' = 'lms-nav2-gazebo-control-build-install.tar.zst'
OR artifact->>'url' LIKE '%/lms-nav2-gazebo-control-build-install.tar.zst'
THEN jsonb_set(
artifact,
'{checksumSha256}',
to_jsonb('ce8949b8a14f6d29b3924b82d4819923368fb67029f45cc50a07175afa659a7a'::text)
)
ELSE artifact
END
)
FROM jsonb_array_elements(module."prebuilt_artifacts") AS artifact
)
WHERE module."folder_name" IN (
'[lms-nav2-gazebo-planning]nav2-planning_v1.0.0_ws',
'[lms-nav2-gazebo-control]nav2-control_v1.0.0_ws'
);
@@ -0,0 +1,20 @@
UPDATE "course_week_modules" AS module
SET "default_parameters" = CASE
WHEN EXISTS (
SELECT 1
FROM jsonb_array_elements(module."default_parameters") AS parameter
WHERE parameter #>> '{}' LIKE 'localization_mode:=%'
) THEN (
SELECT jsonb_agg(
CASE
WHEN parameter #>> '{}' LIKE 'localization_mode:=%'
THEN to_jsonb('localization_mode:=amcl'::text)
ELSE parameter
END
)
FROM jsonb_array_elements(module."default_parameters") AS parameter
)
ELSE COALESCE(module."default_parameters", '[]'::jsonb)
|| jsonb_build_array('localization_mode:=amcl')
END
WHERE module."folder_name" = '[lms-nav2-gazebo-localization]nav2-localization_v1.0.0_ws';
@@ -0,0 +1,7 @@
-- The selected map determines SLAM/navigation and 2D/3D viewer mode.
-- Environment profiles retain only runtime connection configuration.
UPDATE "course_environment_profiles"
SET
"config" = "config" #- '{viewer,mode}',
"updated_at" = CURRENT_TIMESTAMP
WHERE "config" #> '{viewer,mode}' IS NOT NULL;
@@ -0,0 +1,39 @@
-- SLAM/navigation is selected from the map. Keep one runtime configuration
-- for the remote platform instead of separate visualization profiles.
DELETE FROM "course_environment_profiles" legacy
USING "course_environment_profiles" platform
WHERE legacy."course_id" = platform."course_id"
AND legacy."week" = platform."week"
AND platform."platform_key" = 'PLATFORM'
AND legacy."platform_key" IN ('REAL_SLAM', 'REAL_SLAM_3D', 'REAL_NAVIGATION');
WITH canonical AS (
SELECT id
FROM (
SELECT
id,
row_number() OVER (
PARTITION BY "course_id", "week"
ORDER BY CASE "platform_key"
WHEN 'REAL_SLAM' THEN 1
WHEN 'REAL_NAVIGATION' THEN 2
WHEN 'REAL_SLAM_3D' THEN 3
END
) AS rank
FROM "course_environment_profiles"
WHERE "platform_key" IN ('REAL_SLAM', 'REAL_SLAM_3D', 'REAL_NAVIGATION')
) ranked
WHERE rank = 1
)
UPDATE "course_environment_profiles" profile
SET
"platform_key" = 'PLATFORM',
"profile_type" = 'PLATFORM',
"name" = '플랫폼',
"config" = jsonb_set(profile."config" #- '{viewer,mode}', '{type}', '"PLATFORM"'::jsonb, true),
"updated_at" = CURRENT_TIMESTAMP
FROM canonical
WHERE profile.id = canonical.id;
DELETE FROM "course_environment_profiles"
WHERE "platform_key" IN ('REAL_SLAM', 'REAL_SLAM_3D', 'REAL_NAVIGATION');
+121 -31
View File
@@ -25,9 +25,6 @@ WHERE course_id IN (SELECT id FROM target_course);
DELETE FROM course_environment_profiles
WHERE course_id IN (SELECT id FROM target_course);
DELETE FROM course_week_visualization_platforms
WHERE course_id IN (SELECT id FROM target_course);
DELETE FROM course_week_maps
WHERE course_id IN (SELECT id FROM target_course);
@@ -153,23 +150,23 @@ WHERE w.course_id IN (SELECT id FROM target_course) AND w.week = 2;
WITH module_definitions(category, name, folder_name, template_repository_url, prebuilt_artifacts, default_parameters, docker_image) AS (
VALUES
('Simulator', 'NAV2-Gazebo', '[lms-nav2-gazebo-simulator]nav2-simulator_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-simulator.git',
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-simulator/1.0.0/prebuilt/latest/lms-nav2-gazebo-simulator-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-simulator-build-install.tar.zst', 'required', false, 'extractPath', '.')), jsonb_build_array('gui:=false'), 'future-nav2-gazebo:humble'),
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-simulator/1.0.0/prebuilt/latest/lms-nav2-gazebo-simulator-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-simulator-build-install.tar.zst', 'checksumSha256', '7d2cfe092e81ef15a6e55525410d6f30a803d8c81bee20541dd1d7b964b1fe53', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('gui:=false'), 'future-nav2-gazebo:humble'),
('Vehicle', 'NAV2-TurtleBot3Burger', '[lms-nav2-gazebo-vehicle]nav2-vehicle_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-vehicle.git',
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-vehicle/1.0.0/prebuilt/latest/lms-nav2-gazebo-vehicle-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-vehicle-build-install.tar.zst', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true', 'platform:=turtlebot3', 'level:=2d'), 'future-nav2-gazebo:humble'),
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-vehicle/1.0.0/prebuilt/latest/lms-nav2-gazebo-vehicle-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-vehicle-build-install.tar.zst', 'checksumSha256', '42e8179172c178978e5ae5d48e189164f6b8a5030fdaa0eba497ca86a97e96bb', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true', 'platform:=turtlebot3', 'level:=2d'), 'future-nav2-gazebo:humble'),
('Map', 'NAV2-Map', '[lms-nav2-gazebo-map]nav2-map_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-map.git',
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-map/1.0.0/prebuilt/latest/lms-nav2-gazebo-map-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-map-build-install.tar.zst', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'),
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-map/1.0.0/prebuilt/latest/lms-nav2-gazebo-map-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-map-build-install.tar.zst', 'checksumSha256', 'f409ce54c40aeeed8cf3fc06251c2e321887b3ec5f5827dfb74897a62780582f', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'),
('Localization', 'NAV2-SLAM', '[lms-nav2-gazebo-slam]nav2-slam_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-slam.git',
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-slam/1.0.0/prebuilt/latest/lms-nav2-gazebo-slam-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-slam-build-install.tar.zst', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'),
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-slam/1.0.0/prebuilt/latest/lms-nav2-gazebo-slam-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-slam-build-install.tar.zst', 'checksumSha256', '186f2aa926eb41140b2783507b521083536aee3be716cf55c591a32250ec0fce', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'),
('Localization', 'NAV2-Localization', '[lms-nav2-gazebo-localization]nav2-localization_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-localization.git',
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-localization/1.0.0/prebuilt/latest/lms-nav2-gazebo-localization-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-localization-build-install.tar.zst', 'required', false, 'extractPath', '.')), jsonb_build_array('localization_mode:=slam'), 'future-nav2-gazebo:humble'),
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-localization/1.0.0/prebuilt/latest/lms-nav2-gazebo-localization-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-localization-build-install.tar.zst', 'checksumSha256', '44c8d50205f001a196870d03f44d435582b96c51eb4445e50cc8db6203b2c0c9', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('localization_mode:=amcl'), 'future-nav2-gazebo:humble'),
('Perception', 'NAV2-Perception', '[lms-nav2-gazebo-perception]nav2-perception_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-perception.git',
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-perception/1.0.0/prebuilt/latest/lms-nav2-gazebo-perception-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-perception-build-install.tar.zst', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true'), 'future-nav2-gazebo:humble'),
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-perception/1.0.0/prebuilt/latest/lms-nav2-gazebo-perception-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-perception-build-install.tar.zst', 'checksumSha256', '23c6523d98506caee9e982741c1c806f9f58581aba777af58eadc7cd5fca1432', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true'), 'future-nav2-gazebo:humble'),
('Planning', 'NAV2-Planning', '[lms-nav2-gazebo-planning]nav2-planning_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-planning.git',
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-planning/1.0.0/prebuilt/latest/lms-nav2-gazebo-planning-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-planning-build-install.tar.zst', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'),
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-planning/1.0.0/prebuilt/latest/lms-nav2-gazebo-planning-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-planning-build-install.tar.zst', 'checksumSha256', 'b0c698acf54ef71915e2707868726b998fbdbb2e2ad15fe65cefc931a9afaa78', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'),
('Control', 'NAV2-Control', '[lms-nav2-gazebo-control]nav2-control_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-control.git',
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-control/1.0.0/prebuilt/latest/lms-nav2-gazebo-control-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-control-build-install.tar.zst', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true', 'robot:=turtlebot3', 'level:=2d'), 'future-nav2-gazebo:humble'),
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-control/1.0.0/prebuilt/latest/lms-nav2-gazebo-control-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-control-build-install.tar.zst', 'checksumSha256', '63b3b59377c19288f47aa5619fef9b10938f400771df1805d8ca973648ac0ed8', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true', 'robot:=turtlebot3', 'level:=2d'), 'future-nav2-gazebo:humble'),
('Vehicle', 'NAV2-MKMini', '[lms-nav2-mkmini]nav2-mkmini_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-mkmini.git',
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-mkmini/1.0.0/prebuilt/amd64/lms-nav2-mkmini-build-install-amd64.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-mkmini-build-install-amd64.tar.zst', 'checksumSha256', '8b36d3fda6a81a49da140529d9874e99b734d05e01e12a49e719e2b26f8803d9', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=false', 'platform:=mkmini', 'robot:=mkmini', 'level:=2d'), 'future-nav2:humble-jetson_orin')
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-mkmini/1.0.0/prebuilt/amd64/lms-nav2-mkmini-build-install-amd64.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-mkmini-build-install-amd64.tar.zst', 'checksumSha256', '6094d2aa5188a28153452aec08e28870984b36c1bc0c06fec8091901f6d26856', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=false', 'platform:=mkmini', 'level:=2d'), 'future-nav2:humble-jetson_orin')
)
INSERT INTO course_week_modules (week_id, sort_order, category, name, folder_name, template_repository_url, source_type, prebuilt_artifacts, default_parameters, docker_image)
SELECT w.id, row_number() OVER (PARTITION BY w.id ORDER BY module_definitions.folder_name), category, name, folder_name, template_repository_url, 'GIT', prebuilt_artifacts, default_parameters, docker_image
@@ -177,19 +174,92 @@ FROM course_weeks w
CROSS JOIN module_definitions
WHERE w.course_id IN (SELECT id FROM target_course) AND w.week IN (1, 2);
INSERT INTO course_week_visualization_platforms (course_id, week, platform_key, enabled, created_at, updated_at)
SELECT target_course.id, weeks.week, platform.platform_key, true, now(), now()
FROM target_course
CROSS JOIN (VALUES (1), (2)) AS weeks(week)
JOIN visualization_platforms platform ON platform.platform_key IN ('NAV2', 'REAL_SLAM', 'REAL_NAVIGATION');
INSERT INTO course_environment_profiles (course_id, week, platform_key, profile_type, name, config, enabled, created_at, updated_at)
SELECT target_course.id, weeks.week, platform.platform_key, platform.type,
CASE WHEN platform.platform_key = 'NAV2' THEN '로컬' ELSE platform.display_name END,
platform.default_config, true, now(), now()
SELECT target_course.id, weeks.week, environment.platform_key, environment.profile_type,
environment.name, environment.config, true, now(), now()
FROM target_course
CROSS JOIN (VALUES (1), (2)) AS weeks(week)
JOIN visualization_platforms platform ON platform.platform_key IN ('NAV2', 'REAL_SLAM', 'REAL_NAVIGATION');
CROSS JOIN (
VALUES
(
'NAV2',
'NAV2',
'로컬',
jsonb_build_object(
'type', 'NAV2',
'target', 'local',
'viewer', jsonb_build_object('stack', 'nav2'),
'bridge', jsonb_build_object(
'name', 'nav2-ros-bridge',
'port', 8765,
'image', 'slam-ros-bridge:latest',
'env', jsonb_build_object('ROBOT_STACK', 'nav2')
),
'runtimeImages', jsonb_build_array(jsonb_build_object(
'url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/runtime-images/nav2/gazebo/latest/future-nav2-gazebo-humble.tar.zst',
'image', 'future-nav2-gazebo:humble',
'fileName', 'future-nav2-gazebo-humble.tar.zst',
'required', true,
'checksumSha256', '54230e039c1fa5dc82615cef3828c5acae7b8bea8e921b70cfdc5351d8301644'
))
)
),
(
'PLATFORM',
'PLATFORM',
'플랫폼',
jsonb_build_object(
'type', 'PLATFORM',
'target', 'remote-jetson',
'runtimeKind', 'real',
'viewer', jsonb_build_object('stack', 'nav2'),
'bridge', jsonb_build_object(
'name', 'nav2-ros-bridge',
'port', 8765,
'image', 'slam-ros-bridge:latest',
'env', jsonb_build_object('ROBOT_STACK', 'nav2')
),
'runtimeImages', jsonb_build_array(
jsonb_build_object(
'url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/runtime-images/slam-ros-bridge/latest/slam-ros-bridge-latest.tar.zst',
'image', 'slam-ros-bridge:latest',
'fileName', 'slam-ros-bridge-latest.tar.zst',
'required', true
),
jsonb_build_object(
'url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/runtime-images/nav2/jetson-orin/latest/future-nav2-humble-jetson-orin.tar.zst',
'image', 'future-nav2:humble-jetson_orin',
'fileName', 'future-nav2-humble-jetson-orin.tar.zst',
'required', true
)
)
)
)
) AS environment(platform_key, profile_type, name, config);
-- Viewer mode belongs to the selected map, not the platform environment.
UPDATE course_environment_profiles profile
SET config = profile.config::jsonb #- '{viewer,mode}'
WHERE profile.course_id IN (SELECT id FROM target_course);
-- Real-platform modules need the Jetson NAV2 runtime in addition to the
-- visualization bridge image. The desktop app downloads this artifact on
-- demand before starting a remote module.
UPDATE course_environment_profiles profile
SET config = jsonb_set(
profile.config::jsonb,
'{runtimeImages}',
COALESCE(profile.config::jsonb->'runtimeImages', '[]'::jsonb) || jsonb_build_array(jsonb_build_object(
'url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/runtime-images/nav2/jetson-orin/latest/future-nav2-humble-jetson-orin.tar.zst',
'image', 'future-nav2:humble-jetson_orin',
'fileName', 'future-nav2-humble-jetson-orin.tar.zst',
'required', true
)),
true
)
WHERE profile.course_id IN (SELECT id FROM target_course)
AND profile.platform_key = 'PLATFORM'
AND NOT (COALESCE(profile.config::jsonb->'runtimeImages', '[]'::jsonb) @> '[{"image":"future-nav2:humble-jetson_orin"}]'::jsonb);
-- Local simulation gets all Gazebo modules. Platform profiles only receive modules applicable to their workflow.
UPDATE course_environment_profiles profile
@@ -204,19 +274,16 @@ SET config = profile.config::jsonb || jsonb_build_object('moduleSet', jsonb_buil
AND week.week = profile.week
AND (
(profile.platform_key = 'NAV2' AND module.folder_name LIKE '[lms-nav2-gazebo-%')
OR (profile.platform_key IN ('REAL_SLAM', 'REAL_NAVIGATION') AND module.folder_name IN (
'[lms-nav2-mkmini]nav2-mkmini_v1.0.0_ws',
'[lms-nav2-gazebo-map]nav2-map_v1.0.0_ws',
'[lms-nav2-gazebo-perception]nav2-perception_v1.0.0_ws',
'[lms-nav2-gazebo-localization]nav2-localization_v1.0.0_ws',
'[lms-nav2-gazebo-planning]nav2-planning_v1.0.0_ws',
'[lms-nav2-gazebo-control]nav2-control_v1.0.0_ws'
OR (profile.platform_key = 'PLATFORM' AND module.folder_name NOT IN (
'[lms-nav2-gazebo-simulator]nav2-simulator_v1.0.0_ws',
'[lms-nav2-gazebo-vehicle]nav2-vehicle_v1.0.0_ws',
'[lms-nav2-gazebo-gui]nav2-rviz2_v1.0.0_ws'
))
)
), '[]'::jsonb)
))
WHERE profile.course_id IN (SELECT id FROM target_course)
AND profile.platform_key IN ('NAV2', 'REAL_SLAM', 'REAL_NAVIGATION');
AND profile.platform_key IN ('NAV2', 'PLATFORM');
-- Control runs with TurtleBot3/Gazebo defaults locally, but uses MK-mini and the ARM64 image on a platform.
UPDATE course_environment_profiles profile
@@ -238,7 +305,30 @@ SET config = jsonb_set(profile.config::jsonb, '{moduleSet,modules}', (
) AS control_module
), true)
WHERE profile.course_id IN (SELECT id FROM target_course)
AND profile.platform_key IN ('REAL_SLAM', 'REAL_NAVIGATION');
AND profile.platform_key = 'PLATFORM';
-- The 2D perception module defaults to Gazebo in the course module list.
-- Override only that value for the real platform; the other added modules
-- retain their own compatible base parameters.
UPDATE course_environment_profiles profile
SET config = jsonb_set(profile.config::jsonb, '{moduleSet,modules}', (
SELECT jsonb_agg(
CASE WHEN item->>'id' = perception_module.id::text THEN item || jsonb_build_object(
'defaultParameters', jsonb_build_array('sim_mode:=false')
) ELSE item END
)
FROM jsonb_array_elements(profile.config->'moduleSet'->'modules') AS item
CROSS JOIN LATERAL (
SELECT module.id
FROM course_weeks week
JOIN course_week_modules module ON module.week_id = week.id
WHERE week.course_id = profile.course_id
AND week.week = profile.week
AND module.name = 'NAV2-Perception'
) AS perception_module
), true)
WHERE profile.course_id IN (SELECT id FROM target_course)
AND profile.platform_key = 'PLATFORM';
INSERT INTO course_week_maps (id, course_id, week, map_key, sort_order, created_at, updated_at)
SELECT gen_random_uuid(), target_course.id, weeks.week, map.map_key, map.sort_order, now(), now()
+3 -1
View File
@@ -2,6 +2,7 @@ import { Body, Controller, Delete, Get, Param, Post, Query, Req } from '@nestjs/
import { Request } from 'express';
import { ChatService } from './chat.service';
import { CancelActiveAiResponseDto } from './dto/cancel-active-ai-response.dto';
import { CreateAiResponseDto } from './dto/create-ai-response.dto';
import { CreateChatMessageDto } from './dto/create-chat-message.dto';
import { CreateInstructorChatMessageDto } from './dto/create-instructor-chat-message.dto';
@@ -64,10 +65,11 @@ export class ChatController {
@Post('ai/cancel-active')
cancelActiveAiResponse(
@Req() request: Request,
@Body() body: { week?: number },
@Body() body: CancelActiveAiResponseDto,
) {
return this.chatService.cancelActiveAiResponse(
this.getBearerToken(request),
body.courseId,
body.week,
);
}
+41 -9
View File
@@ -57,6 +57,7 @@ type FutureDriveLlmChatResponse = {
const STUDENT_CHAT_VISIBLE_MESSAGE_LIMIT = 20;
const AI_CHAT_CONTEXT_MESSAGE_LIMIT = 6;
const PUBLIC_GUIDE_COURSE_SLUG = 'autonomy-studio-lms-guide';
@Injectable()
export class ChatService {
@@ -79,7 +80,7 @@ export class ChatService {
async listThreads(accessToken: string | undefined, query: ListChatThreadsDto) {
const student = await this.getLmsUser(accessToken);
const course = await this.getActiveCourse();
const course = await this.getStudentCourse(student.id, query.courseId);
const week = query.week
? await this.getCourseWeek(course.id, query.week)
: null;
@@ -122,7 +123,7 @@ export class ChatService {
async clearStudentThreads(accessToken: string | undefined, query: ListChatThreadsDto) {
const student = await this.getLmsUser(accessToken);
const course = await this.getActiveCourse();
const course = await this.getStudentCourse(student.id, query.courseId);
const week = query.week
? await this.getCourseWeek(course.id, query.week)
: null;
@@ -167,7 +168,7 @@ export class ChatService {
dto: CreateChatMessageDto,
) {
const student = await this.getLmsUser(accessToken);
const course = await this.getActiveCourse();
const course = await this.getStudentCourse(student.id, dto.courseId);
const week = dto.week ? await this.getCourseWeek(course.id, dto.week) : null;
const thread = await this.getOrCreateThread({
courseId: course.id,
@@ -220,7 +221,7 @@ export class ChatService {
dto: CreateAiResponseDto,
) {
const student = await this.getLmsUser(accessToken);
const course = await this.getActiveCourse();
const course = await this.getStudentCourse(student.id, dto.courseId);
const week = dto.week ? await this.getCourseWeek(course.id, dto.week) : null;
const thread = await this.getOrCreateThread({
courseId: course.id,
@@ -425,10 +426,11 @@ export class ChatService {
async cancelActiveAiResponse(
accessToken: string | undefined,
courseId: string,
weekNumber?: number,
) {
const student = await this.getLmsUser(accessToken);
const course = await this.getActiveCourse();
const course = await this.getStudentCourse(student.id, courseId);
const week = weekNumber ? await this.getCourseWeek(course.id, weekNumber) : null;
const message = await this.prisma.chatMessage.findFirst({
where: {
@@ -487,7 +489,7 @@ export class ChatService {
query: ListChatThreadsDto,
) {
const instructor = await this.getInstructorLmsUser(accessToken);
const course = await this.getActiveCourse();
const course = await this.getInstructorCourse(instructor.id, query.courseId);
const week = query.week
? await this.getCourseWeek(course.id, query.week)
: null;
@@ -680,11 +682,41 @@ export class ChatService {
};
}
private async getActiveCourse() {
private async getStudentCourse(studentId: string, courseId?: string) {
if (!courseId) throw new NotFoundException('Course not found');
const course = await this.prisma.course.findFirst({
where: { slug: 'futuredrive-autonomy-basic', isActive: true },
where: {
id: courseId,
isActive: true,
OR: [
{ slug: PUBLIC_GUIDE_COURSE_SLUG },
{
enrollments: {
some: {
userId: studentId,
status: 'ACTIVE',
},
},
},
],
},
});
if (!course) throw new NotFoundException('Curriculum course not found');
if (!course) throw new NotFoundException('Course not found');
return course;
}
private async getInstructorCourse(instructorId: string, courseId?: string) {
if (!courseId) throw new NotFoundException('Course not found');
const course = await this.prisma.course.findFirst({
where: {
id: courseId,
isActive: true,
teacherId: instructorId,
},
});
if (!course) throw new NotFoundException('Course not found');
return course;
}
@@ -0,0 +1,12 @@
import { IsInt, IsOptional, IsUUID, Max, Min } from 'class-validator';
export class CancelActiveAiResponseDto {
@IsUUID()
courseId: string;
@IsOptional()
@IsInt()
@Min(1)
@Max(100)
week?: number;
}
+4 -1
View File
@@ -1,6 +1,9 @@
import { IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
import { IsInt, IsOptional, IsString, IsUUID, Max, Min } from 'class-validator';
export class CreateAiResponseDto {
@IsUUID()
courseId: string;
@IsString()
content: string;
+4 -1
View File
@@ -1,6 +1,9 @@
import { IsIn, IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
import { IsIn, IsInt, IsOptional, IsString, IsUUID, Max, Min } from 'class-validator';
export class CreateChatMessageDto {
@IsUUID()
courseId: string;
@IsIn(['ai', 'teacher'])
mode: 'ai' | 'teacher';
+5 -1
View File
@@ -1,7 +1,11 @@
import { Transform } from 'class-transformer';
import { IsBoolean, IsIn, IsInt, IsOptional, Max, Min } from 'class-validator';
import { IsBoolean, IsIn, IsInt, IsOptional, IsUUID, Max, Min } from 'class-validator';
export class ListChatThreadsDto {
@IsOptional()
@IsUUID()
courseId?: string;
@IsOptional()
@IsIn(['ai', 'teacher'])
mode?: 'ai' | 'teacher';
@@ -0,0 +1,8 @@
import { IsNotEmpty, IsString, MaxLength } from 'class-validator';
export class CreateInitialInstructorCourseDto {
@IsString()
@IsNotEmpty()
@MaxLength(120)
title!: string;
}
+6
View File
@@ -6,6 +6,7 @@ import { InstructorService } from './instructor.service';
import { AssignInstructorPlatformDto } from './dto/assign-instructor-platform.dto';
import { CreateInstructorPlatformDto } from './dto/create-instructor-platform.dto';
import { CreateInstructorCourseMapDto } from './dto/create-instructor-course-map.dto';
import { CreateInitialInstructorCourseDto } from './dto/create-initial-instructor-course.dto';
import { CreateInstructorStudentDto } from './dto/create-instructor-student.dto';
import { SaveInstructorGroupDto } from './dto/save-instructor-group.dto';
import { SaveSubmissionScoreDto } from './dto/save-submission-score.dto';
@@ -26,6 +27,11 @@ export class InstructorController {
return this.instructorService.listCourses(this.getBearerToken(request));
}
@Post('courses/initial')
createInitialCourse(@Req() request: Request, @Body() body: CreateInitialInstructorCourseDto) {
return this.instructorService.createInitialCourse(this.getBearerToken(request), body.title);
}
@Get('submissions')
listSubmissions(@Req() request: Request) {
return this.instructorService.listSubmissions(this.getBearerToken(request));
+171
View File
@@ -31,6 +31,31 @@ import { NAV2_SYSTEM_MAPS } from '../curriculum/course-maps';
const INSTRUCTOR_ROLES = new Set(['INSTRUCTOR', 'LMS_ADMIN']);
const DEFAULT_SUBMISSION_BOT_EMAIL = 'lms-bot@futuredrive.net';
const COURSE_REFERENCE_UPLOAD_ROOT = join(process.cwd(), 'uploads', 'course-references');
const BASE_COURSE_SLUGS = ['futuredrive-autonomy-basic', 'yeungnam-autonomy-basic'];
function copyJson(value: Prisma.JsonValue): Prisma.InputJsonValue | Prisma.JsonNullValueInput {
return value === null ? Prisma.JsonNull : JSON.parse(JSON.stringify(value)) as Prisma.InputJsonValue;
}
function remapProfileModuleIds(value: Prisma.JsonValue, moduleIds: Map<string, string>) {
const config = copyJson(value);
if (!config || typeof config !== 'object' || Array.isArray(config)) return config;
const moduleSet = (config as Record<string, unknown>).moduleSet;
if (!moduleSet || typeof moduleSet !== 'object' || Array.isArray(moduleSet)) return config;
const modules = (moduleSet as Record<string, unknown>).modules;
if (!Array.isArray(modules)) return config;
for (const module of modules) {
if (!module || typeof module !== 'object' || Array.isArray(module)) continue;
const id = (module as Record<string, unknown>).id;
if (typeof id === 'string' && moduleIds.has(id)) {
(module as Record<string, unknown>).id = moduleIds.get(id)!;
}
}
return config;
}
type SubmissionCommitFile = {
path: string;
@@ -3612,6 +3637,152 @@ export class InstructorService {
return { authUser: user, instructor };
}
async createInitialCourse(accessToken: string | undefined, title: string) {
const { authUser, instructor } = await this.getInstructor(accessToken);
const organizationName = authUser.primaryAffiliation?.organizationName?.trim();
if (!organizationName) throw new BadRequestException('강사 소속 조직을 확인할 수 없습니다.');
const course = await this.createBaseCourseForInstructor(instructor.id, organizationName, title);
return {
course: {
id: course.id,
slug: course.slug,
title: course.title,
subtitle: course.subtitle,
description: course.description,
organizationName: course.organizationName,
},
};
}
private async createBaseCourseForInstructor(instructorId: string, organizationName: string, title: string) {
const courseTitle = title.trim();
if (!courseTitle) throw new BadRequestException('강좌명을 입력해 주세요.');
const existingCourse = await this.prisma.course.findFirst({
where: { teacherId: instructorId, isActive: true },
select: { id: true },
});
if (existingCourse) throw new ConflictException('이미 생성된 강좌가 있습니다.');
const template = await this.prisma.course.findFirst({
where: { slug: { in: BASE_COURSE_SLUGS }, isActive: true },
include: {
weeks: {
orderBy: { week: 'asc' },
include: {
topics: { orderBy: { sortOrder: 'asc' } },
practices: { orderBy: { sortOrder: 'asc' } },
references: { orderBy: [{ sortOrder: 'asc' }, { createdAt: 'asc' }] },
modules: { orderBy: { sortOrder: 'asc' } },
},
},
environmentProfiles: { orderBy: [{ week: 'asc' }, { platformKey: 'asc' }] },
maps: { orderBy: { key: 'asc' } },
weekMaps: { orderBy: [{ week: 'asc' }, { sortOrder: 'asc' }] },
},
});
if (!template) {
this.logger.error('Base NAV2 course was not found; skipping instructor course initialization.');
throw new NotFoundException('베이스 강좌를 찾을 수 없습니다.');
}
const courseSlug = `nav2-autonomy-${this.slug(instructorId).slice(0, 12)}`;
return this.prisma.$transaction(async (tx) => {
const courseAlreadyCreated = await tx.course.findFirst({
where: { teacherId: instructorId, isActive: true },
select: { id: true },
});
if (courseAlreadyCreated) throw new ConflictException('이미 생성된 강좌가 있습니다.');
const course = await tx.course.create({
data: {
slug: courseSlug,
title: courseTitle,
subtitle: template.subtitle,
description: template.description,
organizationName,
teacherId: instructorId,
isActive: true,
},
});
const moduleIds = new Map<string, string>();
for (const sourceWeek of template.weeks) {
const week = await tx.courseWeek.create({
data: {
courseId: course.id,
week: sourceWeek.week,
title: sourceWeek.title,
tags: sourceWeek.tags,
progress: sourceWeek.progress,
state: sourceWeek.state,
duration: sourceWeek.duration,
github: sourceWeek.github,
overview: sourceWeek.overview,
deliverable: sourceWeek.deliverable,
requiresSourceSubmission: sourceWeek.requiresSourceSubmission,
},
});
await Promise.all([
tx.courseWeekTopic.createMany({ data: sourceWeek.topics.map((topic) => ({ weekId: week.id, title: topic.title, detail: topic.detail, sortOrder: topic.sortOrder })) }),
tx.courseWeekPractice.createMany({ data: sourceWeek.practices.map((practice) => ({ weekId: week.id, title: practice.title, description: practice.description, sortOrder: practice.sortOrder })) }),
tx.courseWeekReference.createMany({ data: sourceWeek.references.map((reference) => ({ weekId: week.id, kind: reference.kind, title: reference.title, originalFileName: reference.originalFileName, storedFileName: reference.storedFileName, contentType: reference.contentType, fileSizeBytes: reference.fileSizeBytes, sortOrder: reference.sortOrder })) }),
]);
for (const sourceModule of sourceWeek.modules) {
const module = await tx.courseWeekModule.create({
data: {
weekId: week.id,
sortOrder: sourceModule.sortOrder,
category: sourceModule.category,
name: sourceModule.name,
folderName: sourceModule.folderName,
templateRepositoryUrl: sourceModule.templateRepositoryUrl,
sourceType: sourceModule.sourceType,
prebuiltArtifacts: copyJson(sourceModule.prebuiltArtifacts),
defaultParameters: copyJson(sourceModule.defaultParameters),
dockerImage: sourceModule.dockerImage,
},
});
moduleIds.set(sourceModule.id, module.id);
}
}
await Promise.all([
...template.environmentProfiles.map((profile) => tx.courseEnvironmentProfile.create({
data: {
courseId: course.id,
week: profile.week,
platformKey: profile.platformKey,
profileType: profile.profileType,
name: profile.name,
config: remapProfileModuleIds(profile.config, moduleIds),
enabled: profile.enabled,
},
})),
...template.maps.map((map) => tx.courseMap.create({
data: {
courseId: course.id,
key: map.key,
name: map.name,
description: map.description,
platformKey: map.platformKey,
worldFile: map.worldFile,
viewerModel: map.viewerModel,
layout: copyJson(map.layout),
},
})),
...template.weekMaps.map((weekMap) => tx.courseWeekMap.create({
data: { courseId: course.id, week: weekMap.week, mapKey: weekMap.mapKey, sortOrder: weekMap.sortOrder },
})),
]);
return course;
});
}
private toUserSyncData(user: AuthResponseUser) {
return {
authUserId: user.id,