diff --git a/apps/control-station/src/App.tsx b/apps/control-station/src/App.tsx index 41e3d80..2d14ad3 100644 --- a/apps/control-station/src/App.tsx +++ b/apps/control-station/src/App.tsx @@ -47,7 +47,6 @@ import type { import { useRecordedSessionAdmission } from "./core/observation/useRecordedSessionAdmission"; import { useWorkspaceLayoutProfile } from "./core/observation/useWorkspaceLayoutProfile"; import { resolvePolygonRunRoute } from "./core/polygon/runArchive"; -import { usePolygonWorkerCapability } from "./core/polygon/usePolygonWorkerCapability"; import { OBSERVATION_WORKSPACE_ID, OBSERVATION_WORKSPACE_LAYOUT_VERSION, @@ -55,7 +54,7 @@ import { } from "./core/observation/workspaceLayout"; import { rootById, - rootsForCapabilities, + roots, workspaceById, workspacesForRoot, type RootId, @@ -151,7 +150,6 @@ function mergeViewerSettings( export default function App() { const runtime = useMissionRuntime(); const { selection } = useDevicePluginHost(); - const polygonWorker = usePolygonWorkerCapability(); const polygonRunRoute = useMemo( () => resolvePolygonRunRoute(typeof window === "undefined" ? "" : window.location.search), [], @@ -199,10 +197,7 @@ export default function App() { const polygonRunRouteOpenedRef = useRef(false); const currentRoot = rootById(activeRoot); - const visibleRoots = useMemo( - () => rootsForCapabilities({ polygonWorkerAvailable: polygonWorker.available }), - [polygonWorker.available], - ); + const visibleRoots = roots; const activeDefinition = workspaceById(workspace.activeView); const spatialWorkspaceActive = Boolean( workspace.contentOpen && activeDefinition?.kind === "spatial", diff --git a/apps/control-station/src/productModel.ts b/apps/control-station/src/productModel.ts index 8e40748..4e67628 100644 --- a/apps/control-station/src/productModel.ts +++ b/apps/control-station/src/productModel.ts @@ -796,14 +796,6 @@ export function rootById(id: RootId | null): RootDefinition | null { return id ? roots.find((root) => root.id === id) ?? null : null; } -export function rootsForCapabilities({ - polygonWorkerAvailable, -}: { - polygonWorkerAvailable: boolean; -}): RootDefinition[] { - return roots.filter((root) => root.id !== "polygon" || polygonWorkerAvailable); -} - export function workspaceById(id: string | null): WorkspaceDefinition | null { return id ? workspaces.find((workspace) => workspace.id === id) ?? null : null; } diff --git a/apps/control-station/src/styles/workspaces.css b/apps/control-station/src/styles/workspaces.css index aa49599..a14a27c 100644 --- a/apps/control-station/src/styles/workspaces.css +++ b/apps/control-station/src/styles/workspaces.css @@ -1441,9 +1441,8 @@ .lidar-field-cloud { overflow: hidden; min-width: 0; - border: 1px solid rgb(255 255 255 / 0.09); border-radius: 0.9rem; - background: #071018; + background: #06070a; } .lidar-field-camera { @@ -1461,7 +1460,6 @@ .lidar-field-camera > .lidar-field-stage__label, .lidar-field-cloud > header { min-height: 3.45rem; - border-bottom: 1px solid rgb(255 255 255 / 0.08); padding: 0.66rem 0.72rem; } @@ -1487,7 +1485,6 @@ display: flex; justify-content: space-between; gap: 0.5rem; - border-top: 1px solid rgb(255 255 255 / 0.08); padding: 0.52rem 0.7rem; } @@ -1548,7 +1545,7 @@ overflow: hidden; margin-top: 0.8rem; border-radius: 0.9rem; - background: #071018; + background: #06070a; } .lidar-fallback-review > header { @@ -1557,7 +1554,6 @@ align-items: center; justify-content: space-between; gap: 0.8rem; - border-bottom: 1px solid rgb(255 255 255 / 0.08); padding: 0.65rem 0.75rem; } @@ -1569,7 +1565,6 @@ } .lidar-fallback-review > footer { - border-top: 1px solid rgb(255 255 255 / 0.08); padding: 0.58rem 0.75rem; color: var(--nodedc-text-muted); font-size: 0.58rem; @@ -1646,11 +1641,8 @@ display: grid; gap: 0.75rem; margin-top: 0.85rem; - border: 1px solid rgb(74 215 255 / 0.18); border-radius: 1rem; - background: - radial-gradient(circle at 18% 0%, rgb(56 124 255 / 0.12), transparent 34%), - rgb(4 12 19 / 0.72); + background: #06070a; padding: 0.85rem; } @@ -1716,9 +1708,9 @@ .lidar-ground-modes button, .lidar-ground-frame-control button, .lidar-ground-scene__toolbar button { - border: 1px solid var(--station-hairline); + border: 0; border-radius: 999px; - background: rgb(255 255 255 / 0.035); + background: transparent; padding: 0.4rem 0.62rem; color: var(--nodedc-text-secondary); font: inherit; @@ -1730,9 +1722,16 @@ .lidar-ground-modes button[data-active="true"], .lidar-ground-frame-control button:hover:not(:disabled), .lidar-ground-scene__toolbar button:hover { - border-color: rgb(74 215 255 / 0.48); - background: rgb(74 215 255 / 0.1); + background: rgb(255 255 255 / 0.11); color: var(--nodedc-text-primary); + outline: none; +} + +.lidar-ground-modes button:focus-visible, +.lidar-ground-frame-control button:focus-visible, +.lidar-ground-scene__toolbar button:focus-visible { + background: rgb(255 255 255 / 0.14); + outline: none; } .lidar-ground-frame-control button { @@ -1751,16 +1750,15 @@ .lidar-ground-frame-control input { width: min(16rem, 24vw); - accent-color: #4ad7ff; + accent-color: #f2f2f2; } .lidar-ground-scene { position: relative; overflow: hidden; min-height: 30rem; - border: 1px solid rgb(255 255 255 / 0.09); border-radius: 0.9rem; - background: #071018; + background: #06070a; } .lidar-ground-scene__viewport { @@ -1782,15 +1780,15 @@ display: flex; align-items: center; gap: 0.5rem; - border: 1px solid rgb(255 255 255 / 0.08); + border: 0; border-radius: 999px; - background: rgb(5 13 21 / 0.82); + background: rgb(10 11 14 / 0.78); padding: 0.28rem; backdrop-filter: blur(14px); } .lidar-ground-scene__toolbar button { - background: rgb(74 215 255 / 0.08); + background: rgb(255 255 255 / 0.06); } .lidar-ground-scene__error { @@ -1808,9 +1806,8 @@ place-content: center; justify-items: center; gap: 0.55rem; - border: 1px solid var(--station-hairline); border-radius: 0.9rem; - background: #071018; + background: #06070a; text-align: center; } diff --git a/apps/control-station/src/workspaces/LidarGroundPointCloud.tsx b/apps/control-station/src/workspaces/LidarGroundPointCloud.tsx index 8faee87..073104e 100644 --- a/apps/control-station/src/workspaces/LidarGroundPointCloud.tsx +++ b/apps/control-station/src/workspaces/LidarGroundPointCloud.tsx @@ -60,12 +60,13 @@ function frameColors( const candidateAssigned = frame.masks.candidateAssigned[index] === 1; if (mode === "intensity") { const intensity = (frame.intensity0To255?.[index] ?? 96) / 255; + const neutral = 0.16 + intensity * 0.8; setRgb( colors, offset, - 0.12 + intensity * 0.74, - 0.24 + intensity * 0.68, - 0.34 + intensity * 0.6, + neutral, + neutral, + neutral, ); } else if (mode === "current") { setRgb( @@ -131,7 +132,7 @@ export function LidarGroundPointCloud({ } renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.outputColorSpace = THREE.SRGBColorSpace; - renderer.setClearColor(0x071018, 0.96); + renderer.setClearColor(0x06070a, 1); renderer.domElement.setAttribute( "aria-label", "Интерактивное облако LiDAR", @@ -139,7 +140,7 @@ export function LidarGroundPointCloud({ host.prepend(renderer.domElement); const scene = new THREE.Scene(); - const fog = new THREE.FogExp2(0x071018, 0.035); + const fog = new THREE.FogExp2(0x06070a, 0.035); scene.fog = fog; fogRef.current = fog; const camera = new THREE.PerspectiveCamera(48, 1, 0.01, 1_000); @@ -172,7 +173,7 @@ export function LidarGroundPointCloud({ materialRef.current = material; scene.add(new THREE.Points(geometry, material)); - const grid = new THREE.GridHelper(10, 40, 0x71963d, 0x26312a); + const grid = new THREE.GridHelper(10, 40, 0x454846, 0x252725); gridRef.current = grid; const gridMaterials = Array.isArray(grid.material) ? grid.material diff --git a/apps/control-station/src/workspaces/PolygonRoverScene.tsx b/apps/control-station/src/workspaces/PolygonRoverScene.tsx index eb1fecf..7bfb962 100644 --- a/apps/control-station/src/workspaces/PolygonRoverScene.tsx +++ b/apps/control-station/src/workspaces/PolygonRoverScene.tsx @@ -378,7 +378,7 @@ export function PolygonRoverScene({ {workerAvailable ? "Запустите PX4/Gazebo — модель останется доступна для осмотра." - : "Полигон появится в шапке после подтверждения Simulation Worker."} + : "Прогоны и датасеты доступны; запуск и управление откроются после подтверждения Simulation Worker."} ) : null} diff --git a/apps/control-station/test/polygonRunArchive.test.mjs b/apps/control-station/test/polygonRunArchive.test.mjs index 2f7072e..5b18787 100644 --- a/apps/control-station/test/polygonRunArchive.test.mjs +++ b/apps/control-station/test/polygonRunArchive.test.mjs @@ -21,7 +21,7 @@ let stopPolygonWorker; let PolygonWorkerContractError; let workspaceById; let workspacesForRoot; -let rootsForCapabilities; +let roots; before(async () => { server = await createServer({ @@ -51,7 +51,7 @@ before(async () => { ({ workspaceById, workspacesForRoot, - rootsForCapabilities, + roots, } = await server.ssrLoadModule("/src/productModel.ts")); }); @@ -290,7 +290,7 @@ function commandAcceptance(overrides = {}) { }; } -test("Polygon is a worker-gated product root and keeps a compatible direct route", () => { +test("Polygon stays visible while worker capability gates live actions", () => { assert.deepEqual(resolvePolygonRunRoute("?workspace=polygon-run"), { active: true, runId: null, @@ -320,16 +320,7 @@ test("Polygon is a worker-gated product root and keeps a compatible direct route workspacesForRoot("system").some(({ id }) => id === "polygon-run"), false, ); - assert.equal( - rootsForCapabilities({ polygonWorkerAvailable: false }) - .some(({ id }) => id === "polygon"), - false, - ); - assert.equal( - rootsForCapabilities({ polygonWorkerAvailable: true }) - .some(({ id }) => id === "polygon"), - true, - ); + assert.equal(roots.some(({ id }) => id === "polygon"), true); }); test("Polygon contracts decode path-free evidence and preserve the safety boundary", () => { diff --git a/docs/01_IMPLEMENTATION_PLAN.md b/docs/01_IMPLEMENTATION_PLAN.md index dc63b7b..b5d1e43 100644 --- a/docs/01_IMPLEMENTATION_PLAN.md +++ b/docs/01_IMPLEMENTATION_PLAN.md @@ -164,8 +164,8 @@ fresh loopback-only namespace, the run reached `completed`, and both provider groups stopped without residue. S1C commit `b7ccca3` adds a persistent unprivileged worker agent in the same isolation boundary, a bounded Unix-socket gateway, an explicit `internal-virtual-only` backend gate and a browser-native -live panel. UI-2 commit `60e7916` promotes that panel into a worker-gated -top-level product root and replaces the diagnostic 2D marker with a +live panel. UI-2 commit `60e7916` promotes that panel into a top-level product +root and replaces the diagnostic 2D marker with a browser-native Three.js Ackermann Rover scene. The exact D-only generation passed 35 target tests and one UI-driven provider run with diagnostic Gazebo `VehicleState`, eight persisted lifecycle events, terminal `completed` and zero diff --git a/docs/12_SIMULATION_POLYGON_PRODUCT_AND_SRS.md b/docs/12_SIMULATION_POLYGON_PRODUCT_AND_SRS.md index f7fbf08..28dbaa8 100644 --- a/docs/12_SIMULATION_POLYGON_PRODUCT_AND_SRS.md +++ b/docs/12_SIMULATION_POLYGON_PRODUCT_AND_SRS.md @@ -834,15 +834,16 @@ safety policy. ## 20. UI gate -Polygon is implemented as a capability-gated seventh top-level bounded context: +Polygon is implemented as a permanent seventh top-level bounded context: ```text -Center | Fleet | Observation | Missions | Data | Polygon* | System - -* visible only while Mission Core confirms an admitted available Simulation Worker +Center | Fleet | Observation | Missions | Data | Polygon | System ``` -Two UI gates are intentionally distinct. +The product surface and execution capability are intentionally distinct. +`Полигон` and its dataset catalog remain visible without a worker. Worker +capability gates only live lifecycle and command actions, which fail closed and +show an explicit offline state. The Polygon navigation is task-oriented: @@ -886,7 +887,7 @@ read-only evidence contract backed by the server-owned run repository and is composed into UI-2. A shared review deployment requires a reviewed read-only D mount or a co-located backend; SSH is not a product data plane. -### UI-2 — worker-gated top-level Polygon +### UI-2 — permanent Polygon with worker-gated live actions The full UI-2 control/scenario gate begins only after: @@ -894,8 +895,10 @@ The full UI-2 control/scenario gate begins only after: 2. S1 target backend lifecycle and command authority are proven; 3. run history is persisted independently of a browser session. -Commit `60e7916` accepts the presentation/lifecycle slice before all three -conditions are complete: conditional header root, direct workspace, +Commit `60e7916` accepted the presentation/lifecycle slice before all three +conditions were complete. The root is now permanent so datasets and persisted +evidence remain reachable while the worker is offline; live start/stop and +commands remain capability-gated. The slice includes the direct workspace, virtual-only start/stop, 3D rover inspection, diagnostic ENU pose and read-only history. It does not claim command authority. Scenarios, pause/step/reset, canonical rover commands, Compare and Report remain planned. UI-2 cannot own diff --git a/docs/adr/0015-simulation-polygon-qualification-boundary.md b/docs/adr/0015-simulation-polygon-qualification-boundary.md index 03204f4..2c405bf 100644 --- a/docs/adr/0015-simulation-polygon-qualification-boundary.md +++ b/docs/adr/0015-simulation-polygon-qualification-boundary.md @@ -55,9 +55,10 @@ reports easy to misuse. 13. S1 uses exact `px4_msgs` directly. The experimental `px4-ros2-interface-lib` is not a mandatory S1 dependency. 14. The read-only direct UI-0 run view consumes proven target S1B persisted - history through GET-only server routes. It is hidden from navigation and - cannot mutate a run. Top-level Polygon UI and control remain gated on - accepted S1 target command/lifecycle behavior. + history through GET-only server routes and cannot mutate a run. The + top-level Polygon product surface remains visible so datasets and persisted + evidence are reachable offline; live lifecycle and command actions remain + gated on admitted worker capability and accepted S1 target behavior. 15. Real actuator authority requires a new decision and physical safety gate. 16. S0 processes run inside an ephemeral loopback-only Linux network namespace. Micro XRCE-DDS Agent's UDP wildcard bind is acceptable only inside that