From 38b61ea6f53a7257a2475cad05c192edf426f4f4 Mon Sep 17 00:00:00 2001 From: mathewdunne <73080543+mathewdunne@users.noreply.github.com> Date: Sat, 12 Sep 2026 17:51:24 -0400 Subject: [PATCH 1/5] Create preview-plan.md --- preview-plan.md | 129 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 preview-plan.md diff --git a/preview-plan.md b/preview-plan.md new file mode 100644 index 00000000..4e41d763 --- /dev/null +++ b/preview-plan.md @@ -0,0 +1,129 @@ +# Project Preview implementation plan + +Status: proposed; implementation has not started. + +## Goal and agreed UX + +Let students read project Markdown and generated HTML reports beside VSCodium without depending on VSCodium's preview or a runtime CDN. + +- Keep the existing topbar toggle in its current position and preserve its pill styling. Add **Preview** after AdvantageScope and PathPlanner, using `FileText` from the already-installed `lucide-react` package. Use a decorative icon and a visible text label. +- Display Preview in the existing resizable right pane. Do not add another permanent pane or move the selector into the pane. +- Give Preview a compact toolbar containing a searchable document picker and a visible **Refresh** button (`RefreshCw`). Reserve space above the content for this toolbar; only the open picker menu overlays content. +- Show the filename and project-relative path in search results so repeated names such as `index.html` are distinguishable. Keep the selected path accessible in the closed toolbar, with truncation only for display. +- On first opening Preview for a project, select the root `README.md`, case-insensitively, when present. Otherwise show the picker and an instruction to choose a document; do not automatically open a license or an arbitrary report. +- Keep the selected entry while switching between the three views. Preserve the existing mounted AdvantageScope and PathPlanner instances. Do not introduce scroll-position storage or restoration. +- **Refresh reloads both the document list and the selected document from disk.** It recreates the iframe and starts the document at the top. It must also pick up changed CSS, images, and scripts. No auto-refresh, watchers, polling, or automatic view changes after a run. +- If the selected file disappears, clear its preview and say that the file is no longer available. Keep the refreshed picker usable. If no documents exist, show “No Markdown or HTML files found. Add a document or generate a report, then refresh.” +- Switching/resetting a project or importing a repository clears the old file selection and content, cancels stale requests, and loads the new project's list when Preview is next visible. Selection need not survive a full browser reload. + +The picker identifies the selected entry document. Links inside an HTML report can navigate within its iframe; Refresh reopens the selected entry document, including when the reader has navigated to a report subpage. This avoids a cross-frame navigation bridge in the first version. + +## Scope boundary + +Include `.md`, `.html`, and `.htm` files, matching extensions case-insensitively. The picker is a file opener, not a second project explorer. Include generated output such as `build/reports/**`, even when gitignored. Do not use `.gitignore` as the discovery filter. + +Exclude `.git`, `.gradle`, and `node_modules` directory trees from discovery and serving, and reject symlinks. Do not blanket-exclude hidden directories: project-authored documentation in another hidden directory remains eligible. Apply explicit traversal/read budgets and disclose incomplete results instead of silently claiming to list everything. + +Defer VSCodium context-menu integration, multiple document tabs, a separate-window action, editing, automatic report detection after runs, scroll restoration, and a new mobile layout. The first version targets the existing desktop right-pane layout; its current 901px visibility breakpoint remains applicable. + +**Planning assumption:** Preview should also work in plain-Java lessons, because their instructions and test reports are useful without simulation. Those lessons currently hide all right-pane and Driver Station UI. For them, put a Preview show/hide action at the existing topbar selector location, reveal only the right Preview pane on demand, and retain the editor's Run hint. Do not start simulation hooks or show AS, PP, or Driver Station for these lessons. This is a proposed scope extension, not an explicitly confirmed user requirement. If the user chooses the existing three-pane layout only, omit this extension and its tests; the rest of the plan is unchanged. + +## Existing integration points + +Inspected against repository HEAD `004d710`. The graph report was read for navigation; its recorded commit is older, so the current source is authoritative. + +- `apps/web/src/components/SimPaneSwitcher.tsx` owns the `scope | pathplanner` values, session-stored view choice, topbar selector, and mounted panels. Extend this surface rather than building a separate navigation mechanism. +- `apps/web/src/components/Topbar.tsx` places the selector immediately before Switch project. Its location stays unchanged. +- `apps/web/src/routes/WorkspacePage.tsx` supplies the workspace slug and increments `reloadNonce` after project replacement. Reuse that invalidation for Preview. Preview uses `workspaceSlug`, not the simulation-only `simSlug`. +- `apps/web/src/components/IDELayout.tsx` owns pane resizing and currently combines right-pane and Driver Station visibility in `showSimPanels`. Separate those concerns only if implementing the plain-Java extension. +- `apps/control/src/app/workspace-routes.ts` already performs `requireWorkspaceOwnership` before dispatching workspace APIs. `auth.workspace.project_path` identifies the project directory visible to the control plane. +- `apps/control/src/app/deploy-files.ts` contains relevant path-containment and descriptor-check patterns, but its API is scoped to PathPlanner deploy files. Add a separate read-only Preview handler; do not broaden deploy-files permissions. +- `packages/contracts/src/index.ts` is the home for shared schemas. `apps/control/src/metrics.ts` templates route labels; preview filenames must not become metric labels. + +## Rendering and API design + +Use the Bun control plane to list and read project files directly. This needs no VSCodium extension changes, new per-student server, published container port, or running simulator. + +Proposed authenticated routes: + +| Route | Behavior | +| --- | --- | +| `GET /u/:slug/api/preview/documents` | Return `{ ok: true, documents: [{ path, kind }], truncated }`, with `kind` equal to `markdown` or `html`. Paths are relative to the project root; do not return file contents or host paths. | +| `GET /u/:slug/api/preview/files/` | Render Markdown as HTML, serve HTML documents, or serve an allowed local report asset with the correct MIME type. Keep the project-relative directory hierarchy in the URL. | + +Both routes, including every asset and linked page request, use existing session-cookie ownership checks. Unauthenticated API requests return an auth error rather than a login page embedded as a document. Only GET is supported initially; reject mutation methods. + +Serve Markdown through a renderer bundled with the control application, with local CSS and system fonts. Use `markdown-it`, installed with Bun and committed in the lockfile; confirm its version and TypeScript support at implementation time. Support headings, lists, fenced code, tables, links, and local images. Keep raw embedded HTML disabled initially and document that limit. Add stable heading anchors so intra-document links work. No CDN-based renderer, fonts, syntax highlighting, or plugins are required. The library's [official documentation](https://github.com/markdown-it/markdown-it) describes its parser API and configurable syntax. + +Preserve HTML reports' own markup and styling rather than applying Markdown styling to them. Support relative links between report pages and local CSS, classic JavaScript, and images, including references to parent directories that remain inside the project. Keep fragments and query strings intact. Explicitly handle URL encoding for spaces, Unicode, `#`, and `%` in filenames. Reject paths that escape the project; do not reinterpret arbitrary root-relative app URLs as project paths. + +The initial compatibility target is a generated Gradle test report and ordinary static HTML, not arbitrary web applications. Reports requiring remote CDNs, backend APIs, ES modules with cross-origin requirements, or browser storage may need later compatibility work. Markdown should work with external requests blocked; externally hosted images and resources are not made offline by this feature. + +### Isolation and bounded reads + +Project HTML is executable student-controlled content. Use an iframe sandbox with `allow-scripts` for report interactivity and **without `allow-same-origin`**. Apply the sandbox policy in HTTP response headers as well, so opening a raw report URL does not bypass isolation. Markdown documents do not need script execution. MDN documents both the [iframe sandbox restrictions](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/iframe) and the [HTTP CSP sandbox directive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/sandbox). + +Restrict resource loading to the authenticated preview resource namespace and explicitly permitted data images; allow the inline styles/scripts needed for static reports. Block network API connections, forms, workers, nested frames, automatic popups, and top-level navigation. Do not grant general app-origin script access or credentialed CORS for opaque origins. Use `nosniff`, `Referrer-Policy: no-referrer`, and private `no-store` responses for the list, rendered documents, and local assets. Apply document isolation to directly navigable SVG assets too. Check compatibility with the app's existing security-header wrapper rather than weakening global headers. + +Use a narrow MIME allowlist for report assets, initially CSS, classic JS, common images including SVG, and fonts as demonstrated by the compatibility fixture. Do not expose arbitrary source/config files through the asset handler. All allowed resources still receive ownership, path, size, and regular-file checks. + +Validate and decode each requested path once. Reject absolute paths, malformed encodings, traversal, prohibited directory components, symlinks, and special files. Verify actual filesystem containment when opening files, including directory-symlink races; reuse the existing Linux descriptor-verification approach where appropriate and document platform limitations. Read from the verified descriptor rather than reopening a checked path. + +Starting budgets: 2,000 discovered documents, 20,000 visited entries, maximum depth 32, 10 MiB per Markdown/HTML document, and 25 MiB per asset. Walk incrementally with bounded work and do not read all document bodies when listing. Report truncation when any discovery budget is reached. Enforce read caps even if files grow after inspection. Tune these constants only against representative report fixtures. + +## Implementation sequence + +### 1. Prove the report delivery model + +- [ ] Add a small representative Gradle report fixture with multiple HTML pages, local CSS, images, and an interactive classic script, plus Markdown fixtures with local links/images. +- [ ] Exercise cookie authentication, opaque-origin sandboxing, HTTP sandbox headers, relative asset loading, report navigation, and full Refresh behavior in a real browser using the mocked E2E harness. +- [ ] Verify that report scripts cannot reach the shell DOM, app storage, or authenticated control APIs. Verify direct navigation remains sandboxed. +- [ ] Record the tested compatibility boundary in a new decision log using the next available number. If essential Gradle behavior fails, resolve the serving architecture before proceeding; do not fix it by combining same-origin privileges with report scripts. + +This is the main technical uncertainty. The UI layout does not depend on its implementation details. + +### 2. Add contracts and read-only delivery + +- [ ] Add path, document-kind, document-entry, and list-response schemas to `packages/contracts/src/index.ts`, with contract tests. +- [ ] Create `apps/control/src/app/preview.ts` for discovery and serving; use a separate rendering helper if that keeps responsibilities clearer. +- [ ] Add the Markdown dependency to `apps/control/package.json` with Bun and update `bun.lock`. +- [ ] Wire the routes behind existing ownership checks in `workspace-routes.ts`. Add bounded metric templates in `metrics.ts`. +- [ ] Cover nested/generated files, duplicate filenames, case-insensitive extensions, exclusions, budgets, disappearing files, read limits, URL encoding, wrong users, unauthenticated requests, traversal, symlinks, special files, and unsupported MIME types in control-plane tests. + +### 3. Add Preview UI + +- [ ] Extend `SimPaneSwitcher.tsx` to accept `preview` in stored-value parsing, activation, selector, and panel composition. Keep AS as the default when no valid preference exists. Change the accessible group name from “Simulation pane” to “Right pane”. +- [ ] Add `PreviewPane.tsx` and `usePreviewDocuments.ts`. Reuse existing button and Base UI interaction conventions for the searchable picker; ensure keyboard search, selection, Escape, focus return, and a clear selected item. +- [ ] Load the file list on first activation, project invalidation, and explicit Refresh only. Keep Preview mounted after first use so its selection survives tab changes; do not add scroll tracking. +- [ ] Refresh starts a new list request and resets the iframe. Reopen the selected file if it remains listed; show a missing-file state otherwise. If list refresh fails, show an actionable error without presenting the old list as fresh. +- [ ] Model loading, ready, empty, missing-file, oversized-file, and request-error states. Render escaped error responses inside the iframe for document failures; an iframe load event alone must not imply an HTTP success. +- [ ] Prevent outdated requests from a prior selection or project from winning. Use an abort controller or request generation, and remount/reset on `reloadNonce`. +- [ ] Integrate Preview in `WorkspacePage.tsx`. Leave the topbar selector's position and existing pane dimensions unchanged. +- [ ] If the plain-Java extension is in scope, split layout visibility flags, add the topbar Preview show/hide action, and keep simulator hooks disabled. + +### 4. Validate the complete workflow + +- [ ] Extend `SimPaneSwitcher.test.tsx` for the third state, session preference, keyboard navigation, and preservation of existing AS/PP instances. +- [ ] Add frontend tests for searching full paths, README selection, no-README behavior, explicit Refresh of both resources, missing files, fetch errors, stale-response cancellation, and project-swap reset. +- [ ] Add mocked E2E flows that open Markdown with all external requests blocked; open a generated report, navigate its pages, and exercise its script; change/add/delete files on disk and click Refresh; switch to AS/PP and back; and replace the project while Preview is active. +- [ ] In E2E, assert refreshed CSS/script/image contents as well as refreshed HTML and file-list contents. Verify a previously scrolled document starts at the top after Refresh. Do not test scroll preservation. +- [ ] Add security E2E coverage for shell isolation, direct report URL isolation, cross-workspace access, and blocked API calls. Reuse existing auth and filesystem fixtures. +- [ ] Check desktop topbar fit at 901px, 1024px, and the supplied screenshot's general layout, long filenames, a narrow resized right pane, and keyboard-only interaction. Retain the current below-901px layout boundary. + +### 5. Document and finish + +- [ ] Add a student-facing Preview page under `docs/lessons/` describing selection, Refresh, report entry-page behavior, generated output discovery, and supported document/resource types. Update `docs/reference/faq.md` with the school-network workaround and external-resource limitation. +- [ ] Update `docs/about/security-model.md`, `docs/about/architecture.md`, and `docs/development/testing.md` as relevant to the final implementation. Keep the decision log aligned with the actual serving policy. +- [ ] Run `bun run check:fix`, then `bun run verify`; build the docs with `bun run docs:build`. Run `graphify update .` after code changes and inspect the final diff. +- [ ] No real-image Java smoke is required unless implementation unexpectedly changes the workspace editor, JDK, extensions, or init logic. + +## Acceptance criteria + +1. Preview appears beside AdvantageScope and PathPlanner in the current topbar position with a Lucide document icon. +2. A student can read the project's README without a third-party renderer request and can select nested Markdown or generated HTML through search. +3. A representative Gradle report renders with its local assets, page navigation, and basic script interactivity. +4. One Refresh action rescans files and fully reloads the selected entry and its assets, starting at the top. Newly generated reports become selectable immediately afterward. +5. Switching views preserves AS/PP state and the selected Preview entry. Replacing the project clears old content and selection. +6. Missing/empty/error states are understandable, and large trees/files have explicit limits rather than unbounded control-plane work. +7. Project content cannot read another workspace, escape the project root, or execute with the CodeRunner shell's privileges. +8. No auto-refresh or scroll-restoration subsystem is introduced. From 5556174b67c757f1f04c2253aeeb85f07545a83d Mon Sep 17 00:00:00 2001 From: Mathew Dunne Date: Tue, 15 Sep 2026 12:49:10 -0400 Subject: [PATCH 2/5] added preview pane and ability to collapse panes --- AGENTS.md | 26 +- apps/control/package.json | 4 + apps/control/src/__tests__/preview.test.ts | 696 + apps/control/src/app.ts | 8 +- apps/control/src/app/assets.ts | 34 + apps/control/src/app/preview-markdown.ts | 132 + apps/control/src/app/preview-token.ts | 88 + apps/control/src/app/preview.ts | 492 + apps/control/src/app/workspace-routes.ts | 41 + apps/control/src/metrics.ts | 5 + .../DriverStation/DriverStation.test.tsx | 53 + .../DriverStation/DriverStation.tsx | 11 +- apps/web/src/components/IDELayout.test.tsx | 27 +- apps/web/src/components/IDELayout.tsx | 416 +- apps/web/src/components/LayoutMenu.tsx | 53 + apps/web/src/components/PreviewPane.test.tsx | 288 + apps/web/src/components/PreviewPane.tsx | 364 + .../src/components/SimPaneSwitcher.test.tsx | 101 +- apps/web/src/components/SimPaneSwitcher.tsx | 86 +- apps/web/src/components/Topbar.tsx | 35 +- apps/web/src/components/UserMenu.tsx | 5 +- apps/web/src/components/ui/resizable.tsx | 3 +- apps/web/src/hooks/usePaneVisibility.test.ts | 56 + apps/web/src/hooks/usePaneVisibility.ts | 116 + .../web/src/hooks/usePreviewDocuments.test.ts | 304 + apps/web/src/hooks/usePreviewDocuments.ts | 143 + apps/web/src/index.css | 28 + apps/web/src/lib/contracts.ts | 3 + apps/web/src/routes/WorkspacePage.tsx | 84 +- bun.lock | 24 +- docs/about/architecture.md | 9 + docs/about/security-model.md | 15 + docs/decisions/041-project-preview.md | 143 + .../042-collapsible-workspace-panes.md | 39 + docs/decisions/README.md | 2 + docs/development/testing.md | 8 +- docs/lessons/preview.md | 38 + docs/using-coderunner.md | 25 +- e2e/fixtures/preview-project.ts | 183 + e2e/specs/preview/console-lesson.spec.ts | 111 + e2e/specs/preview/delivery.spec.ts | 297 + e2e/specs/preview/workflow.spec.ts | 304 + e2e/specs/security/preview-isolation.spec.ts | 235 + e2e/specs/workspace/pane-layout.spec.ts | 151 +- graphify-out/GRAPH_REPORT.md | 938 +- graphify-out/graph.html | 8 +- graphify-out/graph.json | 12848 +++++++++++----- packages/contracts/src/index.ts | 88 + 48 files changed, 14683 insertions(+), 4485 deletions(-) create mode 100644 apps/control/src/__tests__/preview.test.ts create mode 100644 apps/control/src/app/preview-markdown.ts create mode 100644 apps/control/src/app/preview-token.ts create mode 100644 apps/control/src/app/preview.ts create mode 100644 apps/web/src/components/DriverStation/DriverStation.test.tsx create mode 100644 apps/web/src/components/LayoutMenu.tsx create mode 100644 apps/web/src/components/PreviewPane.test.tsx create mode 100644 apps/web/src/components/PreviewPane.tsx create mode 100644 apps/web/src/hooks/usePaneVisibility.test.ts create mode 100644 apps/web/src/hooks/usePaneVisibility.ts create mode 100644 apps/web/src/hooks/usePreviewDocuments.test.ts create mode 100644 apps/web/src/hooks/usePreviewDocuments.ts create mode 100644 docs/decisions/041-project-preview.md create mode 100644 docs/decisions/042-collapsible-workspace-panes.md create mode 100644 docs/lessons/preview.md create mode 100644 e2e/fixtures/preview-project.ts create mode 100644 e2e/specs/preview/console-lesson.spec.ts create mode 100644 e2e/specs/preview/delivery.spec.ts create mode 100644 e2e/specs/preview/workflow.spec.ts create mode 100644 e2e/specs/security/preview-isolation.spec.ts diff --git a/AGENTS.md b/AGENTS.md index d16c5b37..63db4baf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,7 @@ Inside the V2 code container, Java/Gradle/WPILib, VSCodium, `redhat.java`, and ` apps/control/ Bun control plane: HTTP, WS, sessions, orchestration apps/control/src/app.ts slim factory + top-level fetch dispatcher apps/control/src/app/ response/asset/proxy/status helpers + admin, workspace, websocket route groups +apps/control/src/app/preview*.ts discovery/serving, Markdown rendering, and the signed path token for Preview apps/control/src/containers.ts barrel re-exporting the public container surface apps/control/src/containers/ Docker client, metadata, ports, lifecycle, and the LocalDockerRuntimeProvider class apps/control/src/metrics.ts Prometheus registry, metric handles, route-templating helpers @@ -62,6 +63,20 @@ imports keep `.git` for push. The per-import backup/restore flow was removed (pure discard + git). See [`docs/lessons/overview.md`](./docs/lessons/overview.md) and `docs/decisions/029-lessons-and-modules.md`. +**Project Preview (post-V2):** a third right-pane tab beside AdvantageScope +and PathPlanner reads the project's own Markdown and generated HTML reports +(`build/reports/**` included). Markdown is rendered in-process by +`markdown-it`; HTML reports are served unchanged with their local assets. +Project HTML is framed `sandbox="allow-scripts"` **without** +`allow-same-origin`, which costs the frame its `SameSite=Lax` session cookie — +so `/u/:slug/api/preview/files/` is authorised by a short-lived HMAC path +token instead, and is the one workspace route dispatched ahead of the cookie +ownership check (read-only, GET-only). `plain-java` lessons get a Preview +show/hide button in the selector's slot; `IDELayout` splits `showRightPane` +from `showDriverStation` to make that possible. See +`docs/decisions/041-project-preview.md` and +[`docs/lessons/preview.md`](./docs/lessons/preview.md). + **Containerized control plane (post-V2):** the control plane ships as a Docker image (`containers/control/Dockerfile` → `ghcr.io/mathewdunne/coderunner-control`) and is deployed with docker compose (`docker-compose.yml` base + @@ -119,7 +134,7 @@ arch-independent). See `docs/decisions/035-multi-arch-images-and-workflow-split. ## Key References - `docs/` + `website/` — docs site content and Docusaurus config; published at `https://mathewdunne.github.io/CodeRunner/`; run `bun run docs:dev` to browse locally, `bun run docs:build` to build. -- `docs/decisions/` — all architecture decision logs (011–039 active; 001–010 archived under `docs/decisions/archive/`). +- `docs/decisions/` — all architecture decision logs (011–041 active; 001–010 archived under `docs/decisions/archive/`). - Pinned AdvantageScope submodule: `vendor/AdvantageScope` at tag `v26.0.2`. ## Commands @@ -157,10 +172,10 @@ See `docs/deploying/` and `docs/operating/` for operator documentation. Three test tiers, all runnable without Docker: -- **`bun run test`** — Bun unit/integration tests for the control plane (~350 tests). Covers auth, runs, proxy, containers, the lessons catalog + load pipeline, security, reconciliation, property-based tests, and metrics route-templating cardinality. -- **`bun run test:web`** — Vitest frontend tests (~80 tests). Covers React hooks (`useSession`, `useLessons`, `useSimulationState`, `useContainerStatus`, `useAutoChoosers`, `useGamepad`, `useRunChannel`), DriverStation components, Zustand store, keyboard/gamepad mappings. -- **`bun run e2e`** — Playwright E2E mocked tier (~55 tests). Full login→editor→run→telemetry→DS flows against in-process `ControlApp` with fake codium-server, HALSim, and NT4 backends. No Docker required. -- **`bun run e2e:security`** — Playwright security specs (~8 tests): CSRF, XSS output encoding, response headers. +- **`bun run test`** — Bun unit/integration tests for the control plane (~450 tests). Covers auth, runs, proxy, containers, the lessons catalog + load pipeline, preview discovery/serving/tokens, security, reconciliation, property-based tests, and metrics route-templating cardinality. +- **`bun run test:web`** — Vitest frontend tests (~135 tests). Covers React hooks (`useSession`, `useLessons`, `useSimulationState`, `useContainerStatus`, `useAutoChoosers`, `useGamepad`, `useRunChannel`, `usePreviewDocuments`), DriverStation and PreviewPane components, Zustand store, keyboard/gamepad mappings. +- **`bun run e2e`** — Playwright E2E mocked tier (~75 tests). Full login→editor→run→telemetry→DS flows against in-process `ControlApp` with fake codium-server, HALSim, and NT4 backends, plus the Preview delivery/workflow/console-lesson specs. No Docker required. +- **`bun run e2e:security`** — Playwright security specs (~12 tests): CSRF, XSS output encoding, response headers, Preview isolation. E2E tests use a custom Playwright fixture (`e2e/fixtures/app.ts`) that creates an isolated `ControlApp` per test with its own random port, SQLite DB, and fake upstream servers. Auth is seeded via `loginAs()` which writes user/session rows and HMAC-signs cookies. @@ -169,6 +184,7 @@ Key E2E fixtures: - `e2e/fixtures/fake-halsim.ts` — Fake HALSim bridge (WS, supports stop/restart) - `e2e/fixtures/fake-nt4.ts` — Fake NT4 server for topic announcement - `e2e/fixtures/gamepad-shim.ts` — Playwright addInitScript gamepad override +- `e2e/fixtures/preview-project.ts` — Project tree with a Gradle-shaped HTML report - `e2e/fixtures/runtime.ts` — Runtime seeding helpers The broad Docker smoke tier remains intentionally unimplemented — see diff --git a/apps/control/package.json b/apps/control/package.json index 30610c7c..a240174f 100644 --- a/apps/control/package.json +++ b/apps/control/package.json @@ -14,6 +14,10 @@ "@frc-coderunner/contracts": "workspace:*", "@logtape/logtape": "^2.0.7", "better-auth": "^1.6.10", + "markdown-it": "^15.0.2", "prom-client": "^15.1.3" + }, + "devDependencies": { + "@types/markdown-it": "^14.2.0" } } diff --git a/apps/control/src/__tests__/preview.test.ts b/apps/control/src/__tests__/preview.test.ts new file mode 100644 index 00000000..fce0148c --- /dev/null +++ b/apps/control/src/__tests__/preview.test.ts @@ -0,0 +1,696 @@ +import { describe, expect, test } from "bun:test"; +import { mkdir, symlink, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import type { PreviewDocumentsResponse } from "@frc-coderunner/contracts"; +import type { ControlApp } from "../app"; +import { + mintPreviewToken, + PREVIEW_TOKEN_TTL_SECONDS, + verifyPreviewToken, +} from "../app/preview-token"; +import { + cookieFrom, + createFakeDocker, + login, + withApp, + workspaceProjectPath, +} from "./helpers"; + +const REPORT = "build/reports/tests/test"; + +async function write( + projectPath: string, + relativePath: string, + contents: string, +): Promise { + const target = join(projectPath, relativePath); + await mkdir(dirname(target), { recursive: true }); + await writeFile(target, contents, "utf8"); +} + +async function seedProject(projectPath: string): Promise { + await write(projectPath, "README.md", "# Robot\n\nHello.\n"); + await write(projectPath, "LICENSE.md", "# Licence\n"); + await write(projectPath, "docs/guide.md", "# Guide\n"); + await write(projectPath, "docs/notes/index.html", "

notes

"); + await write(projectPath, `${REPORT}/index.html`, "

report

"); + await write(projectPath, `${REPORT}/css/base.css`, "body{color:red}"); + await write(projectPath, `${REPORT}/js/report.js`, "console.log(1)"); + await write(projectPath, `${REPORT}/secrets.properties`, "token=hunter2"); + await write(projectPath, "src/main/java/Robot.java", "class Robot {}"); + // Excluded trees. + await write(projectPath, ".git/HEAD", "ref: refs/heads/main"); + await write(projectPath, ".git/notes.md", "# secret\n"); + await write(projectPath, ".gradle/cache/out.html", "

x

"); + await write(projectPath, "node_modules/pkg/readme.md", "# dep\n"); + // A hidden directory that is NOT excluded. + await write(projectPath, ".docs/notes.md", "# hidden\n"); +} + +async function documentsFor( + app: ControlApp, + slug: string, + cookie: string, +): Promise { + const resp = await app.fetch( + new Request(`http://localhost/u/${slug}/api/preview/documents`, { + headers: { cookie }, + }), + ); + expect(resp.status).toBe(200); + return (await resp.json()) as PreviewDocumentsResponse; +} + +function fileRequest( + slug: string, + token: string, + encodedPath: string, +): Request { + return new Request( + `http://localhost/u/${slug}/api/preview/files/${token}/${encodedPath}`, + ); +} + +describe("preview tokens", () => { + test("round-trips for the workspace it was minted for", () => { + const { token, expiresIn } = mintPreviewToken("secret", "ws_a".repeat(1)); + expect(expiresIn).toBe(PREVIEW_TOKEN_TTL_SECONDS); + expect(verifyPreviewToken("secret", "ws_a", token)).toBe(true); + }); + + test("does not verify against a different workspace", () => { + // The whole point of signing the workspace id: pasting one student's URL + // into another student's slug must not authorise anything. + const { token } = mintPreviewToken("secret", "ws_a"); + expect(verifyPreviewToken("secret", "ws_b", token)).toBe(false); + }); + + test("does not verify under a different secret", () => { + const { token } = mintPreviewToken("secret", "ws_a"); + expect(verifyPreviewToken("other", "ws_a", token)).toBe(false); + }); + + test("rejects an expired token", () => { + const now = Math.floor(Date.now() / 1000); + const { token } = mintPreviewToken("secret", "ws_a", now); + expect(verifyPreviewToken("secret", "ws_a", token, now + 10)).toBe(true); + expect( + verifyPreviewToken( + "secret", + "ws_a", + token, + now + PREVIEW_TOKEN_TTL_SECONDS + 1, + ), + ).toBe(false); + }); + + test("rejects tampering with the expiry, signature, version or shape", () => { + const { token } = mintPreviewToken("secret", "ws_a"); + const [version, expiresAt, signature] = token.split("."); + // Push the expiry out without re-signing. + expect( + verifyPreviewToken( + "secret", + "ws_a", + `${version}.${Number(expiresAt) + 99999}.${signature}`, + ), + ).toBe(false); + // Flip a signature byte. + const flipped = `${signature?.slice(0, -1)}${signature?.endsWith("A") ? "B" : "A"}`; + expect( + verifyPreviewToken( + "secret", + "ws_a", + `${version}.${expiresAt}.${flipped}`, + ), + ).toBe(false); + // Wrong version, truncated signature, and junk shapes. + expect( + verifyPreviewToken("secret", "ws_a", `p0.${expiresAt}.${signature}`), + ).toBe(false); + expect( + verifyPreviewToken( + "secret", + "ws_a", + `${version}.${expiresAt}.${signature?.slice(0, 5)}`, + ), + ).toBe(false); + expect(verifyPreviewToken("secret", "ws_a", "")).toBe(false); + expect(verifyPreviewToken("secret", "ws_a", "not-a-token")).toBe(false); + expect( + verifyPreviewToken( + "secret", + "ws_a", + `${version}.notanumber.${signature}`, + ), + ).toBe(false); + }); +}); + +describe("GET /u/:slug/api/preview/documents", () => { + test("lists Markdown and HTML, including generated output, excluding machine trees", async () => { + const docker = createFakeDocker(); + await withApp( + async (app) => { + const cookie = cookieFrom(await login(app, "alice")); + await seedProject(workspaceProjectPath(app, "alice")); + + const body = await documentsFor(app, "alice", cookie); + expect(body.ok).toBe(true); + expect(body.truncated).toBe(false); + + const paths = body.documents.map((d) => d.path); + expect(paths).toContain("README.md"); + expect(paths).toContain("docs/guide.md"); + expect(paths).toContain("docs/notes/index.html"); + // Generated output is in scope even though it is gitignored. + expect(paths).toContain(`${REPORT}/index.html`); + // A dot-directory is not excluded just for being hidden. + expect(paths).toContain(".docs/notes.md"); + + // Excluded directory trees never appear, including Markdown inside them. + expect(paths).not.toContain(".git/notes.md"); + expect(paths.some((p) => p.startsWith(".gradle/"))).toBe(false); + expect(paths.some((p) => p.startsWith("node_modules/"))).toBe(false); + // Non-document files are not listed. + expect(paths).not.toContain("src/main/java/Robot.java"); + expect(paths).not.toContain(`${REPORT}/css/base.css`); + + // Shallow-first, then alphabetical. + expect(paths.slice(0, 3)).toEqual([ + "LICENSE.md", + "README.md", + ".docs/notes.md", + ]); + // Deep generated report pages sort below root-level docs. + expect(paths.indexOf(`${REPORT}/index.html`)).toBeGreaterThan( + paths.indexOf("docs/guide.md"), + ); + }, + { dockerRunner: docker.runner }, + ); + }); + + test("classifies kinds by extension, case-insensitively", async () => { + const docker = createFakeDocker(); + await withApp( + async (app) => { + const cookie = cookieFrom(await login(app, "alice")); + const project = workspaceProjectPath(app, "alice"); + await write(project, "SHOUTING.MD", "# loud\n"); + await write(project, "Page.HTM", "

page

"); + await write(project, "Other.HTML", "

page

"); + + const body = await documentsFor(app, "alice", cookie); + const byPath = new Map(body.documents.map((d) => [d.path, d.kind])); + expect(byPath.get("SHOUTING.MD")).toBe("markdown"); + expect(byPath.get("Page.HTM")).toBe("html"); + expect(byPath.get("Other.HTML")).toBe("html"); + }, + { dockerRunner: docker.runner }, + ); + }); + + test("omits document names that violate the preview path contract", async () => { + const docker = createFakeDocker(); + await withApp( + async (app) => { + const cookie = cookieFrom(await login(app, "alice")); + const project = workspaceProjectPath(app, "alice"); + await write(project, "README.md", "# valid\n"); + await write(project, "Notes: Week 1.md", "# invalid path\n"); + await write(project, "docs/valid.md", "# valid\n"); + + const body = await documentsFor(app, "alice", cookie); + expect(body.documents.map((document) => document.path)).toEqual([ + "README.md", + "docs/valid.md", + ]); + }, + { dockerRunner: docker.runner }, + ); + }); + + test("keeps duplicate filenames distinguishable by full path", async () => { + const docker = createFakeDocker(); + await withApp( + async (app) => { + const cookie = cookieFrom(await login(app, "alice")); + const project = workspaceProjectPath(app, "alice"); + await write(project, "index.html", "

root

"); + await write(project, "a/index.html", "

a

"); + await write(project, "b/deep/index.html", "

b

"); + + const body = await documentsFor(app, "alice", cookie); + const paths = body.documents.map((d) => d.path); + expect(paths).toContain("index.html"); + expect(paths).toContain("a/index.html"); + expect(paths).toContain("b/deep/index.html"); + }, + { dockerRunner: docker.runner }, + ); + }); + + test("skips symlinked files and directories", async () => { + const docker = createFakeDocker(); + await withApp( + async (app, root) => { + const cookie = cookieFrom(await login(app, "alice")); + const project = workspaceProjectPath(app, "alice"); + await mkdir(project, { recursive: true }); + + const outsideDir = join(root, "outside"); + await mkdir(outsideDir, { recursive: true }); + await writeFile(join(outsideDir, "leak.md"), "# leak\n", "utf8"); + + await symlink(join(outsideDir, "leak.md"), join(project, "link.md")); + await symlink(outsideDir, join(project, "linkdir")); + + const body = await documentsFor(app, "alice", cookie); + const paths = body.documents.map((d) => d.path); + expect(paths).not.toContain("link.md"); + expect(paths.some((p) => p.startsWith("linkdir"))).toBe(false); + }, + { dockerRunner: docker.runner }, + ); + }); + + test("returns an empty list for an empty project", async () => { + const docker = createFakeDocker(); + await withApp( + async (app) => { + const cookie = cookieFrom(await login(app, "alice")); + const body = await documentsFor(app, "alice", cookie); + expect(body.documents).toEqual([]); + expect(body.truncated).toBe(false); + }, + { dockerRunner: docker.runner }, + ); + }); + + test("requires the session cookie and rejects another student", async () => { + const docker = createFakeDocker(); + await withApp( + async (app) => { + await login(app, "alice"); + const bobCookie = cookieFrom(await login(app, "bob")); + + const anonymous = await app.fetch( + new Request("http://localhost/u/alice/api/preview/documents"), + ); + expect(anonymous.status).toBe(401); + + const wrongUser = await app.fetch( + new Request("http://localhost/u/alice/api/preview/documents", { + headers: { cookie: bobCookie }, + }), + ); + expect(wrongUser.status).toBe(403); + }, + { dockerRunner: docker.runner }, + ); + }); +}); + +describe("GET /u/:slug/api/preview/files//", () => { + test("renders Markdown as an isolated HTML document with heading anchors", async () => { + const docker = createFakeDocker(); + await withApp( + async (app) => { + const cookie = cookieFrom(await login(app, "alice")); + await seedProject(workspaceProjectPath(app, "alice")); + const { token } = await documentsFor(app, "alice", cookie); + + const resp = await app.fetch(fileRequest("alice", token, "README.md")); + expect(resp.status).toBe(200); + expect(resp.headers.get("content-type")).toContain("text/html"); + + const csp = resp.headers.get("content-security-policy") ?? ""; + // Markdown needs no scripts, so the sandbox does not grant any. + expect(csp).toContain("sandbox;"); + expect(csp).toContain("script-src 'none'"); + expect(csp).not.toContain("allow-same-origin"); + expect(resp.headers.get("x-content-type-options")).toBe("nosniff"); + expect(resp.headers.get("referrer-policy")).toBe("no-referrer"); + expect(resp.headers.get("cache-control")).toContain("no-store"); + + const html = await resp.text(); + expect(html).toContain('

Robot

'); + }, + { dockerRunner: docker.runner }, + ); + }); + + test("serves HTML documents unmodified, under a scripted sandbox", async () => { + const docker = createFakeDocker(); + await withApp( + async (app) => { + const cookie = cookieFrom(await login(app, "alice")); + const project = workspaceProjectPath(app, "alice"); + await write(project, `${REPORT}/index.html`, "

report

"); + const { token } = await documentsFor(app, "alice", cookie); + + const resp = await app.fetch( + fileRequest("alice", token, `${REPORT}/index.html`), + ); + expect(resp.status).toBe(200); + // The report's own markup is preserved, not re-rendered. + expect(await resp.text()).toBe("

report

"); + + const csp = resp.headers.get("content-security-policy") ?? ""; + expect(csp).toContain("sandbox allow-scripts"); + expect(csp).not.toContain("allow-same-origin"); + // Resources are pinned to this token's own namespace. + expect(csp).toContain( + `http://localhost/u/alice/api/preview/files/${token}/`, + ); + expect(csp).toContain("connect-src 'none'"); + expect(csp).toContain("form-action 'none'"); + expect(csp).toContain("navigate-to 'none'"); + expect(csp).toContain("frame-src 'none'"); + }, + { dockerRunner: docker.runner }, + ); + }); + + test("serves allowlisted report assets and refuses everything else", async () => { + const docker = createFakeDocker(); + await withApp( + async (app) => { + const cookie = cookieFrom(await login(app, "alice")); + await seedProject(workspaceProjectPath(app, "alice")); + const { token } = await documentsFor(app, "alice", cookie); + + const css = await app.fetch( + fileRequest("alice", token, `${REPORT}/css/base.css`), + ); + expect(css.status).toBe(200); + expect(css.headers.get("content-type")).toContain("text/css"); + + const js = await app.fetch( + fileRequest("alice", token, `${REPORT}/js/report.js`), + ); + expect(js.status).toBe(200); + expect(js.headers.get("content-type")).toContain("text/javascript"); + + // The handler must not become a way to read project config or source. + const properties = await app.fetch( + fileRequest("alice", token, `${REPORT}/secrets.properties`), + ); + expect(properties.status).toBe(415); + expect(await properties.text()).not.toContain("hunter2"); + + const java = await app.fetch( + fileRequest("alice", token, "src/main/java/Robot.java"), + ); + expect(java.status).toBe(415); + }, + { dockerRunner: docker.runner }, + ); + }); + + test("isolates directly navigable SVG assets", async () => { + const docker = createFakeDocker(); + await withApp( + async (app) => { + const cookie = cookieFrom(await login(app, "alice")); + const project = workspaceProjectPath(app, "alice"); + await write(project, "README.md", "# x\n"); + await write( + project, + "docs/chart.svg", + '', + ); + const { token } = await documentsFor(app, "alice", cookie); + + const resp = await app.fetch( + fileRequest("alice", token, "docs/chart.svg"), + ); + expect(resp.status).toBe(200); + expect(resp.headers.get("content-type")).toContain("image/svg+xml"); + // An SVG can be navigated to as a document, so it gets document isolation. + const csp = resp.headers.get("content-security-policy") ?? ""; + expect(csp).toContain("sandbox;"); + expect(csp).toContain("script-src 'none'"); + }, + { dockerRunner: docker.runner }, + ); + }); + + test("handles URL-encoded spaces, Unicode, # and % in filenames", async () => { + const docker = createFakeDocker(); + await withApp( + async (app) => { + const cookie = cookieFrom(await login(app, "alice")); + const project = workspaceProjectPath(app, "alice"); + const names = [ + "my notes.md", + "café ☕.md", + "weird #1.md", + "100% done.md", + ]; + for (const name of names) { + await write(project, `docs/${name}`, `# ${name}\n`); + } + const body = await documentsFor(app, "alice", cookie); + const listed = body.documents.map((d) => d.path); + + for (const name of names) { + expect(listed).toContain(`docs/${name}`); + const encoded = `docs/${name}` + .split("/") + .map(encodeURIComponent) + .join("/"); + const resp = await app.fetch( + fileRequest("alice", body.token, encoded), + ); + expect(resp.status).toBe(200); + expect(await resp.text()).toContain(name.replace(/&/g, "&")); + } + }, + { dockerRunner: docker.runner }, + ); + }); + + test("rejects an encoded separator smuggled inside one segment", async () => { + const docker = createFakeDocker(); + await withApp( + async (app) => { + const cookie = cookieFrom(await login(app, "alice")); + await seedProject(workspaceProjectPath(app, "alice")); + const { token } = await documentsFor(app, "alice", cookie); + + // "docs%2Fguide.md" decodes to a separator that was never part of the + // URL's structure; it must not be re-interpreted as one. + const resp = await app.fetch( + fileRequest("alice", token, "docs%2Fguide.md"), + ); + expect(resp.status).toBe(400); + }, + { dockerRunner: docker.runner }, + ); + }); + + test("rejects traversal, absolute paths and excluded directories", async () => { + const docker = createFakeDocker(); + await withApp( + async (app) => { + const cookie = cookieFrom(await login(app, "alice")); + await seedProject(workspaceProjectPath(app, "alice")); + const { token } = await documentsFor(app, "alice", cookie); + + for (const path of [ + "../escape.md", + "..%2Fescape.md", + "%2e%2e/escape.md", + "docs/../../escape.md", + "/etc/passwd", + "%2Fetc%2Fpasswd", + ".git/notes.md", + ".gradle/cache/out.html", + "node_modules/pkg/readme.md", + "docs/", + "", + ]) { + const resp = await app.fetch(fileRequest("alice", token, path)); + expect([400, 403, 404]).toContain(resp.status); + expect(await resp.text()).not.toContain("secret"); + } + }, + { dockerRunner: docker.runner }, + ); + }); + + test("refuses a symlinked file even when it is named directly", async () => { + const docker = createFakeDocker(); + await withApp( + async (app, root) => { + const cookie = cookieFrom(await login(app, "alice")); + const project = workspaceProjectPath(app, "alice"); + await write(project, "README.md", "# x\n"); + + const outside = join(root, "outside.md"); + await writeFile(outside, "# leaked\n", "utf8"); + await symlink(outside, join(project, "link.md")); + + const { token } = await documentsFor(app, "alice", cookie); + const resp = await app.fetch(fileRequest("alice", token, "link.md")); + expect(resp.status).toBe(403); + expect(await resp.text()).not.toContain("leaked"); + }, + { dockerRunner: docker.runner }, + ); + }); + + test("refuses a file reached through a directory symlink", async () => { + const docker = createFakeDocker(); + await withApp( + async (app, root) => { + const cookie = cookieFrom(await login(app, "alice")); + const project = workspaceProjectPath(app, "alice"); + await write(project, "README.md", "# x\n"); + + const outsideDir = join(root, "outside"); + await mkdir(outsideDir, { recursive: true }); + await writeFile(join(outsideDir, "leak.md"), "# leaked\n", "utf8"); + await symlink(outsideDir, join(project, "linkdir")); + + const { token } = await documentsFor(app, "alice", cookie); + const resp = await app.fetch( + fileRequest("alice", token, "linkdir/leak.md"), + ); + expect([403, 404]).toContain(resp.status); + expect(await resp.text()).not.toContain("leaked"); + }, + { dockerRunner: docker.runner }, + ); + }); + + test("refuses directories and non-regular files", async () => { + const docker = createFakeDocker(); + await withApp( + async (app) => { + const cookie = cookieFrom(await login(app, "alice")); + const project = workspaceProjectPath(app, "alice"); + await write(project, "README.md", "# x\n"); + // A directory whose name ends in .md still must not be read as one. + await mkdir(join(project, "dir.md"), { recursive: true }); + + const { token } = await documentsFor(app, "alice", cookie); + const resp = await app.fetch(fileRequest("alice", token, "dir.md")); + expect([403, 404]).toContain(resp.status); + }, + { dockerRunner: docker.runner }, + ); + }); + + test("reports a file that disappeared between listing and opening", async () => { + const docker = createFakeDocker(); + await withApp( + async (app) => { + const cookie = cookieFrom(await login(app, "alice")); + await seedProject(workspaceProjectPath(app, "alice")); + const { token } = await documentsFor(app, "alice", cookie); + + const resp = await app.fetch( + fileRequest("alice", token, "does-not-exist.md"), + ); + expect(resp.status).toBe(404); + // The failure is rendered as a document, because the frame is what the + // student is looking at. + expect(resp.headers.get("content-type")).toContain("text/html"); + expect(await resp.text()).toContain("no longer available"); + }, + { dockerRunner: docker.runner }, + ); + }); + + test("refuses an oversized document", async () => { + const docker = createFakeDocker(); + await withApp( + async (app) => { + const cookie = cookieFrom(await login(app, "alice")); + const project = workspaceProjectPath(app, "alice"); + // 10 MiB is the document ceiling. + await write(project, "huge.md", "x".repeat(10 * 1024 * 1024 + 1)); + + const { token } = await documentsFor(app, "alice", cookie); + const resp = await app.fetch(fileRequest("alice", token, "huge.md")); + expect(resp.status).toBe(413); + expect(await resp.text()).toContain("too large"); + }, + { dockerRunner: docker.runner }, + ); + }); + + test("rejects a missing, forged, or other-workspace token", async () => { + const docker = createFakeDocker(); + await withApp( + async (app) => { + const aliceCookie = cookieFrom(await login(app, "alice")); + const bobCookie = cookieFrom(await login(app, "bob")); + await seedProject(workspaceProjectPath(app, "alice")); + await seedProject(workspaceProjectPath(app, "bob")); + + const alice = await documentsFor(app, "alice", aliceCookie); + const bob = await documentsFor(app, "bob", bobCookie); + + // Bob's token does not open Alice's files. + const crossUser = await app.fetch( + fileRequest("alice", bob.token, "README.md"), + ); + expect(crossUser.status).toBe(403); + + // A forged token is refused. + const forged = await app.fetch( + fileRequest("alice", "p1.99999999999.forged", "README.md"), + ); + expect(forged.status).toBe(403); + + // Alice's own token still works, and a session cookie is not needed. + const ok = await app.fetch( + fileRequest("alice", alice.token, "README.md"), + ); + expect(ok.status).toBe(200); + }, + { dockerRunner: docker.runner }, + ); + }); + + test("rejects mutation methods", async () => { + const docker = createFakeDocker(); + await withApp( + async (app) => { + const cookie = cookieFrom(await login(app, "alice")); + await seedProject(workspaceProjectPath(app, "alice")); + const { token } = await documentsFor(app, "alice", cookie); + + for (const method of ["POST", "PUT", "PATCH", "DELETE"]) { + const resp = await app.fetch( + new Request( + `http://localhost/u/alice/api/preview/files/${token}/README.md`, + { method }, + ), + ); + expect(resp.status).toBe(405); + } + }, + { dockerRunner: docker.runner }, + ); + }); + + test("returns 404 for an unknown workspace slug without revealing anything", async () => { + const docker = createFakeDocker(); + await withApp( + async (app) => { + const cookie = cookieFrom(await login(app, "alice")); + await seedProject(workspaceProjectPath(app, "alice")); + const { token } = await documentsFor(app, "alice", cookie); + + const resp = await app.fetch(fileRequest("nobody", token, "README.md")); + expect(resp.status).toBe(404); + }, + { dockerRunner: docker.runner }, + ); + }); +}); diff --git a/apps/control/src/app.ts b/apps/control/src/app.ts index 9e478013..daae85a4 100644 --- a/apps/control/src/app.ts +++ b/apps/control/src/app.ts @@ -196,6 +196,10 @@ export async function createApp( const url = new URL(request.url); const start = performance.now(); const route = templateRoute(url.pathname); + // Preview file URLs contain a bearer capability and a private project path. + // Keep both out of logs; the templated route is enough to identify traffic. + const loggedPath = + route === "/u/:slug/api/preview/files/*" ? route : url.pathname; httpRequestsInFlight.inc(); let response: Response; let observedStatus: number; @@ -210,7 +214,7 @@ export async function createApp( ); httpLog.error("unhandled error in request dispatcher", { method: request.method, - path: url.pathname, + path: loggedPath, err: err instanceof Error ? err : new Error(String(err)), }); throw err; @@ -236,7 +240,7 @@ export async function createApp( NOISY_WORKSPACE_PATH.test(url.pathname); const fields = { method: request.method, - path: url.pathname, + path: loggedPath, status: response.status, durationMs, }; diff --git a/apps/control/src/app/assets.ts b/apps/control/src/app/assets.ts index e46b25a3..4a564651 100644 --- a/apps/control/src/app/assets.ts +++ b/apps/control/src/app/assets.ts @@ -1,8 +1,10 @@ +import { existsSync } from "node:fs"; import { cp, mkdir, readdir, readFile, + realpath, rm, stat, writeFile, @@ -64,6 +66,38 @@ export function isInsideDirectory(root: string, target: string): boolean { ); } +/** + * Whether this host exposes `/proc/self/fd`, which is what lets us resolve an + * open descriptor back to the file it actually refers to. Linux (every + * deployment target, and the CI/dev containers) has it; macOS does not, and + * Node exposes no portable equivalent. + */ +const HAS_PROC_SELF_FD = existsSync("/proc/self/fd"); + +/** + * Confirm the file an open descriptor actually refers to lives inside `realRoot` + * (which must itself already be realpath-resolved). + * + * Path-based containment checks are check-then-use: where the caller can write + * into the tree, a directory can be swapped for a symlink in the window between + * the check and the open. Resolving the descriptor we already hold closes that + * race — it names the inode we opened, which cannot be re-pointed underneath us. + * + * Returns true on a host without `/proc` rather than failing closed: there the + * path-based checks are all we have, and refusing every read would be worse + * than the race. Deployments run on Linux, where the check is live. + */ +export async function isOpenFileInsideRoot( + fd: number, + realRoot: string, +): Promise { + if (!HAS_PROC_SELF_FD) { + return true; + } + const real = await realpath(`/proc/self/fd/${fd}`).catch(() => null); + return real !== null && isInsideDirectory(realRoot, real); +} + export function safeRelativeAssetPath(value: string): string | null { if ( value.length === 0 || diff --git a/apps/control/src/app/preview-markdown.ts b/apps/control/src/app/preview-markdown.ts new file mode 100644 index 00000000..a49486c8 --- /dev/null +++ b/apps/control/src/app/preview-markdown.ts @@ -0,0 +1,132 @@ +import MarkdownIt from "markdown-it"; + +/** + * Markdown rendering for Preview. Everything ships with the control plane — + * no CDN renderer, no webfonts, no syntax-highlighting bundle — because the + * whole point is that a student on a locked-down school network can still read + * their project's README. + * + * Raw embedded HTML is disabled (`html: false`). Markdown documents are + * rendered into a frame that is sandboxed *without* `allow-scripts`, so an + * inline ` +`; + +const DEFAULT_GUIDE = `# Guide + +Back to the [README](../README.md). +`; + +const NESTED_INDEX_HTML = ` +Nested index +

Nested index

+`; + +function reportIndexHtml(): string { + return ` + + + Test results + + + + +

Test Summary

+ +

no script

+ + +

closed

+ + +`; +} + +function reportClassHtml(): string { + return ` + + + MyRobotTest + + + +

MyRobotTest

+ + back + + +`; +} + +function reportCss(headingColor: string): string { + return `#report-heading { color: ${headingColor}; }\n`; +} + +function reportJs(marker: string): string { + return `document.addEventListener("DOMContentLoaded", function () { + document.getElementById("script-output").textContent = ${JSON.stringify(marker)}; + var toggle = document.getElementById("toggle"); + if (toggle) { + toggle.addEventListener("click", function () { + document.getElementById("toggle-output").textContent = "open"; + }); + } +}); +`; +} diff --git a/e2e/specs/preview/console-lesson.spec.ts b/e2e/specs/preview/console-lesson.spec.ts new file mode 100644 index 00000000..88b439e7 --- /dev/null +++ b/e2e/specs/preview/console-lesson.spec.ts @@ -0,0 +1,111 @@ +/** + * Preview in a `plain-java` console lesson. + * + * These lessons have no simulation, so they hide the whole right pane and the + * Driver Station. Their instructions and test reports are still worth reading, + * so Preview gets a show/hide button in the topbar's selector slot — and + * revealing it must not drag any simulation chrome back in with it. + */ +import type { Page } from "@playwright/test"; +import type { ControlApp } from "../../../apps/control/src/app"; +import { expect, test } from "../../fixtures/app"; +import { loginAs } from "../../fixtures/auth"; +import { seedPreviewProject } from "../../fixtures/preview-project"; + +function makeConsoleLesson(app: ControlApp, workspaceId: string): void { + app.storage.db + .query( + "UPDATE workspaces SET current_module = ?, current_module_kind = ? WHERE id = ?", + ) + .run("hello-world", "plain-java", workspaceId); +} + +function previewFrame(page: Page) { + return page.frameLocator('[data-testid="preview-frame"]'); +} + +test("a console lesson hides Preview until the student asks for it", async ({ + page, + app, + baseURL, +}) => { + const login = await loginAs(page, app, { name: "console" }); + const workspace = app.storage.findWorkspaceBySlug(login.user.slug); + await seedPreviewProject(workspace?.project_path ?? ""); + makeConsoleLesson(app, workspace?.id ?? ""); + + await page.goto(`${baseURL}/u/${login.user.slug}/`); + + // The lesson's own chrome: a Run hint, and no simulation UI at all. + await expect(page.locator('[data-pane="console-hint"]')).toBeVisible(); + await expect(page.getByRole("tab", { name: "AdvantageScope" })).toHaveCount( + 0, + ); + await expect(page.getByRole("tab", { name: "PathPlanner" })).toHaveCount(0); + await expect(page.locator('[data-pane="console"]')).not.toBeVisible(); + + // Preview is offered, but closed. + const toggle = page.getByRole("button", { name: "Preview", exact: true }); + await expect(toggle).toBeVisible(); + await expect(toggle).toHaveAttribute("aria-pressed", "false"); + await expect(page.locator('[data-pane="preview"]')).not.toBeVisible(); + + await toggle.click(); + + // Only the Preview pane appears — no Driver Station, no AS/PP. + await expect(toggle).toHaveAttribute("aria-pressed", "true"); + await expect(previewFrame(page).locator("h1").first()).toHaveText( + "Robot Project", + ); + await expect(page.locator('[data-pane="console"]')).not.toBeVisible(); + await expect(page.getByRole("tab", { name: "AdvantageScope" })).toHaveCount( + 0, + ); + // The Run hint survives alongside it. + await expect(page.locator('[data-pane="console-hint"]')).toBeVisible(); + + // And it hides again. + await toggle.click(); + await expect(toggle).toHaveAttribute("aria-pressed", "false"); + await expect(page.locator('[data-pane="preview"]')).not.toBeVisible(); +}); + +test("a console lesson starts no simulation traffic when Preview opens", async ({ + page, + app, + baseURL, +}) => { + const login = await loginAs(page, app, { name: "quiet" }); + const workspace = app.storage.findWorkspaceBySlug(login.user.slug); + await seedPreviewProject(workspace?.project_path ?? ""); + makeConsoleLesson(app, workspace?.id ?? ""); + + const simRequests: string[] = []; + page.on("request", (request) => { + const path = new URL(request.url()).pathname; + if (/\/(sim|ws\/run|ws\/gamepad)/.test(path)) simRequests.push(path); + }); + + await page.goto(`${baseURL}/u/${login.user.slug}/`); + await expect( + page.getByRole("button", { name: "Preview", exact: true }), + ).toBeVisible(); + + // The shell cannot know the lesson kind until /api/session answers, so a + // couple of sim polls fire during the initial load and then stop. That is + // pre-existing console-lesson behaviour; the claim under test is that + // *opening Preview* adds nothing to it. + await page.waitForTimeout(500); + const beforePreview = [...simRequests]; + + await page.getByRole("button", { name: "Preview", exact: true }).click(); + await expect(previewFrame(page).locator("h1").first()).toHaveText( + "Robot Project", + ); + await page.getByRole("button", { name: "Refresh" }).click(); + await page.waitForTimeout(500); + + // Preview reads files; it must not wake the simulator hooks this lesson + // deliberately leaves switched off. + expect(simRequests).toEqual(beforePreview); +}); diff --git a/e2e/specs/preview/delivery.spec.ts b/e2e/specs/preview/delivery.spec.ts new file mode 100644 index 00000000..3e5fd624 --- /dev/null +++ b/e2e/specs/preview/delivery.spec.ts @@ -0,0 +1,297 @@ +/** + * Step 1 of the Preview plan: prove the report delivery model in a real browser + * before any UI is built on top of it. + * + * The thing being proved is that a generated Gradle report renders *correctly* + * (its own stylesheet, script, image and page links all resolve) from inside a + * frame that has been stripped of same-origin privileges — and that the report's + * script, once running, can reach neither the CodeRunner shell nor its + * authenticated APIs. + */ + +import type { PreviewDocumentsResponse } from "@frc-coderunner/contracts"; +import type { Frame, Page } from "@playwright/test"; +import { expect, test } from "../../fixtures/app"; +import { loginAs } from "../../fixtures/auth"; +import { seedPreviewProject } from "../../fixtures/preview-project"; + +async function setUp( + page: Page, + app: { + storage: { + findWorkspaceBySlug: (s: string) => { project_path: string } | null; + }; + } & Parameters[1], +) { + const login = await loginAs(page, app, { name: "previewer" }); + const workspace = app.storage.findWorkspaceBySlug(login.user.slug); + if (!workspace) throw new Error("workspace missing after login"); + await seedPreviewProject(workspace.project_path); + return { slug: login.user.slug, projectPath: workspace.project_path }; +} + +async function fetchDocuments( + page: Page, + baseURL: string, + slug: string, +): Promise { + const response = await page.request.get( + `${baseURL}/u/${slug}/api/preview/documents`, + ); + expect(response.status()).toBe(200); + return (await response.json()) as PreviewDocumentsResponse; +} + +function fileUrl( + baseURL: string, + slug: string, + token: string, + path: string, +): string { + const encoded = path.split("/").map(encodeURIComponent).join("/"); + return `${baseURL}/u/${slug}/api/preview/files/${token}/${encoded}`; +} + +/** + * Mounts the document in a frame configured exactly the way the Preview pane + * will: sandboxed for scripts, denied same-origin. + */ +async function mountPreviewFrame(page: Page, src: string): Promise { + await page.evaluate((url) => { + document.querySelector("#preview-probe")?.remove(); + const frame = document.createElement("iframe"); + frame.id = "preview-probe"; + frame.setAttribute("sandbox", "allow-scripts"); + frame.style.cssText = "width:800px;height:600px;border:0"; + frame.src = url; + document.body.appendChild(frame); + }, src); + const handle = await page.waitForSelector("#preview-probe"); + const frame = await handle.contentFrame(); + if (!frame) throw new Error("preview frame did not attach"); + await frame.waitForLoadState("domcontentloaded"); + return frame; +} + +test.describe("preview delivery model", () => { + test("lists project documents and excludes machine trees", async ({ + page, + app, + baseURL, + }) => { + const { slug } = await setUp(page, app); + const body = await fetchDocuments(page, baseURL, slug); + + expect(body.ok).toBe(true); + expect(body.truncated).toBe(false); + expect(body.token).toMatch(/^p1\.\d+\./); + expect(body.tokenExpiresIn).toBeGreaterThan(0); + + const paths = body.documents.map((d) => d.path); + expect(paths).toContain("README.md"); + expect(paths).toContain("docs/guide.md"); + expect(paths).toContain("docs/notes/index.html"); + expect(paths).toContain("build/reports/tests/test/index.html"); + // Generated output is in scope even though it is gitignored. + expect(paths).toContain( + "build/reports/tests/test/classes/MyRobotTest.html", + ); + // Hidden directories are not blanket-excluded. + expect(paths).toContain(".docs/hidden-notes.md"); + + // Excluded trees never appear. + expect(paths.some((p) => p.startsWith(".git/"))).toBe(false); + expect(paths.some((p) => p.startsWith(".gradle/"))).toBe(false); + expect(paths.some((p) => p.startsWith("node_modules/"))).toBe(false); + + // Shallow paths sort above deep ones, so hand-written docs sit above a + // generated report tree rather than being buried under it. + expect(paths.indexOf("README.md")).toBeLessThan( + paths.indexOf("build/reports/tests/test/index.html"), + ); + + // Kinds are classified, case-insensitively by extension. + const readme = body.documents.find((d) => d.path === "README.md"); + expect(readme?.kind).toBe("markdown"); + const report = body.documents.find( + (d) => d.path === "build/reports/tests/test/index.html", + ); + expect(report?.kind).toBe("html"); + }); + + test("renders Markdown with no external requests and working anchors", async ({ + page, + app, + baseURL, + }) => { + const { slug } = await setUp(page, app); + const { token } = await fetchDocuments(page, baseURL, slug); + + // Fail loudly if anything reaches off-origin: the whole point is that a + // student behind a school firewall can still read their README. + const external: string[] = []; + await page.route("**/*", async (route) => { + const url = route.request().url(); + if (!url.startsWith(baseURL) && !url.startsWith("data:")) { + external.push(url); + await route.abort(); + return; + } + await route.continue(); + }); + + await page.goto(`${baseURL}/login`); + const frame = await mountPreviewFrame( + page, + fileUrl(baseURL, slug, token, "README.md"), + ); + + await expect(frame.locator("h1")).toHaveText("Robot Project"); + await expect(frame.locator("table td").first()).toHaveText("1"); + await expect(frame.locator("pre code")).toContainText("public class Robot"); + + // Stable heading anchors, so the hand-written "#wiring" link resolves. + expect(await frame.locator("#getting-started").count()).toBe(1); + expect(await frame.locator("#wiring").count()).toBe(1); + + // The relative image resolves through the token prefix. + const imageWidth = await frame + .locator("img") + .first() + .evaluate((img: HTMLImageElement) => img.naturalWidth); + expect(imageWidth).toBeGreaterThan(0); + + // Raw HTML in Markdown is disabled at the parser, and the frame denies + // scripts anyway, so the embedded +

Rebuilt Summary

+

no script

`, + ); + await writeProjectFile( + project, + `${REPORT}/css/base.css`, + "#report-heading { color: rgb(0, 0, 255); }", + ); + await writeProjectFile( + project, + `${REPORT}/js/report.js`, + `document.addEventListener("DOMContentLoaded", function () { + document.getElementById("script-output").textContent = "rebuilt-script"; + });`, + ); + // ...and add a brand new document. + await writeProjectFile(project, "docs/NEWLY-ADDED.md", "# Newly added\n"); + + await page.getByRole("button", { name: "Refresh" }).click(); + + // One Refresh reloads the document *and* its assets, not just the HTML. + await expect(previewFrame(page).locator("#report-heading")).toHaveText( + "Rebuilt Summary", + ); + await expect(previewFrame(page).locator("#report-heading")).toHaveCSS( + "color", + "rgb(0, 0, 255)", + ); + await expect(previewFrame(page).locator("#script-output")).toHaveText( + "rebuilt-script", + ); + + // The new file is immediately selectable. + await page.getByTestId("preview-picker").click(); + await page.getByPlaceholder("Search documents…").fill("NEWLY-ADDED"); + await expect(page.getByRole("option")).toHaveCount(1); + }); + + test("Refresh returns a scrolled document to the top", async ({ + page, + app, + baseURL, + }) => { + const login = await loginAs(page, app, { name: "scroller" }); + const workspace = app.storage.findWorkspaceBySlug(login.user.slug); + const project = workspace?.project_path ?? ""; + const long = Array.from( + { length: 400 }, + (_, i) => `Paragraph ${i} with enough text to make the page scroll.`, + ).join("\n\n"); + await seedPreviewProject(project, { readme: `# Long\n\n${long}\n` }); + + await openWorkspace(page, baseURL, login.user.slug); + await expect(previewFrame(page).locator("h1").first()).toHaveText("Long"); + + await previewFrame(page) + .locator("body") + .evaluate((body: HTMLElement) => { + body.ownerDocument.defaultView?.scrollTo(0, 2000); + }); + const scrolled = await previewFrame(page) + .locator("body") + .evaluate( + (body: HTMLElement) => body.ownerDocument.defaultView?.scrollY ?? 0, + ); + expect(scrolled).toBeGreaterThan(0); + + await page.getByRole("button", { name: "Refresh" }).click(); + + // Explicitly the documented behaviour: Refresh starts at the top. There is + // no scroll-restoration subsystem to test. + await expect + .poll(async () => + previewFrame(page) + .locator("body") + .evaluate( + (body: HTMLElement) => body.ownerDocument.defaultView?.scrollY ?? 0, + ), + ) + .toBe(0); + }); + + test("a deleted file is reported and the picker stays usable", async ({ + page, + app, + baseURL, + }) => { + const login = await loginAs(page, app, { name: "deleter" }); + const workspace = app.storage.findWorkspaceBySlug(login.user.slug); + const project = workspace?.project_path ?? ""; + await seedPreviewProject(project); + + await openWorkspace(page, baseURL, login.user.slug); + await expect(previewFrame(page).locator("h1").first()).toHaveText( + "Robot Project", + ); + + await rm(join(project, "README.md")); + await page.getByRole("button", { name: "Refresh" }).click(); + + await expect( + page.getByText("README.md is no longer available."), + ).toBeVisible(); + // The refreshed list still works. + await page.getByTestId("preview-picker").click(); + await page.getByPlaceholder("Search documents…").fill("guide"); + await page.getByRole("option").first().click(); + await expect(previewFrame(page).locator("h1").first()).toHaveText("Guide"); + }); + + test("switching to AdvantageScope and back preserves both instances and the selection", async ({ + page, + app, + baseURL, + }) => { + const login = await loginAs(page, app, { name: "switcher" }); + const workspace = app.storage.findWorkspaceBySlug(login.user.slug); + await seedPreviewProject(workspace?.project_path ?? ""); + + await openWorkspace(page, baseURL, login.user.slug); + + await page.getByTestId("preview-picker").click(); + await page.getByPlaceholder("Search documents…").fill("guide"); + await page.getByRole("option").first().click(); + await expect(previewFrame(page).locator("h1").first()).toHaveText("Guide"); + + // The AdvantageScope iframe must survive the round trip, not remount. + const scopeFrameId = await page + .locator('[data-pane="scope"] iframe') + .first() + .evaluate((el: HTMLIFrameElement) => { + const tagged = el as HTMLIFrameElement & { __e2eId?: string }; + tagged.__e2eId ??= Math.random().toString(36); + return tagged.__e2eId; + }); + + await page.getByRole("tab", { name: "AdvantageScope" }).click(); + await page.getByRole("tab", { name: "Preview" }).click(); + + await expect(page.getByTestId("preview-picker")).toHaveText( + /docs\/guide\.md/, + ); + await expect(previewFrame(page).locator("h1").first()).toHaveText("Guide"); + + const afterId = await page + .locator('[data-pane="scope"] iframe') + .first() + .evaluate( + (el: HTMLIFrameElement) => + (el as HTMLIFrameElement & { __e2eId?: string }).__e2eId, + ); + expect(afterId).toBe(scopeFrameId); + }); + + test("replacing the project clears the old content and selection", async ({ + page, + app, + baseURL, + }) => { + const login = await loginAs(page, app, { name: "swapper" }); + const workspace = app.storage.findWorkspaceBySlug(login.user.slug); + const project = workspace?.project_path ?? ""; + await seedPreviewProject(project); + + await openWorkspace(page, baseURL, login.user.slug); + await expect(previewFrame(page).locator("h1").first()).toHaveText( + "Robot Project", + ); + + // Stand in for a lesson load / repo import: the project is replaced on + // disk and the shell is told to invalidate. + await rm(project, { recursive: true, force: true }); + await writeProjectFile(project, "OTHER.md", "# Other project\n"); + await page.getByRole("button", { name: "Refresh" }).click(); + + await expect( + page.getByText("README.md is no longer available."), + ).toBeVisible(); + await page.getByTestId("preview-picker").click(); + await page.getByPlaceholder("Search documents…").fill("Other"); + await page.getByRole("option").first().click(); + await expect(previewFrame(page).locator("h1").first()).toHaveText( + "Other project", + ); + }); + + test("truncation is disclosed rather than silently hidden", async ({ + page, + app, + baseURL, + }) => { + const login = await loginAs(page, app, { name: "many" }); + const workspace = app.storage.findWorkspaceBySlug(login.user.slug); + const project = workspace?.project_path ?? ""; + await seedPreviewProject(project); + // The document budget is 2,000; go past it. + await Promise.all( + Array.from({ length: 2100 }, (_, i) => + writeProjectFile(project, `bulk/doc-${i}.md`, `# Doc ${i}\n`), + ), + ); + + await openWorkspace(page, baseURL, login.user.slug); + await page.getByTestId("preview-picker").click(); + + await expect( + page.getByText("This project has more documents than Preview lists."), + ).toBeVisible(); + }); +}); diff --git a/e2e/specs/security/preview-isolation.spec.ts b/e2e/specs/security/preview-isolation.spec.ts new file mode 100644 index 00000000..360586d5 --- /dev/null +++ b/e2e/specs/security/preview-isolation.spec.ts @@ -0,0 +1,235 @@ +/** + * Preview isolation. Project HTML is student-authored, executable content that + * the control plane serves from the app's own origin, so the boundary around it + * is the security property that matters most in this feature. + * + * Covered here: the shell is unreachable from a report; a report URL opened + * directly is isolated just the same; and no preview URL reaches another + * student's files. + */ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import type { PreviewDocumentsResponse } from "@frc-coderunner/contracts"; +import { expect, test } from "../../fixtures/app"; +import { loginAs } from "../../fixtures/auth"; +import { seedPreviewProject } from "../../fixtures/preview-project"; + +/** A report that actively tries to escape, rather than a benign one. */ +const HOSTILE_REPORT = ` + +

idle

+ + +`; + +async function documentsFor( + page: import("@playwright/test").Page, + baseURL: string, + slug: string, +): Promise { + const response = await page.request.get( + `${baseURL}/u/${slug}/api/preview/documents`, + ); + expect(response.status()).toBe(200); + return (await response.json()) as PreviewDocumentsResponse; +} + +function fileUrl( + baseURL: string, + slug: string, + token: string, + path: string, +): string { + return `${baseURL}/u/${slug}/api/preview/files/${token}/${path + .split("/") + .map(encodeURIComponent) + .join("/")}`; +} + +test("a hostile report cannot reach the shell, its storage, or its cookies", async ({ + page, + app, + baseURL, +}) => { + const login = await loginAs(page, app, { name: "hostile" }); + const workspace = app.storage.findWorkspaceBySlug(login.user.slug); + const project = workspace?.project_path ?? ""; + await seedPreviewProject(project); + await writeFile(join(project, "hostile.html"), HOSTILE_REPORT, "utf8"); + + // Driven through the real pane, so the frame is configured exactly as a + // student would see it rather than by the test. + await page.goto(`${baseURL}/u/${login.user.slug}/`); + await page.getByRole("tab", { name: "Preview" }).click(); + await page.getByTestId("preview-picker").click(); + await page.getByPlaceholder("Search documents…").fill("hostile"); + await page.getByRole("option").first().click(); + + const frame = page.frameLocator('[data-testid="preview-frame"]'); + await expect(frame.locator("#result")).toHaveText("idle"); + + const results = await frame.locator("#result").evaluate(() => { + const probe = ( + window as unknown as { + __probe: (label: string, fn: () => unknown) => string; + } + ).__probe; + return { + origin: window.origin, + parentDom: probe("parent", () => window.parent.document.body.innerHTML), + topLocation: probe("top", () => window.top?.location.href), + localStorage: probe("storage", () => { + window.localStorage.setItem("pwn", "1"); + return "wrote"; + }), + cookies: probe("cookie", () => document.cookie), + }; + }); + + // An opaque origin is what actually contains the report: it is not + // same-origin with the shell, so none of the shell's state is addressable. + expect(results.origin).toBe("null"); + expect(results.parentDom).toMatch(/^parent:blocked:/); + expect(results.topLocation).toMatch(/^top:blocked:/); + expect(results.localStorage).toMatch(/^storage:blocked:/); + // document.cookie is either blocked outright or empty; never the session. + expect(results.cookies).not.toContain("coderunner_session"); + + // The shell's own session is untouched by all of that. + await expect(page.getByTestId("preview-picker")).toBeVisible(); +}); + +test("a report cannot call authenticated control-plane APIs", async ({ + page, + app, + baseURL, +}) => { + const login = await loginAs(page, app, { name: "fetcher" }); + const workspace = app.storage.findWorkspaceBySlug(login.user.slug); + const project = workspace?.project_path ?? ""; + await seedPreviewProject(project); + await writeFile(join(project, "hostile.html"), HOSTILE_REPORT, "utf8"); + + const { token } = await documentsFor(page, baseURL, login.user.slug); + await page.goto(`${baseURL}/login`); + await page.evaluate( + (url) => { + const frame = document.createElement("iframe"); + frame.id = "probe"; + frame.setAttribute("sandbox", "allow-scripts"); + frame.src = url; + document.body.appendChild(frame); + }, + fileUrl(baseURL, login.user.slug, token, "hostile.html"), + ); + + const handle = await page.waitForSelector("#probe"); + const frame = await handle.contentFrame(); + await frame?.waitForLoadState("domcontentloaded"); + + // connect-src 'none' blocks XHR/fetch/WebSocket out of the document. + for (const target of [ + `${baseURL}/u/${login.user.slug}/api/session`, + `${baseURL}/u/${login.user.slug}/api/lessons`, + `${baseURL}/api/auth/providers`, + ]) { + const result = await frame?.evaluate(async (url) => { + try { + const response = await fetch(url, { credentials: "include" }); + return `reachable:${response.status}`; + } catch (error) { + return `blocked:${(error as Error).name}`; + } + }, target); + expect(result).toMatch(/^blocked:/); + } +}); + +test("a preview URL opened directly is sandboxed and uncacheable", async ({ + page, + app, + baseURL, +}) => { + const login = await loginAs(page, app, { name: "direct" }); + const workspace = app.storage.findWorkspaceBySlug(login.user.slug); + await seedPreviewProject(workspace?.project_path ?? ""); + const { token } = await documentsFor(page, baseURL, login.user.slug); + + const response = await page.goto( + fileUrl( + baseURL, + login.user.slug, + token, + "build/reports/tests/test/index.html", + ), + ); + expect(response?.status()).toBe(200); + + const headers = response?.headers() ?? {}; + expect(headers["content-security-policy"]).toContain("sandbox allow-scripts"); + expect(headers["content-security-policy"]).not.toContain("allow-same-origin"); + expect(headers["content-security-policy"]).toContain("navigate-to 'none'"); + expect(headers["x-content-type-options"]).toBe("nosniff"); + // Keeps the capability token out of the Referer of any outbound navigation. + expect(headers["referrer-policy"]).toBe("no-referrer"); + // One student's private files must not sit in a shared cache. + expect(headers["cache-control"]).toContain("no-store"); + expect(headers["cache-control"]).toContain("private"); + + // The header alone, with no iframe involved, still produces an opaque origin. + expect(await page.evaluate(() => window.origin)).toBe("null"); +}); + +test("no preview route reaches another student's project", async ({ + page, + app, + baseURL, +}) => { + const victim = await loginAs(page, app, { name: "victim" }); + const victimWorkspace = app.storage.findWorkspaceBySlug(victim.user.slug); + await writeFile( + join(victimWorkspace?.project_path ?? "", "SECRET.md"), + "# victim secret\n", + "utf8", + ); + const victimDocs = await documentsFor(page, baseURL, victim.user.slug); + + // Now become a different student in the same browser context. + const attacker = await loginAs(page, app, { name: "attacker" }); + const attackerWorkspace = app.storage.findWorkspaceBySlug(attacker.user.slug); + await seedPreviewProject(attackerWorkspace?.project_path ?? ""); + const attackerDocs = await documentsFor(page, baseURL, attacker.user.slug); + + // The victim's document list is refused outright. + const listResponse = await page.request.get( + `${baseURL}/u/${victim.user.slug}/api/preview/documents`, + ); + expect(listResponse.status()).toBe(403); + + // The attacker's own token does not open the victim's files... + const withOwnToken = await page.request.get( + fileUrl(baseURL, victim.user.slug, attackerDocs.token, "SECRET.md"), + ); + expect(withOwnToken.status()).toBe(403); + expect(await withOwnToken.text()).not.toContain("victim secret"); + + // ...and the victim's token does not travel to the attacker's slug either. + const crossSlug = await page.request.get( + fileUrl(baseURL, attacker.user.slug, victimDocs.token, "README.md"), + ); + expect(crossSlug.status()).toBe(403); + + // Traversal out of the attacker's own project is refused. + for (const path of ["../victim/project/SECRET.md", "..%2F..%2Fapp.db"]) { + const attempt = await page.request.get( + `${baseURL}/u/${attacker.user.slug}/api/preview/files/${attackerDocs.token}/${path}`, + ); + expect([400, 403, 404]).toContain(attempt.status()); + expect(await attempt.text()).not.toContain("victim secret"); + } +}); diff --git a/e2e/specs/workspace/pane-layout.spec.ts b/e2e/specs/workspace/pane-layout.spec.ts index 3ee608ce..58c6f3a2 100644 --- a/e2e/specs/workspace/pane-layout.spec.ts +++ b/e2e/specs/workspace/pane-layout.spec.ts @@ -3,7 +3,10 @@ */ import { expect, test } from "../../fixtures/app"; import { loginAs } from "../../fixtures/auth"; -import { seedRuntimeRunning } from "../../fixtures/runtime"; +import { + seedRuntimeRunning, + seedWorkspaceProject, +} from "../../fixtures/runtime"; test("resized pane sizes survive a reload and reset in a new session", async ({ page, @@ -16,6 +19,7 @@ test("resized pane sizes survive a reload and reset in a new session", async ({ const workspace = app.storage.findWorkspaceBySlug( session.user.slug as never, )!; + await seedWorkspaceProject(workspace.project_path); seedRuntimeRunning({ runtime, workspaceId: workspace.id, @@ -53,6 +57,7 @@ test("resized pane sizes survive a reload and reset in a new session", async ({ const freshWorkspace = app.storage.findWorkspaceBySlug( freshSession.user.slug as never, )!; + await seedWorkspaceProject(freshWorkspace.project_path); seedRuntimeRunning({ runtime, workspaceId: freshWorkspace.id, @@ -67,3 +72,147 @@ test("resized pane sizes survive a reload and reset in a new session", async ({ await freshContext.close(); }); + +test("collapse restores sizes and live frames, survives reload, and has a reset escape hatch", async ({ + page, + app, + runtime, + fakeVscode, + fakeHalsim, +}) => { + const { user } = await loginAs(page, app, { name: "Collapse" }); + const workspace = app.storage.findWorkspaceBySlug(user.slug as never)!; + await seedWorkspaceProject(workspace.project_path); + seedRuntimeRunning({ + runtime, + workspaceId: workspace.id, + fakeVscode, + fakeHalsim, + }); + await page.goto(`/u/${user.slug}/`); + const editor = page.locator('[data-pane="editor"] iframe'); + await expect(editor).toBeVisible(); + const editorBody = editor.contentFrame().locator("body"); + await expect(editorBody).toHaveAttribute("data-fake-vscode-ready", "true"); + await editorBody.evaluate((body) => + body.setAttribute("data-layout-sentinel", "alive"), + ); + await page.getByRole("tab", { name: "PathPlanner" }).click(); + const planner = page.locator('iframe[data-pane="pathplanner"]'); + await expect(planner).toBeVisible(); + const plannerBody = planner.contentFrame().locator("body"); + await expect(plannerBody).toHaveAttribute("data-fake-pathplanner-loads", "1"); + const separator = page.getByRole("separator", { + name: "Resize editor and right pane", + }); + const hideEditor = page.getByRole("button", { + name: "Hide editor", + exact: true, + }); + const editorControl = hideEditor.locator(".."); + await page.getByRole("tab", { name: "PathPlanner" }).focus(); + await expect(editorControl).toHaveCSS("opacity", "0"); + await separator.hover(); + await expect(editorControl).toHaveCSS("opacity", "1"); + await separator.focus(); + await separator.press("ArrowLeft"); + await separator.press("ArrowLeft"); + const width = (await editor.boundingBox())!.width; + await page.getByRole("button", { name: "Hide editor", exact: true }).click(); + await expect(editor).not.toBeVisible(); + await expect(planner).toBeVisible(); + // Restore tab overlays the edge: no full-height rail steals pane width. + const restore = page.locator("[data-pane=editor-restore]"); + expect((await restore.boundingBox())!.height).toBeLessThan(50); + expect( + Math.abs( + (await planner.boundingBox())!.width - + (await page.locator("#ide-workbench").boundingBox())!.width, + ), + ).toBeLessThan(3); + await page.getByRole("button", { name: "Show editor", exact: true }).click(); + await expect(editor).toBeVisible(); + await page.mouse.move(0, 0); + await expect(hideEditor).not.toBeFocused(); + await expect(editorControl).toHaveCSS("opacity", "0"); + // Keyboard activation keeps focus on the corresponding control. + await hideEditor.focus(); + await hideEditor.press("Enter"); + const showEditor = page.getByRole("button", { + name: "Show editor", + exact: true, + }); + await expect(showEditor).toBeFocused(); + await showEditor.press("Enter"); + await expect(hideEditor).toBeFocused(); + await expect(editorControl).toHaveCSS("opacity", "1"); + expect(Math.abs((await editor.boundingBox())!.width - width)).toBeLessThan(3); + await expect(editorBody).toHaveAttribute("data-layout-sentinel", "alive"); + await page.getByRole("button", { name: "Hide right pane" }).click(); + await expect(planner).not.toBeVisible(); + // Selecting the already-active tab must reveal the collapsed pane too. + await page.getByRole("tab", { name: "PathPlanner" }).click(); + await expect(planner).toBeVisible(); + await expect(plannerBody).toHaveAttribute("data-fake-pathplanner-loads", "1"); + await page.getByRole("button", { name: "Hide Driver Station" }).click(); + await expect(page.locator("#ide-console")).not.toBeVisible(); + const compact = page.locator('[data-pane="console-restore"]'); + await expect(compact.getByRole("status")).toBeVisible(); + const disable = page.waitForRequest( + (req) => + req.method() === "PATCH" && req.url().includes("/sim/driver-station"), + ); + await compact.getByRole("button", { name: "Disable", exact: true }).click(); + expect((await disable).postDataJSON()).toMatchObject({ enabled: false }); + await page.reload(); + await expect(compact).toBeVisible(); + await page.getByRole("button", { name: "User menu", exact: true }).click(); + await page.getByRole("menuitem", { name: "Layout", exact: true }).click(); + await page.getByRole("menuitem", { name: "Reset layout" }).click(); + await expect(page.locator("#ide-console")).toBeVisible(); + await expect(editor).toBeVisible(); + await expect(planner).toBeVisible(); + expect( + Math.abs( + (await editor.boundingBox())!.width - + (await planner.boundingBox())!.width, + ), + ).toBeLessThan(3); +}); + +test("narrow screens switch upper panes and recover the desktop split", async ({ + page, + app, + runtime, + fakeVscode, + fakeHalsim, +}) => { + const { user } = await loginAs(page, app, { name: "Narrow panes" }); + const workspace = app.storage.findWorkspaceBySlug(user.slug as never)!; + await seedWorkspaceProject(workspace.project_path); + seedRuntimeRunning({ + runtime, + workspaceId: workspace.id, + fakeVscode, + fakeHalsim, + }); + await page.goto(`/u/${user.slug}/`); + const editor = page.locator('[data-pane="editor"] iframe'); + await expect(editor).toBeVisible(); + const width = (await editor.boundingBox())!.width; + await page.setViewportSize({ width: 800, height: 720 }); + await expect(editor).toBeVisible(); + await expect(page.locator("#ide-scope")).not.toBeVisible(); + await page.getByRole("tab", { name: "PathPlanner" }).click(); + await expect(editor).not.toBeVisible(); + await expect(page.locator("#ide-scope")).toBeVisible(); + await page.reload(); + await expect(page.locator("#ide-scope")).toBeVisible(); + await expect(editor).not.toBeVisible(); + await page.getByRole("button", { name: "Show editor" }).click(); + await expect(editor).toBeVisible(); + await expect(page.locator("#ide-scope")).not.toBeVisible(); + await page.setViewportSize({ width: 1280, height: 720 }); + await expect(page.locator("#ide-scope")).toBeVisible(); + expect(Math.abs((await editor.boundingBox())!.width - width)).toBeLessThan(3); +}); diff --git a/graphify-out/GRAPH_REPORT.md b/graphify-out/GRAPH_REPORT.md index f5558a7f..992b9c94 100644 --- a/graphify-out/GRAPH_REPORT.md +++ b/graphify-out/GRAPH_REPORT.md @@ -1,16 +1,16 @@ -# Graph Report - CodeRunner (2026-09-07) +# Graph Report - CodeRunner (2026-09-15) ## Corpus Check -- 347 files · ~268,836 words +- 367 files · ~288,928 words - Verdict: corpus is large enough that graph structure adds value. ## Summary -- 4094 nodes · 5929 edges · 362 communities (276 shown, 86 thin omitted) -- Extraction: 97% EXTRACTED · 3% INFERRED · 0% AMBIGUOUS · INFERRED: 187 edges (avg confidence: 0.8) +- 4297 nodes · 6239 edges · 370 communities (285 shown, 85 thin omitted) +- Extraction: 97% EXTRACTED · 3% INFERRED · 0% AMBIGUOUS · INFERRED: 189 edges (avg confidence: 0.8) - Token cost: 0 input · 0 output ## Graph Freshness -- Built from commit: `1601a6f1` +- Built from commit: `38b61ea6` - Run `git rev-parse HEAD` and compare to check if the graph is stale. - Run `graphify update .` after code changes (no API cost). @@ -296,6 +296,7 @@ - [[_COMMUNITY_Community 278|Community 278]] - [[_COMMUNITY_Community 279|Community 279]] - [[_COMMUNITY_Community 280|Community 280]] +- [[_COMMUNITY_Community 281|Community 281]] - [[_COMMUNITY_Community 282|Community 282]] - [[_COMMUNITY_Community 283|Community 283]] - [[_COMMUNITY_Community 284|Community 284]] @@ -303,21 +304,20 @@ - [[_COMMUNITY_Community 286|Community 286]] - [[_COMMUNITY_Community 287|Community 287]] - [[_COMMUNITY_Community 288|Community 288]] -- [[_COMMUNITY_Community 289|Community 289]] - [[_COMMUNITY_Community 290|Community 290]] - [[_COMMUNITY_Community 291|Community 291]] - [[_COMMUNITY_Community 292|Community 292]] +- [[_COMMUNITY_Community 293|Community 293]] - [[_COMMUNITY_Community 294|Community 294]] - [[_COMMUNITY_Community 295|Community 295]] - [[_COMMUNITY_Community 296|Community 296]] +- [[_COMMUNITY_Community 297|Community 297]] +- [[_COMMUNITY_Community 298|Community 298]] +- [[_COMMUNITY_Community 299|Community 299]] +- [[_COMMUNITY_Community 300|Community 300]] +- [[_COMMUNITY_Community 302|Community 302]] +- [[_COMMUNITY_Community 303|Community 303]] - [[_COMMUNITY_Community 304|Community 304]] -- [[_COMMUNITY_Community 305|Community 305]] -- [[_COMMUNITY_Community 306|Community 306]] -- [[_COMMUNITY_Community 307|Community 307]] -- [[_COMMUNITY_Community 308|Community 308]] -- [[_COMMUNITY_Community 309|Community 309]] -- [[_COMMUNITY_Community 310|Community 310]] -- [[_COMMUNITY_Community 311|Community 311]] - [[_COMMUNITY_Community 312|Community 312]] - [[_COMMUNITY_Community 313|Community 313]] - [[_COMMUNITY_Community 314|Community 314]] @@ -365,31 +365,39 @@ - [[_COMMUNITY_Community 356|Community 356]] - [[_COMMUNITY_Community 357|Community 357]] - [[_COMMUNITY_Community 358|Community 358]] +- [[_COMMUNITY_Community 359|Community 359]] - [[_COMMUNITY_Community 360|Community 360]] +- [[_COMMUNITY_Community 361|Community 361]] +- [[_COMMUNITY_Community 362|Community 362]] +- [[_COMMUNITY_Community 363|Community 363]] +- [[_COMMUNITY_Community 364|Community 364]] +- [[_COMMUNITY_Community 365|Community 365]] +- [[_COMMUNITY_Community 366|Community 366]] +- [[_COMMUNITY_Community 368|Community 368]] ## God Nodes (most connected - your core abstractions) -1. `cn()` - 75 edges +1. `cn()` - 77 edges 2. `AppStorage` - 44 edges 3. `LocalDockerRuntimeProvider` - 40 edges -4. `LocalDockerRuntimeProvider` - 33 edges -5. `test` - 32 edges -6. `loginAs()` - 30 edges -7. `withApp()` - 29 edges +4. `test` - 36 edges +5. `loginAs()` - 35 edges +6. `LocalDockerRuntimeProvider` - 33 edges +7. `withApp()` - 30 edges 8. `RunManager` - 28 edges 9. `HalSimBridge` - 26 edges -10. `getLogger()` - 23 edges +10. `getLogger()` - 24 edges ## Surprising Connections (you probably didn't know these) - `nt4-multi-workspace.spec.ts (T35.1 NT4 isolation test)` --semantically_similar_to--> `embedded-mode NT4 endpoint injection mechanism` [INFERRED] [semantically similar] e2e/specs/telemetry/nt4-multi-workspace.spec.ts → patches/advantagescope/README.md - `Grafana ops dashboard screenshot: host VM, workspaces, runs, control-plane panels` --conceptually_related_to--> `rebuildWorkspaces()` [AMBIGUOUS] website/static/img/screenshots/grafana-ops-dashboard.png → scripts/rebuild-workspaces.ts -- `run-sim.sh two-phase sim runner` --references--> `robot-starter build.gradle` [INFERRED] - containers/code/README.md → catalog/modules/robot-starter/build.gradle - `WorkspacePage (page object)` --conceptually_related_to--> `SimPaneSwitcher.tsx` [INFERRED] e2e/page-objects/workspace.po.ts → docs/superpowers/plans/2026-08-30-pathplanner-integration.md - `applyAdvantageScopePatches()` --references--> `001-lite-nt4-endpoint-injection.patch` [INFERRED] scripts/apply-ascope-patches.ts → patches/advantagescope/README.md +- `demo mode test suite` --conceptually_related_to--> `Demo Mode Quick Start` [INFERRED] + apps/control/src/__tests__/auth-demo.test.ts → README.md ## Hyperedges (group relationships) - **V2 container-lease port model evolution (V1 sim/lsp -> V2 nt4/vscode/halsim)** — 001_v1_core_container_leases_table, 004_v2_code_container_vscode_port_column, 005_drop_v1_columns_nt4_port_column, 006_halsim_port_halsim_port_column, 007_betterauth_tables_container_leases_new_table [EXTRACTED 0.90] @@ -442,1065 +450,1101 @@ - **Three-Pane Workspace UI Screenshots** — screenshot_workspace_shell_three_panes, screenshot_pathplanner_overview, screenshot_using_coderunner_ready [INFERRED 0.80] - **Project Onboarding and Team Import Flow** — screenshot_sign_in_page, screenshot_switch_project_dialog, screenshot_team_import_progress, screenshot_using_coderunner_start [INFERRED 0.75] -## Communities (362 total, 86 thin omitted) +## Communities (370 total, 85 thin omitted) ### Community 0 - "Admin API Contracts" Cohesion: 0.03 -Nodes (66): AdminActionResponse, adminActionResponseSchema, AdminStatusResponse, adminStatusResponseSchema, AdminWorkspaceStatus, adminWorkspaceStatusSchema, AllianceStation, AuthProvider (+58 more) +Nodes (74): AdminActionResponse, adminActionResponseSchema, AdminStatusResponse, adminStatusResponseSchema, AdminWorkspaceStatus, adminWorkspaceStatusSchema, AllianceStation, AuthProvider (+66 more) ### Community 1 - "Allowlist Management E2E" -Cohesion: 0.07 -Nodes (40): handleAdminRoute(), addAllowlistEntry(), AllowlistData, EMPTY, getAllowlist(), isEmailAllowed(), loadAllowlist(), normalize() (+32 more) +Cohesion: 0.05 +Nodes (42): getDemoSessionResponseBody(), seedDemoUser(), input, stripped, authorizeMetrics(), bootLog, constantTimeEqual(), httpLog (+34 more) ### Community 2 - "Driver Station Enable/Disable UI" -Cohesion: 0.08 -Nodes (43): AssetManifest, contentTypeFor(), handleUploadAsset(), isInsideDirectory(), pathplannerResponse(), readScopeAssetManifest(), safeRelativeAssetPath(), scopeResponse() (+35 more) +Cohesion: 0.07 +Nodes (28): addAllowlistEntry(), allowlist.spec.ts (auth), e2e/fixtures/app.ts fixture (ControlApp harness), createApp(), Commits 066141e / 1a4f5e6 (Vite base path revert), loginAs() / cookieHeader() auth fixture, ControlApp in-process fixture / createApp(), Decision 015 — workspace routing / DS sync (+20 more) ### Community 3 - "Driver Station Switch Project Flow" -Cohesion: 0.05 -Nodes (38): activeWorkspaces, DockerStatsPoller, DockerStatsPollerOptions, log, containerCpuPercent, containerMemoryPercent, containerStartBuckets, containerStartDuration (+30 more) +Cohesion: 0.07 +Nodes (40): addAllowlistEntry(), AllowlistData, EMPTY, getAllowlist(), isEmailAllowed(), loadAllowlist(), normalize(), normalizeEntry() (+32 more) ### Community 4 - "Gamepad/Keyboard WPILib Mapping" +Cohesion: 0.05 +Nodes (37): sendUpstreamWebSocketMessage(), AppSocket, ControlApp, ControlAppOptions, GamepadSocketData, HalSimSocketData, ImportSocketData, LessonLoadSocketData (+29 more) + +### Community 5 - "Admin Layout & Polling" Cohesion: 0.04 Nodes (43): code:ts (describe("deployFilePathSchema", () => {), code:ts (pathplannerDistDir: string;), code:ts (export async function createPathPlannerDist(root: string): P), code:ts (const pathplannerDistDir = await createPathPlannerDist(root)), code:bash (bun run check:fix), code:ts (test("templates PathPlanner routes with bounded cardinality"), code:ts (if (path.startsWith("/pathplanner/")) return "/pathplanner/*), code:ts (if (suffix.startsWith("/api/deploy-files/"))) (+35 more) -### Community 5 - "Admin Layout & Polling" +### Community 6 - "Container Runtime Status Helpers" Cohesion: 0.05 Nodes (47): Web Contracts Re-export, dpadToPov (gamepad-mapping), gamepadFrameToWpilib, WPILib XboxController Axis/Button Layout, dpadToPov (keyboard-mapping), gamepadStateToVisualizerFrame, Keyboard-as-Virtual-Gamepad Emulation, KEYBOARD_BINDINGS (+39 more) -### Community 6 - "Container Runtime Status Helpers" -Cohesion: 0.08 -Nodes (29): Avatar(), UserMenuProps, CodeStatus, CodeStatusPill(), TONES, codeStatusFromRun(), ConsoleLine(), ConsolePanel() (+21 more) - ### Community 7 - "Shared UI Primitives" Cohesion: 0.07 Nodes (24): chooserRoots(), decodeMsgPack(), displayKey(), encodeMsgPack(), isChooser(), normalizeTopicName(), Nt4AutoChooserBridge, Nt4AutoChooserBridgeOptions (+16 more) ### Community 8 - "Control Plane App & Metrics" -Cohesion: 0.11 -Nodes (27): AdminLayout(), Tab, tabs, useAdminPoll(), Allowlist(), AllowlistData, AuditEntry, AuditLog() (+19 more) +Cohesion: 0.07 +Nodes (30): AutoPanel(), AutoPanelProps, DriverStation(), DriverStationProps, change, { container, rerender }, props, MODE_CLASSES (+22 more) ### Community 9 - "Docker Runtime Provider Core" -Cohesion: 0.05 -Nodes (45): listWorkspaceDiskLimitDevices, parseDockerStatsLine, runtimeFromLease, statusFromLease, upstreamEndpoints, dockerPortBindError, inspectContainer, inspectContainers (+37 more) +Cohesion: 0.09 +Nodes (30): BINDING_GROUPS, Bumper(), ControlsPanel(), ControlsPanelProps, DpadArm(), FaceButton(), GamepadVisualizer(), SmallButton() (+22 more) ### Community 10 - "Run Execution Metrics" -Cohesion: 0.07 -Nodes (27): AutoPanel(), AutoPanelProps, ControlsPanel(), DriverStationProps, MODE_CLASSES, MODE_LABELS, ModeColumn(), ModeColumnProps (+19 more) +Cohesion: 0.09 +Nodes (28): AdminLayout(), Tab, tabs, useAdminPoll(), Allowlist(), AllowlistData, AuditEntry, AuditLog() (+20 more) ### Community 11 - "NT4 Auto Chooser Protocol" -Cohesion: 0.1 -Nodes (7): runtimeFromLease(), statusFromLease(), dockerPortBindError(), LocalDockerRuntimeProvider, containerRuntimeState(), v2LabelsMatch(), workspaceHomePath() +Cohesion: 0.05 +Nodes (45): listWorkspaceDiskLimitDevices, parseDockerStatsLine, runtimeFromLease, statusFromLease, upstreamEndpoints, dockerPortBindError, inspectContainer, inspectContainers (+37 more) ### Community 12 - "Control Plane Route Test Suite" Cohesion: 0.11 -Nodes (32): parseDockerStatsLine(), parsePercent(), defaultDockerRunner(), dockerError(), inspectContainer(), inspectContainerOrThrow(), inspectContainers(), runDocker() (+24 more) +Nodes (34): parseDockerStatsLine(), parsePercent(), runtimeFromLease(), upstreamEndpoints(), defaultDockerRunner(), dockerError(), inspectContainer(), inspectContainerOrThrow() (+26 more) ### Community 13 - "Driver Station Auto/Console Panels" -Cohesion: 0.05 -Nodes (37): 10. Become the first admin, 1. Create and configure the GCP project, 2. Create the Terraform state bucket, 3. Configure Terraform variables, 4. Apply Terraform, 5. Populate Secret Manager, 6. Add a DNS A record, 7. Reset the VM to render config (+29 more) +Cohesion: 0.1 +Nodes (6): statusFromLease(), dockerPortBindError(), LocalDockerRuntimeProvider, configMountType(), v2LabelsMatch(), release ### Community 14 - "WebSocket Upstream Proxy" -Cohesion: 0.07 -Nodes (28): ADMIN_GET_ROUTES, adminCookie, cookie, req, studentCookie, cookieFrom(), createFakeDocker(), ExecOverride (+20 more) +Cohesion: 0.05 +Nodes (37): 10. Become the first admin, 1. Create and configure the GCP project, 2. Create the Terraform state bucket, 3. Configure Terraform variables, 4. Apply Terraform, 5. Populate Secret Manager, 6. Add a DNS A record, 7. Reset the VM to render config (+29 more) ### Community 15 - "Web App Shell & Theming" Cohesion: 0.07 -Nodes (35): AutoPanel, preferredChooser, CodeStatusPill, codeStatusFromRun, ConsolePanel, ConsoleViewport, ControlsPanel, GamepadVisualizer (+27 more) +Nodes (30): ADMIN_GET_ROUTES, adminCookie, cookie, req, studentCookie, adminCookie, body, fakeDocker (+22 more) ### Community 16 - "Auth & Allowlist Test Bodies" -Cohesion: 0.13 -Nodes (20): body, body, body, body, absolute, refs, headers, received (+12 more) +Cohesion: 0.07 +Nodes (35): AutoPanel, preferredChooser, CodeStatusPill, codeStatusFromRun, ConsolePanel, ConsoleViewport, ControlsPanel, GamepadVisualizer (+27 more) ### Community 17 - "E2E Runtime & Resilience Specs" Cohesion: 0.08 -Nodes (18): AdminApp(), ResolvedTheme, Theme, THEME_VALUES, ThemeProvider(), ThemeProviderContext, ThemeProviderProps, ThemeProviderState (+10 more) +Nodes (21): PreviewProjectOptions, RED_PIXEL_PNG, reportClassHtml(), reportCss(), reportIndexHtml(), reportJs(), seedPreviewProject(), write() (+13 more) ### Community 18 - "WebSocket Handler Factory" -Cohesion: 0.12 -Nodes (26): AdminRouteContext, log, createProjectArchive(), directorySizeBytes(), restoreProjectArchive(), runTar(), apiErrorResponse(), capacityErrorResponse() (+18 more) +Cohesion: 0.08 +Nodes (18): AdminApp(), ResolvedTheme, Theme, THEME_VALUES, ThemeProvider(), ThemeProviderContext, ThemeProviderProps, ThemeProviderState (+10 more) ### Community 19 - "App Storage (SQLite)" -Cohesion: 0.08 -Nodes (30): audit_log table, runtime_config table, audit log test suite, recordAuditEvent (audit.ts, tested), Physical disk vs virtual device filtering for --device-read-bps, listWorkspaceDiskLimitDevices (containers/block-devices), container concurrency cap test suite, max-active-containers cap persisted via runtime_config, adoption/restart gating (+22 more) +Cohesion: 0.14 +Nodes (16): body, body, body, body, absolute, refs, headers, received (+8 more) ### Community 20 - "Docker Runtime Types & Ports" Cohesion: 0.14 -Nodes (27): halsimWebSocketResponse(), HOP_BY_HOP_HEADERS, log, nt4AliveResponse(), nt4WebSocketResponse(), probeVscodeReady(), requestedProtocols(), stripHopByHopHeaders() (+19 more) +Nodes (24): AdminRouteContext, handleAdminRoute(), log, createProjectArchive(), directorySizeBytes(), restoreProjectArchive(), runTar(), apiErrorResponse() (+16 more) ### Community 21 - "E2E Workspace Fixtures" -Cohesion: 0.09 -Nodes (18): BINDING_GROUPS, Bumper(), ControlsPanelProps, DpadArm(), FaceButton(), GamepadVisualizer(), SmallButton(), StatusTone (+10 more) +Cohesion: 0.1 +Nodes (21): SimPanePanels(), SimPanePanelsProps, SimPaneTab, SimPaneTabs(), SimPaneTabSelector(), SimPaneTabsProps, onPreviewActivated, pathplannerTab (+13 more) ### Community 22 - "Import URL Security Tests" -Cohesion: 0.12 -Nodes (5): AppStorage, ensureWorkspaceFiles(), nowIso(), projectPathFor(), randomId() - -### Community 23 - "HALSim/NT4 WebSocket Responses" Cohesion: 0.07 Nodes (27): Adding an entry, Admin API break-glass, Audit log, Between sessions, Checking and adjusting the cap at runtime, code:bash (docker compose exec control coderunner ), code:bash (# Remove all entries before a date), code:bash (# Overall system status: workspaces, container states, activ) (+19 more) +### Community 23 - "HALSim/NT4 WebSocket Responses" +Cohesion: 0.08 +Nodes (27): audit_log table, runtime_config table, audit log test suite, recordAuditEvent (audit.ts, tested), Reload allowlist before each sign-in check, refreshAllowlistBeforeCheck, container concurrency cap test suite, max-active-containers cap persisted via runtime_config, adoption/restart gating (+19 more) + ### Community 24 - "Docker Client & Lifecycle" Cohesion: 0.09 Nodes (9): AppFixtures, seedWorkspaceProject(), WorkspacePage, status, wsp, Deps, dialog, openWorkspace() (+1 more) ### Community 25 - "Container Lifecycle Test Suite" -Cohesion: 0.09 -Nodes (21): first, NUM_RUNS, second, cloneCall, copyCall, importer, mock, samples (+13 more) +Cohesion: 0.11 +Nodes (16): DemoBanner(), EditorPane(), EditorPaneProps, LayoutMenu(), ScopePane, Topbar(), EditorReachability, EditorStatus (+8 more) ### Community 26 - "Admin Backup/Restore Test Suite" -Cohesion: 0.07 -Nodes (26): code:bash (gcloud compute snapshots create coderunner-data-eoy2026 \), code:bash (gcloud compute snapshots delete coderunner-data-eoy2026), code:bash (gcloud compute snapshots describe coderunner-data-eoy2026 \), code:bash (gcloud compute instances delete coderunner --zone=northameri), code:bash (gcloud compute disks delete coderunner-data --zone=northamer), code:bash (# Only if you choose to release the IP), code:bash (terraform state rm google_compute_instance.coderunner google), code:hcl (resource "google_compute_disk" "data" {) (+18 more) +Cohesion: 0.16 +Nodes (25): parseDeployFilePath(), halsimWebSocketResponse(), HOP_BY_HOP_HEADERS, log, nt4AliveResponse(), nt4WebSocketResponse(), probeVscodeReady(), requestedProtocols() (+17 more) ### Community 27 - "Admin Backup Archive Routes" -Cohesion: 0.07 -Nodes (26): Build times out, code:bash (# Confirm Docker is running), code:bash (SIM_PORT_RANGE=25810-25999), code:bash (# 1. Prune run logs (safest, often largest single contributo), code:bash (bun run backup), code:bash (docker compose restart control), code:bash (curl -H "Authorization: Bearer $ADMIN_TOKEN" \), code:bash (stat -c '%g' /var/run/docker.sock # e.g. 999) (+18 more) +Cohesion: 0.12 +Nodes (3): AppStorage, nowIso(), randomId() ### Community 28 - "Sim Pane Switcher & Topbar" -Cohesion: 0.16 -Nodes (13): e2e/fixtures/app.ts fixture (ControlApp harness), Commits 066141e / 1a4f5e6 (Vite base path revert), loginAs() / cookieHeader() auth fixture, Decision 015 — workspace routing / DS sync, Commit d111f70 (driver-station payload shape bug), e2e/fixtures/fake-halsim.ts, e2e/fixtures/fake-vscode.ts, Commit 158bab4 (hop-by-hop header stripping) (+5 more) +Cohesion: 0.09 +Nodes (20): createWebSocketHandlers(), createApp(), createCatalogSource(), baBody, body, userRow, workspace, denied (+12 more) ### Community 29 - "Better Auth Providers & Storage" -Cohesion: 0.09 -Nodes (25): afterFirst, afterSecond, aliceCookie, allowlistPath, baseOptions, boss, coach, cookie (+17 more) +Cohesion: 0.07 +Nodes (26): code:bash (gcloud compute snapshots create coderunner-data-eoy2026 \), code:bash (gcloud compute snapshots delete coderunner-data-eoy2026), code:bash (gcloud compute snapshots describe coderunner-data-eoy2026 \), code:bash (gcloud compute instances delete coderunner --zone=northameri), code:bash (gcloud compute disks delete coderunner-data --zone=northamer), code:bash (# Only if you choose to release the IP), code:bash (terraform state rm google_compute_instance.coderunner google), code:hcl (resource "google_compute_disk" "data" {) (+18 more) ### Community 30 - "AdvantageScope Patch Application" -Cohesion: 0.08 -Nodes (25): addBody, adminCookie, after, aliceWorkspace, backedUpProject, backupsDir, bob, bobWorkspace (+17 more) +Cohesion: 0.07 +Nodes (26): Build times out, code:bash (# Confirm Docker is running), code:bash (SIM_PORT_RANGE=25810-25999), code:bash (# 1. Prune run logs (safest, often largest single contributo), code:bash (bun run backup), code:bash (docker compose restart control), code:bash (curl -H "Authorization: Bearer $ADMIN_TOKEN" \), code:bash (stat -c '%g' /var/run/docker.sock # e.g. 999) (+18 more) ### Community 31 - "Lesson Catalog Authoring & Gradle Cache" -Cohesion: 0.08 -Nodes (24): aliceWorkspace, bobWorkspace, byName, calls, config, cookie, expectedName, fakeDocker (+16 more) +Cohesion: 0.16 +Nodes (5): consumeLines(), lineLooksReady(), randomRunId(), runLogPath(), RunManager ### Community 32 - "Grafana Alloy & Cloudflare Deploy Config" Cohesion: 0.11 Nodes (13): BundledCatalogSource, CatalogManifest, CatalogSource, findModuleOrThrow(), log, parseCatalogRepo(), RemoteCatalogSource, sortByOrder() (+5 more) ### Community 33 - "Run Lifecycle Test Fixtures" -Cohesion: 0.1 -Nodes (15): sendUpstreamWebSocketMessage(), log, WebSocketHandlerContext, upstreamEndpoints(), DEFAULT_PORT, GamepadLease, GamepadLeaseResolver, GamepadMessageOutcome (+7 more) +Cohesion: 0.08 +Nodes (25): addBody, adminCookie, after, aliceWorkspace, backedUpProject, backupsDir, bob, bobWorkspace (+17 more) ### Community 34 - "Sim API & Chooser Announcements" -Cohesion: 0.11 -Nodes (16): seedRuntimeRunning(), cookie, frames, snap, status, cookie, snapshot, console (+8 more) +Cohesion: 0.08 +Nodes (24): aliceWorkspace, bobWorkspace, byName, calls, config, cookie, expectedName, fakeDocker (+16 more) ### Community 35 - "Deploy Files & WS Origin Guards" -Cohesion: 0.11 -Nodes (19): SimPanePanels(), SimPanePanelsProps, SimPaneTab, SimPaneTabs(), SimPaneTabSelector(), SimPaneTabsProps, pathplannerTab, [scopePanel, pathplannerPanel] (+11 more) +Cohesion: 0.15 +Nodes (22): isOpenFileInsideRoot(), ASSET_CONTENT_TYPES, discoverDocuments(), documentCsp(), documentKindFor(), errorDocument(), log, documentShell() (+14 more) ### Community 36 - "Audit Log & Break-Glass Admin" -Cohesion: 0.11 -Nodes (15): DemoBanner(), ScopePane, useGamepadChannel(), ScopeStatus, useScopeHandshake(), LoadState, heartbeatCalls, { result } (+7 more) +Cohesion: 0.1 +Nodes (16): RunCommandFactory, waitFor(), announceChooser(), body, controlled, cookie, encodeMsgPack(), fakeDocker (+8 more) ### Community 37 - "Structured Logging" Cohesion: 0.14 Nodes (23): applyAdvantageScopePatches(), ascopeRoot, CommandResult, patchDir, patchFiles(), repoRoot, run(), ascopeLiteStatic (+15 more) ### Community 38 - "Control Plane Config Loading" -Cohesion: 0.1 -Nodes (7): cookie, firstBody, rows, runtime2, secondBody, states, MockWorkspaceRuntimeProvider +Cohesion: 0.11 +Nodes (25): Authoring Lesson Modules doc, Bundled lesson catalog (catalog/), catalog/modules/robot-starter license files, github.com/mathewdunne/coderunner-lessons, Decision 010: Gradle Project Cache Isolation for Sim and LSP, --project-cache-dir $HOME/.gradle-project-sim, stop-sim.sh, V1-10 three-user smoke test (+17 more) ### Community 39 - "Import Pipeline Test Fixtures" -Cohesion: 0.12 -Nodes (16): CATALOG, fetchMock, { result }, useLessons(), UseLessonsReturn, INITIAL_STATE, ProjectSwapKind, ProjectSwapState (+8 more) +Cohesion: 0.08 +Nodes (22): aliceConnection, aliceMessages, aliceRun, aliceRunId, aliceWorkspace, baseOptions, bobConnection, bobMessages (+14 more) ### Community 40 - "Admin App Allowlist Page" Cohesion: 0.08 -Nodes (22): aliceConnection, aliceMessages, aliceRun, aliceRunId, aliceWorkspace, baseOptions, bobConnection, bobMessages (+14 more) +Nodes (23): 1. Choose the new version, 1. Clone and configure, 2. Pull the new images and restart the control plane, 2. Start and verify, 3. Rebuild student workspaces, 3. Sign in and allow students, 4. Verify the update, code:bash (git clone https://github.com/mathewdunne/CodeRunner.git Code) (+15 more) ### Community 41 - "Allowlist Core Module" Cohesion: 0.11 -Nodes (15): RunCommandFactory, announceChooser(), body, controlled, cookie, encodeMsgPack(), fakeDocker, FakeWebSocket (+7 more) +Nodes (24): collectFiles, deployFilesSnapshotResponse, parseDeployFilePath, isAllowedWebSocketOrigin, requireWebSocketOrigin, halsimWebSocketResponse, nt4AliveResponse, nt4WebSocketResponse (+16 more) ### Community 42 - "Admin Route Dispatch & Security Headers" -Cohesion: 0.08 -Nodes (23): 1. Choose the new version, 1. Clone and configure, 2. Pull the new images and restart the control plane, 2. Start and verify, 3. Rebuild student workspaces, 3. Sign in and allow students, 4. Verify the update, code:bash (git clone https://github.com/mathewdunne/CodeRunner.git Code) (+15 more) +Cohesion: 0.1 +Nodes (24): ADMIN_TOKEN admin API break-glass, data/allowlist.json, data/app.db (SQLite), Audit log system, bun run audit:prune, Backups doc, Capacity and Sizing doc, CODE_DISK_READ_LIMIT env var (+16 more) ### Community 43 - "Self-Inspection (Container Auto-Detect)" -Cohesion: 0.11 -Nodes (24): collectFiles, deployFilesSnapshotResponse, parseDeployFilePath, isAllowedWebSocketOrigin, requireWebSocketOrigin, halsimWebSocketResponse, nt4AliveResponse, nt4WebSocketResponse (+16 more) +Cohesion: 0.14 +Nodes (14): Avatar(), UserMenuProps, DropdownMenu(), DropdownMenuCheckboxItem(), DropdownMenuContent(), DropdownMenuItem(), DropdownMenuLabel(), DropdownMenuRadioItem() (+6 more) ### Community 44 - "Public Health/OpenAPI E2E Specs" -Cohesion: 0.1 -Nodes (24): ADMIN_TOKEN admin API break-glass, data/allowlist.json, data/app.db (SQLite), Audit log system, bun run audit:prune, Backups doc, Capacity and Sizing doc, CODE_DISK_READ_LIMIT env var (+16 more) +Cohesion: 0.09 +Nodes (20): bobCookie, body, clearedCache, cloneCall, cookie, copyCall, ctx, docker (+12 more) ### Community 45 - "Gamepad Input Mapping" Cohesion: 0.13 Nodes (22): ANSI, colorize(), configureLogging(), createJsonSink(), createSink(), formatAttrs(), formatAttrValue(), formatRecord() (+14 more) ### Community 46 - "Audit Log & Runtime Config Schema" -Cohesion: 0.09 -Nodes (20): bobCookie, body, clearedCache, cloneCall, cookie, copyCall, ctx, docker (+12 more) - -### Community 47 - "PathPlanner Integration Deploy Pipeline" Cohesion: 0.11 Nodes (23): AdminApp, AdminLayout, Tab (type), Allowlist (admin page), fetchAllowlist, App (default export), FallbackRedirect, RootIndex (+15 more) -### Community 48 - "Gamepad Lease Management" +### Community 47 - "PathPlanner Integration Deploy Pipeline" Cohesion: 0.11 Nodes (23): BACKEND_ORIGIN Pages secret, Cloudflare Offline Page, Pages Function catch-all [[path]].ts, deploy/cloudflare/wrangler.toml, coderunner-ops.json dashboard, Deployment Overview, CodeRunner Docs Overview, c4-standard-4 GCE VM (+15 more) +### Community 48 - "Gamepad Lease Management" +Cohesion: 0.09 +Nodes (21): `bun run e2e`: Playwright mocked tier, `bun run e2e:security`: security E2E tests, `bun run e2e:workspace-java`: real Java workspace smoke, `bun run test`: control-plane unit and integration tests, `bun run test:web`: frontend unit and component tests, `bun run verify`: full CI gate, code:bash (bunx playwright install chromium), code:bash (bun run test) (+13 more) + +### Community 49 - "Container Lease Cleanup CLI" +Cohesion: 0.12 +Nodes (22): handleAdminRoute, applySecurityHeaders, dispatch, fetch (request handler), directorySizeBytes, handleUploadAsset, pathplannerResponse, readScopeAssetManifest (+14 more) + ### Community 50 - "Keyboard Input Mapping" -Cohesion: 0.15 -Nodes (19): defaultContainerUser(), defaultDataDir, defaultHalsimPortRange, defaultSimPortRange, defaultVscodePortRange, DISABLED_DISK_LIMIT_VALUES, envContainerUser(), loadControlConfig() (+11 more) +Cohesion: 0.14 +Nodes (18): DEFAULT_NETWORKS, deriveComposeProject(), deriveContainerUser(), deriveHostDataDir(), deriveNetwork(), inspectFailureMessage(), notDerived(), readComposeProject() (+10 more) ### Community 51 - "ControlApp Test Harness Socket Types" Cohesion: 0.14 -Nodes (18): DEFAULT_NETWORKS, deriveComposeProject(), deriveContainerUser(), deriveHostDataDir(), deriveNetwork(), inspectFailureMessage(), notDerived(), readComposeProject() (+10 more) +Nodes (14): GamepadFrame, GamepadInfo, listConnectedGamepads(), makeLabel(), FakePad, { result }, useGamepad(), UseGamepadResult (+6 more) ### Community 52 - "Architecture & Access Control Docs" -Cohesion: 0.09 -Nodes (21): `bun run e2e`: Playwright mocked tier, `bun run e2e:security`: security E2E tests, `bun run e2e:workspace-java`: real Java workspace smoke, `bun run test`: control-plane unit and integration tests, `bun run test:web`: frontend unit and component tests, `bun run verify`: full CI gate, code:bash (bunx playwright install chromium), code:bash (bun run test) (+13 more) +Cohesion: 0.16 +Nodes (19): defaultContainerUser(), defaultDataDir, defaultHalsimPortRange, defaultSimPortRange, defaultVscodePortRange, DISABLED_DISK_LIMIT_VALUES, envContainerUser(), loadControlConfig() (+11 more) ### Community 53 - "Auto Panel & Gamepad Channel" -Cohesion: 0.13 -Nodes (12): AppOptions, defaultRunCommandFactory, startFakeHalsim(), startFakeVscode(), AppFixture, FakeHalsimHandle, FakeVscodeHandle, SeededUser (+4 more) +Cohesion: 0.1 +Nodes (18): aliceCookie, bobCookie, byPath, cookie, docker, encoded, listed, names (+10 more) ### Community 54 - "HALSim Bridge State Application" -Cohesion: 0.14 -Nodes (14): GamepadFrame, GamepadInfo, listConnectedGamepads(), makeLabel(), FakePad, { result }, useGamepad(), UseGamepadResult (+6 more) +Cohesion: 0.1 +Nodes (20): afterFirst, afterSecond, aliceCookie, allowlistPath, baseOptions, boss, coach, cookie (+12 more) ### Community 55 - "Deploy Files Security Test Suite" Cohesion: 0.1 Nodes (20): 002 — AdvantageScope Lite hosted standalone, 1. `spawnSync(npmCmd, [...], { shell: false })` returns exit `null` on Windows, 2. AS Lite ships a `lite/static/` directory inside the submodule with `index.html` and `popups.css`, 3. Git symlinks under `lite/static/` checked out as 9-byte text files on Windows, 4. AS Lite expects `GET /assets` and `GET /assets//` server routes, 5. AS submodule's `postinstall` is heavy, 6. AS upstream prints `npm audit` warnings about transitive vulns, AdvantageScope as a git submodule pinned to a release tag (+12 more) ### Community 56 - "Asset Upload & Manifest" -Cohesion: 0.11 -Nodes (21): Admin role + break-glass token, Architecture doc, Audit log, Cloudflare Pages Function ([[path]].ts), CODERUNNER_ADMIN_EMAIL bootstrap, Control plane container privileges (non-root, socket access), Decision 031: Containerized Control Plane (referenced), Demo mode (+13 more) +Cohesion: 0.12 +Nodes (21): Classroom-density memory defaults, coderunner-workspace bind-mount contract, coderunner-workspace conservative JVM/Gradle memory bounds, V2 code container (coderunner-workspace) README, Decision 015 (HALSim control protocol), Decision 016 (imported-project sim compat), Decision 024: Container Memory Budget, Decision 025 (two-phase sim runner) (+13 more) ### Community 57 - "Workspace Container Bind-Mount & Memory Bounds" -Cohesion: 0.15 -Nodes (14): clearContainerLeases(), dbPathFromEnv(), DockerCommandResult, DockerRunner, main(), parseContainerNames(), rebuildWorkspaces(), RebuildWorkspacesOptions (+6 more) +Cohesion: 0.13 +Nodes (11): seedRuntimeRunning(), snapshot, cookie, frames, snap, status, cookie, snapshot (+3 more) ### Community 58 - "Java Tooling Smoke Test" Cohesion: 0.1 -Nodes (19): 1. Add the origin A record, 2. Verify the Caddyfile has the origin vhost, 3. Bootstrap the Cloudflare Pages project, 4. Set `BACKEND_ORIGIN` as a Pages secret, 5. Add the custom domain in Cloudflare, 6. Add GitHub Actions variables, Cloudflare Offline Page, code:block1 (student browser ──443──> Cloudflare Pages (coderunner)) (+11 more) +Nodes (18): workspaceBySlug(), config, cookie, dataDir, fakeDocker, lease, name, now (+10 more) ### Community 59 - "Contracts Property-Based Tests" -Cohesion: 0.1 -Nodes (19): 001 — Sim container architecture, 1. Missing `WPILibNewCommands.json` vendor dep, 2. Dockerfile `|| true` swallowed gradle build failure, 3. Image size came in at 2.25 GB, not the 1.0–1.4 GB plan estimate, 4. Wrapper-zip warning is cosmetic, Context, Decisions, `eclipse-temurin:17-jdk-jammy` base image (+11 more) +Cohesion: 0.15 +Nodes (14): clearContainerLeases(), dbPathFromEnv(), DockerCommandResult, DockerRunner, main(), parseContainerNames(), rebuildWorkspaces(), RebuildWorkspacesOptions (+6 more) ### Community 60 - "Admin Allowlist Endpoints" -Cohesion: 0.16 -Nodes (16): digitalAxis(), dpadToPov(), gamepadStateToVisualizerFrame(), isMappedKeyboardCode(), KEYBOARD_BINDINGS, KeyboardBindingGroup, keyboardCodesToWpilib(), MAPPED_CODES (+8 more) +Cohesion: 0.1 +Nodes (19): 1. Add the origin A record, 2. Verify the Caddyfile has the origin vhost, 3. Bootstrap the Cloudflare Pages project, 4. Set `BACKEND_ORIGIN` as a Pages secret, 5. Add the custom domain in Cloudflare, 6. Add GitHub Actions variables, Cloudflare Offline Page, code:block1 (student browser ──443──> Cloudflare Pages (coderunner)) (+11 more) ### Community 61 - "AdvantageScope Lite & Docusaurus" -Cohesion: 0.11 -Nodes (18): Authentication, Capturing a session, code:block1 (14:23:01.482 INFO [control.runs] run started workspaceId=a), code:json ({"timestamp":"2026-05-21T14:23:01.482Z","level":"info","cate), code:bash (docker compose logs -f control | tee coderunner-$(date +%Y%m), code:bash (# Manual probe), code:bash (curl http://localhost:4000/healthz), code:json ({"ok":true,"service":"control","version":"v2-3"}) (+10 more) +Cohesion: 0.1 +Nodes (19): 001 — Sim container architecture, 1. Missing `WPILibNewCommands.json` vendor dep, 2. Dockerfile `|| true` swallowed gradle build failure, 3. Image size came in at 2.25 GB, not the 1.0–1.4 GB plan estimate, 4. Wrapper-zip warning is cosmetic, Context, Decisions, `eclipse-temurin:17-jdk-jammy` base image (+11 more) ### Community 62 - "CI/Test Command Reference" -Cohesion: 0.11 -Nodes (18): Capacity and Sizing, Checking disk usage, Cleaning up, code:bash (# Tighter cap for a memory-constrained host (expect slower c), code:block2 (available_for_containers = total_RAM - 4 GB), code:bash (# Per-container CPU and memory (one-shot)), code:bash (# Overall free space), code:bash (bun run docker:cleanup) (+10 more) +Cohesion: 0.15 +Nodes (12): AppOptions, defaultRunCommandFactory, startFakeHalsim(), startFakeVscode(), AppFixture, FakeHalsimHandle, FakeVscodeHandle, SeededUser (+4 more) ### Community 63 - "Gamepad/Driver Station Zod Schemas" Cohesion: 0.15 -Nodes (19): Authoring Lesson Modules doc, Bundled lesson catalog (catalog/), catalog/modules/robot-starter license files, github.com/mathewdunne/coderunner-lessons, Google API Services User Data Policy, hello-world module, LESSONS_CATALOG_REPO env var, Lessons-are-gitless design principle (+11 more) +Nodes (11): PreviewFrame, PreviewPane(), PreviewPaneProps, PreviewPlaceholderProps, findDefaultDocument(), PreviewDocumentsState, previewFileUrl(), fetchMock (+3 more) ### Community 64 - "Mock Workspace Runtime Provider" -Cohesion: 0.18 -Nodes (19): driverStationPatchSchema, gamepadClientMessageSchema, gamepadServerMessageSchema, gamepadStateSchema, importRequestSchema, packages/contracts/src/index.ts, packages/contracts/src/index.test.ts, isWorkspaceSlug() (+11 more) +Cohesion: 0.16 +Nodes (16): digitalAxis(), dpadToPov(), gamepadStateToVisualizerFrame(), isMappedKeyboardCode(), KEYBOARD_BINDINGS, KeyboardBindingGroup, keyboardCodesToWpilib(), MAPPED_CODES (+8 more) ### Community 65 - "Scope Pane & Demo Banner" -Cohesion: 0.21 -Nodes (3): defaultSnapshot(), HalSimBridge, upstreamUrlFor() +Cohesion: 0.11 +Nodes (16): bashCalls, cloneCall, ctx, importer, mock, row, workspace, CatalogLoadContext (+8 more) ### Community 66 - "HALSim Bridge Message Handling" +Cohesion: 0.12 +Nodes (14): first, NUM_RUNS, second, cloneCall, copyCall, importer, mock, samples (+6 more) + +### Community 67 - "Audit/Reconciliation Test Suite" +Cohesion: 0.11 +Nodes (18): Authentication, Capturing a session, code:block1 (14:23:01.482 INFO [control.runs] run started workspaceId=a), code:json ({"timestamp":"2026-05-21T14:23:01.482Z","level":"info","cate), code:bash (docker compose logs -f control | tee coderunner-$(date +%Y%m), code:bash (# Manual probe), code:bash (curl http://localhost:4000/healthz), code:json ({"ok":true,"service":"control","version":"v2-3"}) (+10 more) + +### Community 68 - "WebSocket Router Test Suite" +Cohesion: 0.11 +Nodes (18): Capacity and Sizing, Checking disk usage, Cleaning up, code:bash (# Tighter cap for a memory-constrained host (expect slower c), code:block2 (available_for_containers = total_RAM - 4 GB), code:bash (# Per-container CPU and memory (one-shot)), code:bash (# Overall free space), code:bash (bun run docker:cleanup) (+10 more) + +### Community 69 - "Deploy Files Path Safety" +Cohesion: 0.13 +Nodes (19): Admin role + break-glass token, Architecture doc, Audit log, Cloudflare Pages Function ([[path]].ts), CODERUNNER_ADMIN_EMAIL bootstrap, Control plane container privileges (non-root, socket access), Decision 031: Containerized Control Plane (referenced), Demo mode (+11 more) + +### Community 70 - "Run Manager Command Factory" +Cohesion: 0.16 +Nodes (11): CodeStatus, CodeStatusPill(), TONES, codeStatusFromRun(), ConsoleLine(), ConsolePanel(), ConsolePanelProps, parseConsoleLine() (+3 more) + +### Community 71 - "Java LSP Bridge (Archived)" Cohesion: 0.11 Nodes (16): bobCookie, body, cookie, docker, evilDir, evilPath, example, newFile (+8 more) -### Community 67 - "Audit/Reconciliation Test Suite" +### Community 72 - "VSCodium Editor Container Defaults" +Cohesion: 0.21 +Nodes (3): defaultSnapshot(), HalSimBridge, upstreamUrlFor() + +### Community 73 - "Contracts Schema Unit Tests" Cohesion: 0.11 Nodes (17): Authoring Lesson Modules, code:text (modules.json ← the catalog manifest (required, ), code:json ({), code:json ({), Example curriculum, Module fields, `plain-java`, Publishing (+9 more) -### Community 68 - "WebSocket Router Test Suite" +### Community 74 - "Import Manager (Git Clone Staging)" Cohesion: 0.11 Nodes (17): Auto-import on Tab (additionalTextEdits), code:dockerfile (FROM gitpod/openvscode-server:1.105.1), code:bash (cd /tmp/frc-spike-openvscode), Container boots and serves (:3000), Ctrl-click into library source (jdt:// URI), Decision 011: V2 Editor Spike — openvscode-server with redhat.java and WPILib, Decisions, Docker Hub vs GitHub Releases (+9 more) -### Community 69 - "Deploy Files Path Safety" +### Community 75 - "Demo Mode Session Seeding" Cohesion: 0.11 Nodes (17): 003 — Minimal web shell, 1. Headless preview screenshot hangs when AS Lite iframe is loading, 2. Orphaned Vite child after `TaskStop` on the npm wrapper, 3. `WARNING:StorageManager: settings timeout, using defaults` in the console, 4. AS Lite tab-controls panel was clipped (initial layout), AS Lite via iframe to `http://localhost:8080`, not bundled, code:block1 ("editor scope"), Context (+9 more) -### Community 70 - "Run Manager Command Factory" +### Community 76 - "Backup/Restore & Catalog Import Flows" Cohesion: 0.11 Nodes (17): 006 - Multi-tenancy spike findings, code:text (alice alive 200 ok), code:bash (npm run spike:multi -- up), code:text (alice: websocket open=true, initialized=true, diagnostics=0), code:text (open=true, initialized=true), code:text (GET /file?user=alice -> 200), code:text (status building at 1 ms), code:text (sim container created in 0.29s) (+9 more) -### Community 71 - "Java LSP Bridge (Archived)" -Cohesion: 0.15 -Nodes (18): BundledCatalogSource (catalog.ts), CatalogSource interface, createCatalogSource (catalog.ts), parseCatalogRepo (catalog.ts), RemoteCatalogSource (catalog.ts), Two-source lesson catalog pattern (bundled + remote), ImportError (imports.ts), sendUpstreamWebSocketMessage (+10 more) +### Community 77 - "Auth Client & Docker Runner Helpers" +Cohesion: 0.16 +Nodes (18): Grafana Alloy config template (config.alloy.tmpl), Alloy log pipeline bounded active-series design, bootstrap.sh first-boot provisioning script, Caddyfile (GCE deployment), Decision 023: Metrics and Observability, Decision 023: Metrics and Observability, Decision 027: Ship control-plane logs to Grafana Cloud Loki, Decision 031 (containerized control plane) (+10 more) -### Community 72 - "VSCodium Editor Container Defaults" +### Community 78 - "Container Capacity & V2 Acceptance Decisions" Cohesion: 0.18 Nodes (15): docker(), DockerResult, migratedSettings, openJavaFile(), repoRoot, startWorkspace(), stopWorkspace(), terminal (+7 more) -### Community 73 - "Contracts Schema Unit Tests" +### Community 79 - "Dist Download Utility" Cohesion: 0.12 Nodes (16): first, NUM_RUNS, parsed, round, slugArb, allianceStationSchema, bridgeConnectionSchema, containerStateSchema (+8 more) -### Community 74 - "Import Manager (Git Clone Staging)" -Cohesion: 0.12 -Nodes (15): AuditLogEntry, adminCookie, body, fakeDocker, rows, student, studentCookie, workspace (+7 more) - -### Community 75 - "Demo Mode Session Seeding" +### Community 80 - "PathPlanner Deploy File Schemas" Cohesion: 0.12 Nodes (16): Alloy config location, code:block1 (https://prometheus-prod-XX-prod-us-central-0.grafana.net/api), code:block2 (https://logs-prod-XXX.grafana.net/loki/api/v1/push), code:bash (echo -n 'https://prometheus-prod-XX-prod-us-central-0.grafan), code:bash (gcloud compute ssh coderunner --zone=us-central1-a --tunnel-), code:bash (cd /opt/coderunner), code:logql (# All logs from one student), code:promql (up{instance="coderunner"}) (+8 more) -### Community 76 - "Backup/Restore & Catalog Import Flows" +### Community 81 - "Bundled Lesson Catalog Modules" Cohesion: 0.12 Nodes (16): Browser API behavior, code:text (Browser -> stateless HTTP API -> control-plane HALSim bridge), code:json ({ "type": "", "device": "", "data": {), Context, Decision, Decision 015 — HALSim WebSocket Control Protocol, DriverStation message (`type: "DriverStation"`, `device: ""`), Future hooks (+8 more) -### Community 77 - "Auth Client & Docker Runner Helpers" +### Community 82 - "Fake NT4 Test Server" Cohesion: 0.12 Nodes (16): Code style and CI gates, code:bash (bun install), code:bash (bun run dev:control), code:bash (bun run dev:control -- --demo), code:bash (bun run dev:web), code:bash (bun run migrate # apply all pending migrations), code:bash (bun run check:fix), code:bash (bun run verify) (+8 more) -### Community 78 - "Container Capacity & V2 Acceptance Decisions" +### Community 83 - "Code Status Pill Component" Cohesion: 0.12 Nodes (16): Bind mounts, Build, code:bash (bun run docker:build:workspace), code:block2 (frc-sim.managed=true), code:bash (docker run -d \), Environment variables, Example run, First-Run Behavior (+8 more) -### Community 79 - "Dist Download Utility" -Cohesion: 0.14 -Nodes (17): Admin allowlist endpoints, addAllowlistEntry (auth/allowlist), isEmailAllowed (auth/allowlist), loadAllowlist (auth/allowlist), reloadAllowlist, removeAllowlistEntry, saveAllowlist, Reload allowlist before each sign-in check (+9 more) +### Community 84 - "Database Migrations Runner" +Cohesion: 0.17 +Nodes (17): MAX_ACTIVE_CONTAINERS capacity limit, Container labels + reconciliation, Decision 011: V2 Editor Spike, Decision 012: V2 Code Image, Decision 013: V2 Acceptance Pass, Decision 017: linuxserver Base Migration, First-run init behavior, gitpod/openvscode-server base image (+9 more) -### Community 80 - "PathPlanner Deploy File Schemas" -Cohesion: 0.14 -Nodes (17): Baseline response security headers, Batched docker inspect, Capacity admission on container adoption, Container isolation (memory/disk/port caps), Container ports (3000/3300/5810), Decision 015: HALSim WebSocket Control Protocol, Decision 018: Gamepad Input via HALSim WebSocket, Decision 019: Keyboard Input Mode (+9 more) +### Community 85 - "Asset Upload Test Suite" +Cohesion: 0.15 +Nodes (17): bun run e2e:workspace-java (real Java workspace smoke), Code container VS Code defaults test suite, containers/code/Dockerfile, ghcr.io/mathewdunne/coderunner-workspace image, codium-server (VSCodium reh-web), Decision 033: workspace disk read limit, scripts/image.ts, containers/code/root/.../init-frc-setup/run (+9 more) -### Community 81 - "Bundled Lesson Catalog Modules" +### Community 86 - "Capacity Cap Test Suite" +Cohesion: 0.13 +Nodes (16): Biome (lint/format/import org), bun run e2e (Playwright mocked tier), bun run e2e:security, bun run test (control-plane unit/integration), bun run test:web (Vitest), bun run verify (CI gate), apps/control/src/config.ts migrations resolution, --demo / CODERUNNER_DEMO_MODE flag (+8 more) + +### Community 87 - "Disk Read Limit Config" Cohesion: 0.12 Nodes (16): AdvantageKit, patches/advantagescope/ source-level patches, AdvantageScope (upstream), 001-lite-nt4-endpoint-injection.patch, AS Lite in-iframe timeout banner, Docusaurus, GitHub CLI, Licenses doc (+8 more) -### Community 82 - "Fake NT4 Test Server" -Cohesion: 0.13 -Nodes (16): Biome (lint/format/import org), bun run e2e (Playwright mocked tier), bun run e2e:security, bun run test (control-plane unit/integration), bun run test:web (Vitest), bun run verify (CI gate), apps/control/src/config.ts migrations resolution, --demo / CODERUNNER_DEMO_MODE flag (+8 more) +### Community 88 - "Canonical Image Naming Decisions" +Cohesion: 0.21 +Nodes (17): driverStationPatchSchema, gamepadClientMessageSchema, gamepadServerMessageSchema, gamepadStateSchema, importRequestSchema, packages/contracts/src/index.ts, packages/contracts/src/index.test.ts, isWorkspaceSlug() (+9 more) -### Community 83 - "Code Status Pill Component" +### Community 89 - "AdvantageScope Lite Hosting (Archived)" +Cohesion: 0.12 +Nodes (12): cookie, firstBody, rows, runtime2, secondBody, states, console, cookie (+4 more) + +### Community 90 - "CLI Reference & Control Container" +Cohesion: 0.23 +Nodes (15): isInsideDirectory(), collectFiles(), deployFileDeleteResponse(), deployFilesSnapshotResponse(), deployFileWriteResponse(), DeployWorkspace, findDeepestExistingAncestor(), HAS_PROC_SELF_FD (+7 more) + +### Community 91 - "IDE Layout Resizable Panes" +Cohesion: 0.23 +Nodes (15): AssetManifest, contentTypeFor(), handleUploadAsset(), HAS_PROC_SELF_FD, pathplannerResponse(), readScopeAssetManifest(), safeRelativeAssetPath(), scopeResponse() (+7 more) + +### Community 93 - "Network Mode Config Test" Cohesion: 0.14 Nodes (13): BridgeEntry, DEFAULT_DRIVER_STATION, DriverStationState, HalSimBridgeOptions, HalSimBridgeSnapshot, HalSimMessage, HalSimWebSocketFactory, JoystickWireState (+5 more) -### Community 84 - "Database Migrations Runner" -Cohesion: 0.21 -Nodes (15): collectFiles(), deployFileDeleteResponse(), deployFilesSnapshotResponse(), deployFileWriteResponse(), DeployWorkspace, findDeepestExistingAncestor(), HAS_PROC_SELF_FD, INVALID_PATH_ERROR (+7 more) - -### Community 85 - "Asset Upload Test Suite" +### Community 94 - "Rebuild Workspaces CLI Args" Cohesion: 0.12 Nodes (15): Backup options, Backups, code:bash (docker compose exec control coderunner backup), code:bash (bun run backup), code:block3 (data/backups/2026-05-16-151038/), code:bash (# Write the backup to a custom location), code:bash (bun run restore -- ), code:bash (# Preview what would be restored without writing anything) (+7 more) -### Community 86 - "Capacity Cap Test Suite" +### Community 95 - "Users CLI Script" Cohesion: 0.12 Nodes (15): Admin and Metrics, Auth and OAuth, code:bash (bun run start -- --demo), Configuration Reference, Demo mode and the `--demo` flag, Docker and Containers, Docker Compose deployment, How environment is loaded (+7 more) -### Community 87 - "Disk Read Limit Config" -Cohesion: 0.17 -Nodes (16): applySecurityHeaders, authorizeMetrics, constantTimeEqual, dispatch, fetch (request handler), handleUploadAsset, userAssetsPath, webShellResponse (+8 more) +### Community 96 - "Extension Reconciliation Test" +Cohesion: 0.12 +Nodes (15): Can CodeRunner run offline or without internet?, Can I build or start simulation from the WPILib extension?, Can I write my own lessons?, Can students accidentally break each other's work?, Can students push code to GitHub?, Do students need accounts? What if I just want to try it?, FAQ, How do students read their README or a test report? (+7 more) -### Community 88 - "Canonical Image Naming Decisions" +### Community 97 - "AdvantageScope Verify Script" Cohesion: 0.13 Nodes (16): makeScriptedRunCommandFactory, consumeLines, defaultRunCommandFactory, dockerRunScript, lineLooksReady, RunManager (app.runs), Startup marks persisted active runs as stopped, run lifecycle and log streaming test suite (+8 more) -### Community 89 - "AdvantageScope Lite Hosting (Archived)" +### Community 98 - "Gamepad Session Safety Disables" Cohesion: 0.16 Nodes (16): containers/lsp/bridge/bridge.ts, Bun-native WebSocket-to-stdio Bridge, container_leases.lsp_state column split, Generic ContainerOrchestrator, apps/web/src/java-lsp.ts, Eclipse JDT LS, Decision 008: V1 LSP Container and Bun-native Bridge, apps/control/src/app.ts (+8 more) -### Community 90 - "CLI Reference & Control Container" +### Community 99 - "Slug/Login Property Tests" Cohesion: 0.13 Nodes (14): authProvidersResponseSchema, autoChooserPatchSchema, autoChoosersResponseSchema, deployFilePathSchema, deployFilesSnapshotResponseSchema, gamepadClientMessageSchema, gamepadServerMessageSchema, importResponseSchema (+6 more) -### Community 91 - "IDE Layout Resizable Panes" +### Community 100 - "Container Isolation & HALSim Decisions" Cohesion: 0.16 Nodes (6): FakeSocket, Listener, { rerender }, { result }, { unmount }, useRunChannel() -### Community 93 - "Network Mode Config Test" +### Community 102 - "Project Swap Fake Socket Test" Cohesion: 0.13 -Nodes (13): AppSocket, waitFor(), CloseCall, fakeDocker, FakeSocket, fakeUpstream, forwarded, hello (+5 more) +Nodes (14): Admin role, Audit log, Authentication, code:json ({), Container isolation, Control plane container privileges, Demo mode, Email allowlist (+6 more) -### Community 94 - "Rebuild Workspaces CLI Args" +### Community 103 - "Proxy Header Test Suite" Cohesion: 0.13 Nodes (14): 10. Third-party software, 11. Changes to these terms, 12. Governing law, 13. Contact, 1. What CodeRunner is, 2. Who may use it, 3. Acceptable use, 4. Your code (+6 more) -### Community 95 - "Users CLI Script" -Cohesion: 0.13 -Nodes (14): Can CodeRunner run offline or without internet?, Can I build or start simulation from the WPILib extension?, Can I write my own lessons?, Can students accidentally break each other's work?, Can students push code to GitHub?, Do students need accounts? What if I just want to try it?, FAQ, How much does cloud hosting cost? (+6 more) - -### Community 96 - "Extension Reconciliation Test" +### Community 104 - "Container Lease Schema (V1/V2)" Cohesion: 0.13 Nodes (14): 031 — Containerized Control Plane, Addendum — non-root control container, Admin bootstrap: `CODERUNNER_ADMIN_EMAIL`, Bind-mount path translation, code:yaml (user: "${CODERUNNER_UID:-1000}:${CODERUNNER_GID:-1000}"), `coderunner` — a dispatching CLI, not raw scripts, Consequences, Context (+6 more) -### Community 97 - "AdvantageScope Verify Script" +### Community 105 - "Core Schema Tables & Better Auth Migration" Cohesion: 0.19 Nodes (15): /admin/workspaces/:id/backup route (manual per-workspace backup), S9-S11 command-injection defense tests, MockWorkspaceRuntimeProvider, admin backup/restore workspace project, Bundled/remote catalog lesson load flow (gitless), GitHub team import flow (all-branches, depth-1, keeps .git), ImportManager, ImportRateLimiter (+7 more) -### Community 98 - "Gamepad Session Safety Disables" +### Community 106 - "WebSocket Upstream Message Send" Cohesion: 0.13 Nodes (15): authClient (better-auth), defaultDockerRunner, inspectContainerOrThrow, configureLogging, getLogger, loadProviders, LoginPage Component, OAuthButton Component (+7 more) -### Community 99 - "Slug/Login Property Tests" -Cohesion: 0.2 -Nodes (15): MAX_ACTIVE_CONTAINERS capacity limit, Container labels + reconciliation, Decision 011: V2 Editor Spike, Decision 012: V2 Code Image, Decision 013: V2 Acceptance Pass, Decision 017: linuxserver Base Migration, First-run init behavior, gitpod/openvscode-server base image (+7 more) +### Community 107 - "Deployment Hardware & FAQ Docs" +Cohesion: 0.14 +Nodes (13): compact, console_, disable, editor, editorBody, editorControl, freshConsole, hideEditor (+5 more) -### Community 100 - "Container Isolation & HALSim Decisions" -Cohesion: 0.15 -Nodes (15): bun run e2e:workspace-java (real Java workspace smoke), deploy-cloudflare job in deploy.yml, ghcr.io/mathewdunne/coderunner-workspace image, deploy.yml GitHub Actions workflow, GCE docker compose stack (control/caddy/alloy), PATHPLANNER_DIST_TAG pin, coderunner rebuild-workspaces CLI, release.yml GitHub Actions workflow (+7 more) +### Community 108 - "E2E ControlApp Test Fixture Setup" +Cohesion: 0.14 +Nodes (10): Doc, fetchMock, firstKey, GUIDE, README, REPORT_INDEX, { rerender }, ROOT_INDEX (+2 more) + +### Community 109 - "Driver Station Page Object & Runtime Seeding" +Cohesion: 0.2 +Nodes (7): IDELayout(), IDELayoutProps, FakeResizeObserver, useSplit(), ResizableHandle(), ResizablePanel(), ResizablePanelGroup() -### Community 101 - "Cloudflare Pages Proxy Function" +### Community 110 - "AS Lite NT4 Endpoint Injection Patch" Cohesion: 0.25 Nodes (10): DistDownload, downloadAndExtract(), run(), withScratch(), artifacts, main(), repoRoot, tagArgIndex (+2 more) -### Community 102 - "Project Swap Fake Socket Test" +### Community 111 - "Editor Pane Reachability" Cohesion: 0.14 -Nodes (13): Admin role, Audit log, Authentication, code:json ({), Container isolation, Control plane container privileges, Demo mode, Email allowlist (+5 more) +Nodes (13): 1. Prove the report delivery model, 2. Add contracts and read-only delivery, 3. Add Preview UI, 4. Validate the complete workflow, 5. Document and finish, Acceptance criteria, Existing integration points, Goal and agreed UX (+5 more) -### Community 103 - "Proxy Header Test Suite" +### Community 112 - "Gamepad Session Lifecycle Methods" Cohesion: 0.14 Nodes (13): Building the image locally, code:bash (bun run docker:build:workspace), code:block2 (docker build -f containers/code/Dockerfile -t ghcr.io/mathew), code:bash (bun run docker:pull:workspace), code:bash (bun run docker:build:workspace), code:bash (bun run docker:rebuild-workspaces), code:bash (bun run docker:rebuild-workspaces -- --dry-run), Editor acceptance smoke (+5 more) -### Community 104 - "Container Lease Schema (V1/V2)" -Cohesion: 0.19 -Nodes (14): Classroom-density memory defaults, coderunner-workspace bind-mount contract, coderunner-workspace conservative JVM/Gradle memory bounds, V2 code container (coderunner-workspace) README, Decision 024: Container Memory Budget, Decision 025 (two-phase sim runner), Decision 025: Detach Sim JVM from Gradle, Decision 037 (Gradle daemon settings vs java.import.gradle.*) (+6 more) - -### Community 105 - "Core Schema Tables & Better Auth Migration" +### Community 113 - "Admin Workspace Backup/Restore Actions" Cohesion: 0.2 Nodes (13): hello-world bundled module, robot-starter bundled module, catalog/modules.json, docker() helper (Bun.spawn wrapper), openJavaFile(), java-tooling.spec.ts (docker smoke test), startWorkspace(), waitFor() polling helper (+5 more) -### Community 106 - "WebSocket Upstream Message Send" +### Community 114 - "Auto Choosers & Container Status Hooks" Cohesion: 0.17 Nodes (11): FakeNt4Handle, FakeNt4Options, startFakeNt4(), aliceMessages, aliceReady, aliceSocket, allAlice, allBob (+3 more) -### Community 107 - "Deployment Hardware & FAQ Docs" +### Community 115 - "Java Tooling Compatibility Decisions" +Cohesion: 0.22 +Nodes (12): stripHopByHopHeaders, getDemoSession(), getSessionFromRequest(), isAllowedWebSocketOrigin(), isLoopbackHost(), log, normalizeHost(), admin allowlist CRUD (+4 more) + +### Community 116 - "Demo Mode Disk/Memory Limits" Cohesion: 0.28 Nodes (11): config, db, log, AppliedMigrationRow, applyMigrations(), ensureMigrationTable(), listAppliedMigrations(), loadMigrations() (+3 more) -### Community 108 - "E2E ControlApp Test Fixture Setup" +### Community 117 - "Editor Migration Decisions" Cohesion: 0.15 Nodes (11): assetDir, assetsDir, config, cookie, formData, manifest, proc, userAssetDir (+3 more) -### Community 109 - "Driver Station Page Object & Runtime Seeding" +### Community 118 - "Playwright Fixture Bootstrap Helpers" Cohesion: 0.17 Nodes (12): adminCookie, alice, aliceCookie, bob, bobCookie, body, carol, fakeContainerFor() (+4 more) -### Community 110 - "AS Lite NT4 Endpoint Injection Patch" +### Community 119 - "Gamepad Shim E2E Test" Cohesion: 0.15 Nodes (12): 100 MB size cap, 6 imports per hour rate limit, Backup before import, Clone inside the container, not the host, Consequences, Context, Decision, Decision 016 — Project Import Strategy (+4 more) -### Community 111 - "Editor Pane Reachability" +### Community 120 - "Driver Station Page Object" Cohesion: 0.15 Nodes (12): 036 — Editor migration: openvscode-server → VSCodium reh-web, 1. VSCodium `reh-web`, not `code-server`, 2. Stay on the LinuxServer base image, 3. The editor keeps container port 3000, 4. Settings and extension paths do not move, 5. Workspace trust: diagnosed, not fixed, Consequences, Context (+4 more) -### Community 112 - "Gamepad Session Lifecycle Methods" +### Community 121 - "Catalog Integrity Test" Cohesion: 0.15 Nodes (12): 009 - LSP reconnect, bridge serialization, and startup throttling, Context, Decision 1: Browser LSP client auto-reconnects with bounded backoff, Decision 2: Bridge serializes JDT LS spawns, Decision 3: Orchestrator-level LSP startup throttle, Decision 4: Cap proxy pending-message buffers, Decision 5: NT4 subprotocol mismatch is fail-fast, not silent, Decision 6: AS Lite in-iframe timeout banner (+4 more) -### Community 113 - "Admin Workspace Backup/Restore Actions" +### Community 122 - "Fake Socket Test Double" +Cohesion: 0.15 +Nodes (13): createApp (app.ts), Physical disk vs virtual device filtering for --device-read-bps, listWorkspaceDiskLimitDevices (containers/block-devices), ControlConfig (config.ts), codeDiskReadLimit validation against Docker byte-rate format, loadControlConfig (config.ts), parseBoolean fallback-to-default semantics for env vars, containers.ts barrel module (+5 more) + +### Community 123 - "Session Hook & Heartbeat" +Cohesion: 0.17 +Nodes (13): container_leases table (V1 core schema), idx_container_leases_lsp_port_unique, idx_container_leases_sim_port_unique, container_leases.lsp_state column, container_leases.code_state column, V2 merged openvscode-server + sim image design, container_leases.vscode_container column, container_leases.vscode_port column (+5 more) + +### Community 124 - "Auto Choosers Hook Test" Cohesion: 0.19 Nodes (13): Archive 005: Java LSP MVP integration, Archive 006: Multi-tenancy spike findings, Archive 007: V1 sim container orchestration, Decision 032: Canonical Image Naming, Decision Logs README index, Docker labels as runtime source of truth, SQLite as cache, Dual-mode runtime provider (port mode / network mode), Local WPILib-aware JDT LS image (frc-lsp:mvp) (+5 more) -### Community 114 - "Auto Choosers & Container Status Hooks" +### Community 125 - "HALSim Test Suite" +Cohesion: 0.17 +Nodes (13): deploy-cloudflare job in deploy.yml, Decision 039: PathPlanner integration, Deploy-files API (/u/:slug/api/deploy-files/...), deploy.yml GitHub Actions workflow, GCE docker compose stack (control/caddy/alloy), PATHPLANNER_DIST_TAG pin, coderunner rebuild-workspaces CLI, release.yml GitHub Actions workflow (+5 more) + +### Community 126 - "Backup Database CLI" Cohesion: 0.19 Nodes (13): Decision 038: PathPlanner integration, deployFileDeleteResponse(), deployFileWriteResponse(), Deploy-files contracts (zod schemas), deployFilePathSchema path traversal safety design, deployFilesSnapshotResponse(), apps/control/src/app/deploy-files.ts, apps/control/src/metrics.ts (templateRoute) (+5 more) -### Community 115 - "Java Tooling Compatibility Decisions" -Cohesion: 0.23 -Nodes (6): IDELayout(), IDELayoutProps, FakeResizeObserver, ResizableHandle(), ResizablePanel(), ResizablePanelGroup() - -### Community 116 - "Demo Mode Disk/Memory Limits" +### Community 127 - "Command Injection Test Suite" Cohesion: 0.18 Nodes (8): SimRunAction, patchCall, postCall, { result }, updatedStatus, VALID_STATUS, useSimulationState(), UseSimulationStateReturn -### Community 117 - "Editor Migration Decisions" -Cohesion: 0.17 -Nodes (9): HalSimBridgeUnavailableError, { bridge }, { bridge, socket }, disable, joystick, messages, outcome, sessions (+1 more) +### Community 128 - "Default-Deny Route Coverage" +Cohesion: 0.26 +Nodes (11): ascopeRoot, assert(), createCatalogDir(), distDir, exists(), patchDir, repoRoot, runGit() (+3 more) -### Community 118 - "Playwright Fixture Bootstrap Helpers" +### Community 129 - "Lessons & Testing Decision Docs" Cohesion: 0.17 -Nodes (11): config, cookie, dataDir, fakeDocker, lease, name, now, passthrough (+3 more) +Nodes (9): HalSimBridgeUnavailableError, { bridge }, { bridge, socket }, disable, joystick, messages, outcome, sessions (+1 more) -### Community 119 - "Gamepad Shim E2E Test" +### Community 130 - "Build Failure.spec" Cohesion: 0.17 Nodes (9): args, argsPath, home, projectRoot, robotJar, sleeper, start, stop (+1 more) -### Community 120 - "Driver Station Page Object" +### Community 131 - "Admin.po" Cohesion: 0.32 Nodes (11): Args, dirExists(), discoverWorkspaces(), fileExists(), main(), parseArgs(), restoreArchive(), restoreDb() (+3 more) -### Community 121 - "Catalog Integrity Test" +### Community 132 - "Path Planner Pane.test" Cohesion: 0.18 Nodes (9): ExtensionRecord, fixture(), old, reconcileScript, record(), repoRoot, staleDirectory, temporaryRoots (+1 more) -### Community 122 - "Fake Socket Test Double" -Cohesion: 0.26 -Nodes (11): ascopeRoot, assert(), createCatalogDir(), distDir, exists(), patchDir, repoRoot, runGit() (+3 more) +### Community 133 - "Ws Bridge" +Cohesion: 0.17 +Nodes (12): code:typescript (test("s6 service script launches codium-server as primary pr), code:typescript (* In-process HTTP+WS server that impersonates the workspace ), code:bash (git add containers/code apps/control/src/__tests__ apps/cont), code:bash (cd /home/matt/dev/CodeRunner), code:bash (rm -rf containers/code/root/etc/s6-overlay/s6-rc.d/svc-openv), code:bash (find containers/code/root -type f | sort), code:block5 (containers/code/root/etc/s6-overlay/s6-rc.d/init-frc-setup/d), code:dockerfile (# V2 merged code container) (+4 more) -### Community 123 - "Session Hook & Heartbeat" +### Community 134 - "Main" Cohesion: 0.17 Nodes (12): code:bash (curl -sS https://raw.githubusercontent.com/VSCodium/vscodium), code:markdown (| [openvscode-server](https://github.com/gitpod-io/openvscod), code:markdown (| [VSCodium](https://github.com/VSCodium/vscodium) / Code – ), code:markdown (- **`linuxserver/vscodium-web` container image** — GNU Gener), code:markdown (Merged per-student container for V2. Combines VSCodium reh-w), code:markdown (| Base image | linuxserver/vscodium-web:1.126.04524-ls35 | G), code:markdown (| 3000 | codium-server (HTTP + WebSocket) — overrides the ba), code:markdown (The container uses s6-overlay for process supervision. The u) (+4 more) -### Community 124 - "Auto Choosers Hook Test" +### Community 135 - "Logging.test" Cohesion: 0.17 -Nodes (12): code:typescript (test("s6 service script launches codium-server as primary pr), code:typescript (* In-process HTTP+WS server that impersonates the workspace ), code:bash (git add containers/code apps/control/src/__tests__ apps/cont), code:bash (cd /home/matt/dev/CodeRunner), code:bash (rm -rf containers/code/root/etc/s6-overlay/s6-rc.d/svc-openv), code:bash (find containers/code/root -type f | sort), code:block5 (containers/code/root/etc/s6-overlay/s6-rc.d/init-frc-setup/d), code:dockerfile (# V2 merged code container) (+4 more) +Nodes (11): Architecture, code:text (Browser (one student)), code:text (data/), How a Run works, How PathPlanner files flow, How Preview reads documents, How telemetry flows, Persistence and data layout (+3 more) -### Community 125 - "HALSim Test Suite" +### Community 136 - "Middleware" Cohesion: 0.17 Nodes (11): Changes to this policy, Children's privacy, Contact, How long it is kept, How the information is used, Privacy Policy, What information is collected, What is not done with it (+3 more) -### Community 126 - "Backup Database CLI" +### Community 137 - "Metadata" Cohesion: 0.17 Nodes (11): Automated Verification, code:block1 (bun run measure), code:bash (bun run typecheck), Comparison with V1, Decision, Decision 013: V2 Acceptance Pass, Host Capacity (10 students), Manual Verification (+3 more) -### Community 127 - "Command Injection Test Suite" -Cohesion: 0.17 -Nodes (12): createApp (app.ts), containers.ts barrel module, Control plane restart rediscovers labeled containers (reconciliation), seedDemoUser, createAdvantageScopeDist, createCatalogDir, createPathPlannerDist, createWebDist (+4 more) - -### Community 128 - "Default-Deny Route Coverage" -Cohesion: 0.24 -Nodes (12): isInsideDirectory, pathplannerResponse, readScopeAssetManifest, safeRelativeAssetPath, scopeResponse, staticFileResponse, webAssetResponse, deployFileDeleteResponse (+4 more) +### Community 138 - "Image" +Cohesion: 0.21 +Nodes (12): GamepadSessions (gamepad.ts), Gamepad release/close safety-disables joystick and driver station, GamepadSessions test suite, HalSimBridge (halsim.ts), HalSimBridgeUnavailableError (halsim.ts), HalSimBridge test suite, createFakeDocker, dockerInspect (fixture builder) (+4 more) -### Community 129 - "Lessons & Testing Decision Docs" +### Community 139 - "023 Metrics And Observability" Cohesion: 0.2 Nodes (12): run_jobs table (V1 core schema), sessions table (V1 core schema), users table (V1 core schema), workspaces table (V1 core schema), Better Auth migration: drop pre-OAuth auth model, run_jobs_new table (Better Auth rebuild), workspaces_new table (Better Auth rebuild), workspaces.current_module column (+4 more) -### Community 130 - "Build Failure.spec" -Cohesion: 0.18 -Nodes (12): slugFromEmail, login, routing and shell APIs test suite, RunJob (type), slugFromEmail property tests (P4-P6), AppStorage, ensureWorkspaceFiles, ensureWorkspaceForUser (slug collision suffixing) (+4 more) +### Community 140 - "User Data" +Cohesion: 0.2 +Nodes (8): removeCodeContainer(), removeCodeVolume(), stopWorkspaceSim(), codeContainerName(), codeVolumeName(), code container orchestration test suite, container reconciliation test suite, s6 svc-vscodium-web run script (codium-server launcher) -### Community 131 - "Admin.po" +### Community 141 - "Ws Proxy.spec" Cohesion: 0.18 Nodes (12): Backup/restore flow removal, Bundled vs remote lesson catalog, Decision 016: Project Import Strategy, Decision 021: Testing Suite Implementation, Decision 022: Skip Docker Smoke and Import Tests, Decision 029: Lessons and Modules (referenced), Decision 038: Workspace Java Smoke (referenced), Docker smoke tier (skipped, broad) (+4 more) -### Community 132 - "Path Planner Pane.test" +### Community 142 - "Halsim.test" +Cohesion: 0.21 +Nodes (12): Container isolation (memory/disk/port caps), Container ports (3000/3300/5810), Decision 015: HALSim WebSocket Control Protocol, Decision 016: Imported Project Simulation Compatibility, Decision 018: Gamepad Input via HALSim WebSocket, Decision 019: Keyboard Input Mode, GamepadSessions class, Control-plane HALSim bridge (+4 more) + +### Community 143 - "Gamepad.test" Cohesion: 0.17 Nodes (12): AdvantageScope git submodule pinned to release tag, Archive 001: Sim container architecture, Archive 002: AdvantageScope Lite hosted standalone, Archive 003: Minimal web shell, AS Lite GET /assets and /assets// routes, AS Lite window.location.hostname NT4 auto-detection, AS Lite embedded via iframe, not bundled/proxied, eclipse-temurin:17-jdk-jammy base image choice (+4 more) -### Community 133 - "Ws Bridge" +### Community 144 - "Audit Prune" Cohesion: 0.24 Nodes (8): Env, isProxiedPath(), onRequest(), PagesFunctionContext, serviceUnavailable(), TOP_LEVEL_PROXIED, assetRequests, proxiedRequests -### Community 134 - "Main" +### Community 145 - "Users" Cohesion: 0.25 Nodes (3): FakeSocket, Listener, { result } -### Community 135 - "Logging.test" +### Community 146 - "README" +Cohesion: 0.22 +Nodes (9): PaneVisibility, readVisibility(), restored, { result }, { result, rerender }, { result, unmount }, UpperPanes, usePaneVisibility() (+1 more) + +### Community 147 - "Helpers" Cohesion: 0.24 Nodes (9): IconRail(), IconRailButton(), IconRailButtonProps, IconRailProps, RailTab, Tooltip(), TooltipContent(), TooltipProvider() (+1 more) -### Community 136 - "Middleware" +### Community 148 - "Catalog.test" Cohesion: 0.2 Nodes (8): missing(), aliceCookie, dockerRunner(), fakeDocker, receivedHeaders, result, source, startedAt -### Community 137 - "Metadata" -Cohesion: 0.22 -Nodes (8): removeCodeContainer(), removeCodeVolume(), stopWorkspaceSim(), codeContainerName(), codeVolumeName(), code container orchestration test suite, container reconciliation test suite, s6 svc-vscodium-web run script (codium-server launcher) - -### Community 138 - "Image" +### Community 149 - "Block Devices" Cohesion: 0.18 Nodes (10): code:bash (git clone https://github.com/mathewdunne/CodeRunner coderunn), code:bash (cd website && bun install && bun run start), code:bash (bun run docs:dev), code:bash (bun run dev:control # Bun control plane on :4000 with --wa), code:bash (bun run verify # typecheck + Bun tests + Vitest + Pla), CodeRunner, Development, Documentation (+2 more) -### Community 139 - "023 Metrics And Observability" +### Community 150 - "Ws Bridge" Cohesion: 0.18 Nodes (10): Build, code:bash (yarn), code:bash (yarn start), code:bash (yarn build), code:bash (USE_SSH=true yarn deploy), code:bash (GIT_USER= yarn deploy), Deployment, Installation (+2 more) -### Community 140 - "User Data" +### Community 151 - "020 Workspace Runtime Provider" Cohesion: 0.18 Nodes (10): code:bash (git clone https://github.com/mathewdunne/CodeRunner coderunn), code:bash (CODERUNNER_DOCKER_GID=$(stat -c '%g' /var/run/docker.sock) C), code:bash (CODERUNNER_DEMO_MODE=1 docker compose up), code:powershell ($env:CODERUNNER_DEMO_MODE = "1"; docker compose up), code:bat (set "CODERUNNER_DEMO_MODE=1" && docker compose up), code:bash (docker compose down --remove-orphans), Deploy for a team, Prerequisites (+2 more) -### Community 141 - "Ws Proxy.spec" +### Community 152 - "030 Control Plane Hardening Pass" Cohesion: 0.18 Nodes (10): Capacity limit, Classroom-density memory defaults, code:block1 (frc-sim.managed=true), Container labels, Container ports, First-run behavior, How student data persists, Idle auto-stop (+2 more) -### Community 142 - "Halsim.test" -Cohesion: 0.18 -Nodes (10): Architecture, code:text (Browser (one student)), code:text (data/), How a Run works, How PathPlanner files flow, How telemetry flows, Persistence and data layout, The single front door (+2 more) - -### Community 143 - "Gamepad.test" +### Community 153 - "Auth" Cohesion: 0.18 Nodes (10): Backup and Restore, Build, CLI Reference, Containerized ops: the `coderunner` CLI, Database, Docker Images and Containers, Docs Site, Quality and Tests (+2 more) -### Community 144 - "Audit Prune" +### Community 154 - "2026 08 30 Pathplanner Integration" Cohesion: 0.18 Nodes (10): code:block1 (https://github.com//), Constraints and limits, How to do an import, Importing a Team Project, Pushing and pulling after import, Running an imported project, Switching away discards the workspace, What a team import is for (+2 more) -### Community 145 - "Users" +### Community 155 - "Xss.spec" Cohesion: 0.18 Nodes (10): 017 — Migrate code container to linuxserver/openvscode-server, Bind mount target changes from /home/frc to /config, Context, Decisions, Layered s6-overlay: additive, not replacement, Migration Notes, Runtime PUID/PGID instead of build-time --user, Sim scripts remain independent of s6 (+2 more) -### Community 146 - "README" -Cohesion: 0.2 -Nodes (11): container_leases table (V1 core schema), idx_container_leases_lsp_port_unique, idx_container_leases_sim_port_unique, container_leases.lsp_state column, container_leases.code_state column, V2 merged openvscode-server + sim image design, container_leases.vscode_container column, container_leases.vscode_port column (+3 more) +### Community 156 - "Build Ascope Lite" +Cohesion: 0.22 +Nodes (11): sendUpstreamWebSocketMessage, PROXY_PENDING_LIMIT, SocketData union type, createWebSocketHandlers, openProxyUpstream, resolveGamepadLease, Characterization-test-before-refactor pattern, websocket message router characterization tests (+3 more) -### Community 147 - "Helpers" +### Community 157 - "Index" Cohesion: 0.2 Nodes (11): CODE_DISK_READ_LIMIT (--device-read-bps cap), CODE_MEMORY_LIMIT raised to 4096m, /config as named volume in demo mode, Decision 028: Demo mode for zero-config local tryout, Decision 033: Workspace Disk Read Limit, Decision 034: Demo mode portability on Docker Desktop, 2026-07-07 disk-thrash production incident, group_add default CODERUNNER_DOCKER_GID=0 (+3 more) -### Community 148 - "Catalog.test" +### Community 158 - "Backup" Cohesion: 0.18 Nodes (11): disk.tf (Terraform disk config), Hardware sizing guidance for team, FAQ, Why first build/run is slow, Offline capability of CodeRunner, Boot disk disposable, data disk precious, Seasonal Teardown, Manual data-disk snapshot (+3 more) -### Community 149 - "Block Devices" +### Community 159 - "Rebuild Workspaces" Cohesion: 0.18 Nodes (11): AdvantageScope Lite NT4 client, e2e/fixtures/app.ts (ControlApp test fixture), E2E_TEST=1 gates Better Auth testUtils plugin, e2e/fixtures/fake-halsim.ts, e2e/fixtures/fake-nt4.ts, e2e/global-setup.ts, NT4 wire protocol: MessagePack binary + JSON text, Always rebuild web bundle before E2E (stale dist worse than none) (+3 more) -### Community 150 - "Ws Bridge" -Cohesion: 0.18 -Nodes (8): ControlApp in-process fixture / createApp(), Decision 018 — gamepad unplug safety, Decision 019 — keyboard focus, Decision 022 — skip Docker smoke / import tests, e2e/fixtures/fake-nt4.ts, Commit 95f450d (auto chooser stale fix), Commit cb9fea6 (gamepad selection persistence + no-lease), e2e/fixtures/gamepad-shim.ts - -### Community 152 - "030 Control Plane Hardening Pass" +### Community 160 - "Img Screenshots" Cohesion: 0.22 -Nodes (8): SimButton(), SimButtonProps, SimControlsBlock(), SimControlsBlockProps, onRestart, onStart, onStop, TONE_CLASSES +Nodes (11): ascope-iframe.spec.ts (T34.1 /scope route test), 001-lite-nt4-endpoint-injection.patch, embedded-mode NT4 endpoint injection mechanism, patches/advantagescope/README.md, runServerMessageSchema, file-permissions.spec.ts (T6.1 exec failure test), Decision 013: per-workspace NT4 isolation, nt4-multi-workspace.spec.ts (T35.1 NT4 isolation test) (+3 more) -### Community 153 - "Auth" +### Community 162 - "Code Container Defaults.test" Cohesion: 0.24 Nodes (8): AllianceToggle(), AllianceToggleProps, Side, SIDE_ACTIVE, SIDE_EDGE, SIDE_STATION, sideOf(), onSelect -### Community 154 - "2026 08 30 Pathplanner Integration" -Cohesion: 0.24 -Nodes (6): EditorPane(), EditorPaneProps, EditorReachability, EditorStatus, { result }, useEditorReachability() +### Community 163 - "Frc Robot" +Cohesion: 0.22 +Nodes (8): SimButton(), SimButtonProps, SimControlsBlock(), SimControlsBlockProps, onRestart, onStart, onStop, TONE_CLASSES -### Community 155 - "Xss.spec" +### Community 164 - "2026 08 30 Pathplanner Integration" +Cohesion: 0.31 +Nodes (3): GamepadSessions, halsimTarget(), resolveLease() + +### Community 165 - "Restore" +Cohesion: 0.2 +Nodes (9): Choosing a document, If a report looks wrong, Plain-Java lessons, Reading Documents (Preview), Refresh, Refreshing a document, Supported files, Things it deliberately does not do (+1 more) + +### Community 166 - "Login.po" Cohesion: 0.2 Nodes (9): 037 — Workspace cache aliases, extension pins, and trust, 1. Share the primed Gradle distribution with WPILib projects, 2. Prevent gallery resolution from replacing pinned VSIXs, 3. Disable workspace trust in the hosted workbench, 4. Keep Gradle daemon limits out of editor build arguments, 5. Close the workspace-image build follow-ups, Browser-owned settings finding, code:text (permwrapper -> wrapper) (+1 more) -### Community 156 - "Build Ascope Lite" +### Community 167 - "Default Deny.test" Cohesion: 0.2 Nodes (9): 1. Why not Better Auth `testUtils`, 2. Auth-callback path tests are deferred, 3. Browser-heavy specs use `test.fixme`, not deletion, 4. HTTP-driven specs preferred over DOM-driven specs where possible, 5. Decisions on smaller details, Decision 021: Testing suite implementation — deviations from TESTING-PLAN.md, Files Touched, Future Work (Deferred) (+1 more) -### Community 157 - "Index" +### Community 168 - "Auth Demo.test" Cohesion: 0.2 Nodes (9): Decision 018: Gamepad Input via HALSim WebSocket, Files Touched, Future Work (Deferred), Safety: disable on disconnect, Summary, Why a dedicated WebSocket from browser to control plane, Why a single controller on port 0 for v1, Why HALSim WS, not the FRC DS UDP protocol (+1 more) -### Community 158 - "Backup" +### Community 169 - "Robot" Cohesion: 0.2 Nodes (9): Alternatives considered, App-side stays vendor-neutral, code:json ({"timestamp":"2026-05-21T14:23:01.482Z","level":"info","cate), Consequences, Context, Decision, Decision 027: Ship control-plane logs to Grafana Cloud Loki, Label cardinality (+1 more) -### Community 159 - "Rebuild Workspaces" +### Community 170 - "Cleanup Containers" Cohesion: 0.2 Nodes (9): Base image: `gitpod/openvscode-server:1.105.1`, Consequences, Context, Decision, Decision 012: V2 Code Image — Base Image and Extension Strategy, Direct launch base path handling, Extension cache seeding pattern, Extensions: download at build time (+1 more) -### Community 160 - "Img Screenshots" +### Community 171 - "Config" Cohesion: 0.2 Nodes (9): 030 — Control-Plane Hardening Pass, Consequences, Context, Correctness fixes, Decision, Performance, Security, Status (+1 more) -### Community 161 - "Use Container Status.test" +### Community 172 - "Use Gamepad" Cohesion: 0.2 Nodes (9): 004 - Backend wiring for save and run, Context, Custom WebSocket sender, no new dependency, Decisions, Host backend plus Docker CLI, Minimal endpoints and run protocol, One-command dev stack without Docker Compose, Replaceable sim process inside long-lived container (+1 more) -### Community 162 - "Code Container Defaults.test" +### Community 173 - "016 Project Import Strategy" Cohesion: 0.2 Nodes (9): 007 - V1 sim container orchestration, code:text (frc-sim.managed=true), Context, Decisions, Docker labels are adopted back into SQLite, Lazy ensure, visible status, Loopback-only published ports, Runtime cache seed (+1 more) -### Community 163 - "Frc Robot" +### Community 174 - "035 Multi Arch Images And Workflow Split" Cohesion: 0.2 Nodes (9): 008 - V1 LSP container and Bun-native bridge, Browser LSP client extended for multi-file projects, Bun-native bridge instead of `vscode-ws-jsonrpc`, code:block1 (data/users//project -> /workspace/project), `container_leases` lease state split, Context, Decisions, Generic `ContainerOrchestrator` (+1 more) -### Community 164 - "2026 08 30 Pathplanner Integration" +### Community 175 - "[[path]]" Cohesion: 0.24 -Nodes (10): container_leases.halsim_port column, container_leases_new table (Better Auth rebuild), HalSimBridge (halsim.ts), HalSimBridge test suite, createFakeDocker, dockerInspect (fixture builder), Nt4AutoChooserBridge, Superseded run must not clobber newer run's status (+2 more) +Nodes (10): Admin workspace backup action, Admin workspace restore action, createProjectArchive, restoreProjectArchive, runTar, isInsideDirectory, deployFileDeleteResponse, deployFileWriteResponse (+2 more) -### Community 165 - "Restore" +### Community 176 - "" +Cohesion: 0.24 +Nodes (10): Admin allowlist endpoints, addAllowlistEntry (auth/allowlist), loadAllowlist (auth/allowlist), reloadAllowlist, removeAllowlistEntry, saveAllowlist, CODERUNNER_ADMIN_EMAIL bootstrap admin design, ensureWorkspaceForUser (storage) (+2 more) + +### Community 177 - "Robot Container" Cohesion: 0.2 Nodes (10): readError, useAutoChoosers test suite, useAutoChoosers, useContainerStatus test suite, useContainerStatus, probeEditor, useEditorReachability test suite, useEditorReachability (+2 more) -### Community 166 - "Login.po" +### Community 178 - "Clean" Cohesion: 0.2 Nodes (10): code-server --abs-proxy-base-path (rejected), Decision 011: V2 editor spike, Decision 017: LinuxServer base migration, Decision 026: Editor Default Theme, Decision 036: Editor migration openvscode-server to VSCodium reh-web, Machine settings.json theme seeding, --user-data-dir ignored by codium-server (post-review correction), VSCodium reh-web chosen over code-server (+2 more) -### Community 167 - "Default Deny.test" +### Community 179 - "Logging" Cohesion: 0.22 Nodes (10): Decision 037: Workspace cache aliases, extension pins, and trust, Decision 038: Java tooling compatibility, extension reconciliation, and real-image smoke, --disable-workspace-trust server flag, --do-not-include-pack-dependencies flag, Separate tooling (JDT, Java 21) and project (Java 17) JDKs, bun run e2e:workspace-java real-container smoke, Managed extension reconciliation on every container start, Gradle daemon JVM args rejected by Tooling API (+2 more) -### Community 168 - "Auth Demo.test" -Cohesion: 0.31 -Nodes (10): Grafana Alloy config template (config.alloy.tmpl), Alloy log pipeline bounded active-series design, bootstrap.sh first-boot provisioning script, Caddyfile (GCE deployment), Decision 031 (containerized control plane), GCE cloud-init user-data.yaml, deploy/ directory README, docker-compose.prod.yml (Caddy/Alloy overlay) (+2 more) +### Community 180 - "Metrics.test" +Cohesion: 0.2 +Nodes (9): DriverStationPage (page object), e2e/fixtures/gamepad-shim.ts, installGamepadShim(), e2e/fixtures/runtime.ts, seedWorkspaceProject(), Switch Project picker (auto-opens on empty workspace), Using CodeRunner, Using CodeRunner: PathPlanner section (+1 more) -### Community 169 - "Robot" +### Community 181 - "Store" Cohesion: 0.22 Nodes (6): test (Playwright base.extend AppFixtures), e2e/fixtures/fake-vscode.ts, MockWorkspaceRuntimeProvider, e2e/fixtures/runtime.ts, seedRuntimeRunning(), startFakeVscode() -### Community 170 - "Cleanup Containers" +### Community 182 - "004 Backend Wiring" Cohesion: 0.31 Nodes (5): connectGamepad(), installGamepadShim(), setGamepadAxes(), consoleErrors, leaseErrors -### Community 171 - "Config" +### Community 183 - "Dist Download" Cohesion: 0.22 Nodes (7): catalog, catalogRoot, ids, manifestPath, repoRoot, subdirPath, lessonCatalogSchema -### Community 172 - "Use Gamepad" +### Community 184 - "Img Screenshots" +Cohesion: 0.25 +Nodes (6): LoadState, heartbeatCalls, { result }, { unmount }, VALID_SESSION, useSession() + +### Community 185 - "Main" Cohesion: 0.25 Nodes (6): patchCall, { result }, updatedResponse, VALID_RESPONSE, useAutoChoosers(), UseAutoChoosersReturn -### Community 173 - "016 Project Import Strategy" +### Community 186 - "Main" Cohesion: 0.39 Nodes (8): Args, backupDatabase(), dirExists(), fileExists(), main(), parseArgs(), runTar(), timestamp() -### Community 174 - "035 Multi Arch Images And Workflow Split" +### Community 187 - "Typecheck" Cohesion: 0.22 Nodes (8): bridge, enabledDisable, flush, joystick, messages, msg, sockets, zeroButtons -### Community 175 - "[[path]]" +### Community 188 - "Sim Api.test" Cohesion: 0.22 -Nodes (8): bashCalls, cloneCall, ctx, importer, mock, row, workspace, GithubImportContext - -### Community 176 - "" -Cohesion: 0.31 -Nodes (7): configMountType(), containerAttachedToNetwork(), containerHasPublishedPorts(), isLoopbackHost(), publishedPortFor(), PublishedPort, WorkspaceRow +Nodes (8): Arrange your workspace, Console lessons, Explore more, Get started, PathPlanner, Preview, Robot lessons and imported projects, Using CodeRunner -### Community 177 - "Robot Container" +### Community 189 - "Switch Project Dialog" Cohesion: 0.22 Nodes (8): 038 — Java tooling compatibility, extension reconciliation, and real-image smoke, Add a targeted real-container smoke, Consequences, Context, Decision, Keep separate tooling and project JDKs, Pin a compatible Java extension matrix, Reconcile managed extensions on every container start -### Community 178 - "Clean" +### Community 190 - "Dialog" Cohesion: 0.22 Nodes (8): Alternatives considered, Auth, Cardinality discipline, Consequences, Context, Decision, Decision 023: Metrics and observability via Prometheus + Grafana Cloud, Status -### Community 179 - "Logging" +### Community 191 - "024 Container Memory Budget" Cohesion: 0.22 Nodes (9): GATED_PATHS list, PUBLIC_PATHS list, Default-deny auth coverage via explicit route manifest (no route table introspection), default-deny route coverage test suite (Plan §A.7.6), PathPlanner subtree writable, choreo subtree read-only design, safeRelativeAssetPath (asset path validation), Symlink and path-traversal defense for deploy-files write/delete, deploy-files snapshot/write/delete test suite (+1 more) -### Community 180 - "Metrics.test" -Cohesion: 0.22 -Nodes (9): Decision 015 (HALSim control protocol), Decision 016 (imported-project sim compat), Decision 016: Imported Project Simulation Compatibility, libstdc++6 PPA upgrade (GLIBCXX_3.4.32), robot-starter build.gradle, robot-starter headless sim configuration (no GUI/DS extensions), robot-starter settings.gradle, sim-headless.init.gradle init script (+1 more) +### Community 192 - "Scripts" +Cohesion: 0.31 +Nodes (7): containerAttachedToNetwork(), containerHasPublishedPorts(), isLoopbackHost(), publishedPortFor(), workspaceHomePath(), PublishedPort, WorkspaceRow -### Community 181 - "Store" +### Community 193 - "Global Setup" Cohesion: 0.28 Nodes (9): coderunner dispatching CLI, containers/control/entrypoint.sh, Configuration Reference, Quick Start (Installation), Troubleshooting Guide, CODERUNNER_DOCKER_GID socket permission fix, OAuth login failure troubleshooting, Port range exhausted troubleshooting (+1 more) -### Community 182 - "004 Backend Wiring" -Cohesion: 0.31 -Nodes (9): Code container VS Code defaults test suite, containers/code/Dockerfile, codium-server (VSCodium reh-web), Decision 033: workspace disk read limit, containers/code/root/.../init-frc-setup/run, robot-starter .vscode/settings.json, svc-vscodium-web/run service script, Why codium-server over code-server: --server-base-path support (+1 more) - -### Community 183 - "Dist Download" -Cohesion: 0.28 -Nodes (9): ascope-iframe.spec.ts (T34.1 /scope route test), 001-lite-nt4-endpoint-injection.patch, embedded-mode NT4 endpoint injection mechanism, patches/advantagescope/README.md, Decision 013: per-workspace NT4 isolation, nt4-multi-workspace.spec.ts (T35.1 NT4 isolation test), pane-layout.spec.ts (pane sizing persistence test), "mocked" Playwright project (+1 more) +### Community 195 - "Sidebars" +Cohesion: 0.29 +Nodes (6): PathPlannerPane, PathPlannerPaneProps, first, frame, { rerender }, second -### Community 185 - "Main" +### Community 196 - "Vite.config" Cohesion: 0.29 Nodes (6): BigButton(), BigButtonProps, EnableDisableRow(), EnableDisableRowProps, onSetEnabled, TONE_ACTIVE -### Community 186 - "Main" -Cohesion: 0.29 -Nodes (6): PathPlannerPane, PathPlannerPaneProps, first, frame, { rerender }, second +### Community 197 - "Constants" +Cohesion: 0.25 +Nodes (7): banner, configInput, demoFlag, log, port, server, enableDefaultMetrics() -### Community 187 - "Typecheck" +### Community 198 - "Icon Rail" Cohesion: 0.29 Nodes (6): defaultLogFormat(), parseLogFormatEnv(), cyclic, err, line, parsed -### Community 188 - "Sim Api.test" +### Community 199 - "Dropdown Menu" Cohesion: 0.25 -Nodes (7): banner, configInput, demoFlag, log, port, server, enableDefaultMetrics() +Nodes (7): CodeRunner, How to use CodeRunner, Lessons and team projects, Next steps, Self-hosted and modest to run, Video walkthrough, What students get -### Community 189 - "Switch Project Dialog" +### Community 200 - "Scroll Area" Cohesion: 0.25 -Nodes (7): CodeRunner, How to use CodeRunner, Lessons and team projects, Next steps, Self-hosted and modest to run, Video walkthrough, What students get +Nodes (7): 041 — Project Preview (Markdown + generated HTML reports), Alternatives rejected, Consequences, Context, Decision, Tested compatibility boundary, The blocking constraint we found first -### Community 190 - "Dialog" +### Community 201 - "Use Gamepad Channel" Cohesion: 0.25 Nodes (7): Broad Docker smoke tier — not implemented, Consequences, Context, Decision, Decision 022: Skip Docker smoke tier and import/backup-restore E2E tests, Import and backup/restore tests — updated after lessons rework, Status -### Community 191 - "024 Container Memory Budget" +### Community 202 - "Workspace Page" Cohesion: 0.25 Nodes (7): 005 - Java LSP MVP integration, Context, Decisions, Local WPILib-aware JDT LS image, Package and Vite choices, Plain Monaco client with direct LSP requests, Verification -### Community 192 - "Scripts" -Cohesion: 0.25 -Nodes (6): Canonical registry-qualified image names, [command, kind], dockerfiles, image, Kind, subprocess - -### Community 193 - "Global Setup" +### Community 203 - "README" Cohesion: 0.32 -Nodes (8): Decision 023: Metrics and Observability, Decision 023: Metrics and Observability, Decision 027: Ship control-plane logs to Grafana Cloud Loki, alloy service (prod override), Grafana Alloy, Grafana Cloud, Loki log shipping pipeline, Prometheus exposition (/metrics) - -### Community 194 - "Docusaurus.config" -Cohesion: 0.25 -Nodes (8): apps/control/src/app/assets.ts, createPathPlannerDist(), DriverStationPage (page object), e2e/fixtures/gamepad-shim.ts, installGamepadShim(), pathplannerResponse(), Using CodeRunner, Using CodeRunner: PathPlanner section +Nodes (8): authorizeMetrics, constantTimeEqual, getDemoSession, Default-deny session gating pattern, getSessionFromRequest, requireAdmin, requireSession, requireWorkspaceOwnership -### Community 195 - "Sidebars" +### Community 204 - "Cleanup Containers" Cohesion: 0.29 -Nodes (8): AdminPage (page object), apps/control/src/app.ts, CLI Reference, apps/control/src/config.ts (ControlConfig), createApp() (ControlApp factory), containers/control/Dockerfile, scripts/fetch-dist.ts, PathPlanner 503 troubleshooting +Nodes (8): isEmailAllowed (auth/allowlist), createAuth, slugFromEmail, login, routing and shell APIs test suite, slugFromEmail property tests (P4-P6), ensureWorkspaceFiles, ensureWorkspaceForUser (slug collision suffixing) -### Community 196 - "Vite.config" +### Community 205 - "Cloudflare Pages Function.test" Cohesion: 0.25 -Nodes (8): Decision 017: LinuxServer base migration, Decision 026: editor default theme, Decision 034: recursive chown retained, Decision 036: VSCodium reh-web migration, Decision 037: --disable-workspace-trust flag, openvscode-server (deprecated editor), VSCodium-web Editor Migration Plan, Workspace trust blocks redhat.java Standard Mode +Nodes (6): Canonical registry-qualified image names, [command, kind], dockerfiles, image, Kind, subprocess -### Community 197 - "Constants" -Cohesion: 0.32 -Nodes (4): createApp(), RunManager (run job lifecycle), makeScriptedRunCommandFactory(), MockWorkspaceRuntimeProvider +### Community 206 - "Extension Reconciliation.test" +Cohesion: 0.25 +Nodes (8): Decision 017: LinuxServer base migration, Decision 026: editor default theme, Decision 034: recursive chown retained, Decision 036: VSCodium reh-web migration, Decision 037: --disable-workspace-trust flag, openvscode-server (deprecated editor), VSCodium-web Editor Migration Plan, Workspace trust blocks redhat.java Standard Mode -### Community 198 - "Icon Rail" +### Community 207 - "Users" Cohesion: 0.29 Nodes (6): consoleErrors, echoPromise, helloPromise, ws, ws1, ws2 -### Community 201 - "Use Gamepad Channel" +### Community 208 - "Vitest.config" +Cohesion: 0.29 +Nodes (4): beforePreview, simRequests, toggle, workspace + +### Community 209 - "Setup" +Cohesion: 0.29 +Nodes (5): INITIAL_STATE, ProjectSwapKind, ProjectSwapState, ProjectSwapStatus, UseProjectSwapReturn + +### Community 210 - "Sim Headless.init" Cohesion: 0.33 -Nodes (4): ListBlockDevicesOptions, listWorkspaceDiskLimitDevices(), log, devices +Nodes (5): CATALOG, fetchMock, { result }, useLessons(), UseLessonsReturn -### Community 202 - "Workspace Page" +### Community 213 - "Helpers" Cohesion: 0.29 Nodes (6): args, beforeIndex, beforeMs, countResult, db, dryRun -### Community 203 - "README" +### Community 214 - "Helpers" Cohesion: 0.29 Nodes (4): [command, email], db, UserRow, users -### Community 204 - "Cleanup Containers" -Cohesion: 0.29 -Nodes (6): Console lessons, Explore more, Get started, PathPlanner, Robot lessons and imported projects, Using CodeRunner - -### Community 205 - "Cloudflare Pages Function.test" +### Community 215 - "Admin Routes" Cohesion: 0.29 Nodes (7): code:bash (IMAGE=ghcr.io/mathewdunne/coderunner-workspace:latest), code:bash (curl -s http://127.0.0.1:33999/u/smoke/vscode/ | grep -oE '/), code:bash (curl -s -i --max-time 5 \), code:bash (docker exec cr-smoke ls /config/extensions), code:bash (docker exec cr-smoke stat -c '%U:%G %n' /config/.gradle /con), code:bash (docker exec cr-smoke java -version), Task 3: Build the image and verify the editor serves under the base path -### Community 206 - "Extension Reconciliation.test" +### Community 216 - "Websocket" Cohesion: 0.29 Nodes (6): Bundled third-party software, CodeRunner, Lesson content, Licenses, Modifications, What this means for you -### Community 207 - "Users" +### Community 217 - "Converters" Cohesion: 0.29 Nodes (6): Bundled catalog (default, zero-config), How Lessons Work, Remote catalog (your own lessons repo), The two bundled demo modules, Two catalog sources, one menu, What happens when a student loads a lesson -### Community 208 - "Vitest.config" +### Community 218 - "Docker Client" Cohesion: 0.29 Nodes (6): Choose a deployment route, Cloudflare Offline Page (advanced, optional), Deployment Overview, Google Cloud Deployment (advanced), Local Deployment (recommended), What both need -### Community 209 - "Setup" +### Community 219 - "Storage" Cohesion: 0.29 Nodes (6): 028 — Demo mode for zero-config local tryout, Affected code, Constraints, Context, Decision, Why synthetic over real sessions -### Community 210 - "Sim Headless.init" +### Community 220 - "Demo Banner" Cohesion: 0.29 Nodes (6): 016 — Imported project simulation compatibility, Context, Decisions, Gradle init script for headless simulation override, Upgrade libstdc++6 via Ubuntu toolchain PPA, Verification -### Community 211 - "Build" +### Community 221 - "Theme Provider" Cohesion: 0.29 Nodes (6): code:json ({), Consequences, Context, Decision, Decision 026: Editor Default Theme, Status -### Community 212 - "Settings" +### Community 222 - "Button" Cohesion: 0.29 Nodes (6): 032 — Canonical Image Naming, code:block1 (${CODERUNNER_IMAGE_NS:-ghcr.io/mathewdunne}/coderunner-contr), Consequences, Context, Decision, Status -### Community 213 - "Helpers" +### Community 223 - "Card" Cohesion: 0.29 Nodes (6): 033 — Workspace Disk Read Limit, Alternatives considered, Consequences, Context, Decision, Status -### Community 214 - "Helpers" +### Community 224 - "Card" Cohesion: 0.29 -Nodes (7): handleAdminRoute, directorySizeBytes, queryAuditLog, recordAuditEvent, apiErrorResponse, adminStatusResponse, auditActor +Nodes (7): createAdvantageScopeDist, createCatalogDir, createPathPlannerDist, createWebDist, withApp, idle lifecycle and admin controls test suite, AdvantageScope Lite and NT4 routing test suite -### Community 215 - "Admin Routes" +### Community 225 - "Card" +Cohesion: 0.43 +Nodes (7): BundledCatalogSource (catalog.ts), CatalogSource interface, createCatalogSource (catalog.ts), parseCatalogRepo (catalog.ts), RemoteCatalogSource (catalog.ts), Two-source lesson catalog pattern (bundled + remote), ImportError (imports.ts) + +### Community 226 - "Card" Cohesion: 0.29 Nodes (7): demo mode test suite, DEMO_SLUG (auth/demo), AdvantageScope (bundled, modified), CodeRunner (project), Demo Mode Quick Start, VSCodium (bundled), WPILib (bundled) -### Community 216 - "Websocket" +### Community 227 - "Card" +Cohesion: 0.33 +Nodes (4): ListBlockDevicesOptions, listWorkspaceDiskLimitDevices(), log, devices + +### Community 228 - "Card" Cohesion: 0.29 Nodes (7): BridgeEntryBase (type), ReconnectingWsBridge.ensureEntry, HalSimBridge, Nt4AutoChooserBridge, ReconnectingWsBridge.open, ReconnectingWsBridge, entry.socket !== socket stale-guard pattern -### Community 217 - "Converters" +### Community 229 - "Card" Cohesion: 0.33 Nodes (7): Decision 020: Workspace Runtime Provider Boundary, control service (docker-compose.yml), coderunner Docker network, workspace-template service, caddy service (prod override), LocalDockerRuntimeProvider, WorkspaceRuntimeProvider interface -### Community 218 - "Docker Client" +### Community 230 - "Dialog" Cohesion: 0.29 -Nodes (5): e2e/fixtures/auth.ts (loginAs helper), Better Auth cookie URL-encoding consistency, loginAs(), LoginPage (page object), signToken() (HMAC session signing) +Nodes (7): Baseline response security headers, Batched docker inspect, Capacity admission on container adoption, Decision 030: Control-Plane Hardening Pass, Nt4AutoChooserBridge, ReconnectingWsBridge base class, Stale run-job status write fix -### Community 219 - "Storage" -Cohesion: 0.33 -Nodes (3): addAllowlistEntry(), allowlist.spec.ts (auth), oauth-callback.spec.ts (OAuth callback flow) +### Community 231 - "Dialog" +Cohesion: 0.29 +Nodes (5): e2e/fixtures/auth.ts (loginAs helper), Better Auth cookie URL-encoding consistency, loginAs(), LoginPage (page object), signToken() (HMAC session signing) -### Community 220 - "Demo Banner" +### Community 232 - "Dialog" Cohesion: 0.48 Nodes (6): UI written and tested but deliberately unwired, PathPlannerPane.tsx, ScopePane.tsx, SimPaneSwitcher.tsx, apps/web/src/routes/WorkspacePage.tsx, WorkspacePage (page object) -### Community 221 - "Theme Provider" -Cohesion: 0.29 -Nodes (7): sessionResponseSchema, open-workspace.spec.ts (first-login empty project test), "security" Playwright project, response-headers.spec.ts (S19/S20 header tests), xss.spec.ts (S16/S17 XSS tests), S16 malicious display name test, S17 run console text rendering test - -### Community 222 - "Button" +### Community 233 - "Dialog" Cohesion: 0.33 Nodes (7): DEPLOY_FILES_WRITE_ROOT constant, deployFilePathSchema, deny-list rationale for PathPlanner deploy file paths, openWorkspace() test helper, pathplannerLoads() helper, pathplanner-pane.spec.ts (sim pane tool tabs test), WorkspacePage page object -### Community 223 - "Card" +### Community 234 - "Dialog" +Cohesion: 0.29 +Nodes (7): sessionResponseSchema, open-workspace.spec.ts (first-login empty project test), "security" Playwright project, response-headers.spec.ts (S19/S20 header tests), xss.spec.ts (S16/S17 XSS tests), S16 malicious display name test, S17 run console text rendering test + +### Community 235 - "Dialog" Cohesion: 0.29 Nodes (6): applyAdvantageScopePatches(), ensureEmscripten(), main() build orchestration, skip owlet/docs postinstall rationale, runPostinstallForLite(), EMSDK (Emscripten SDK) 4.0.12 -### Community 224 - "Card" +### Community 236 - "Dialog" Cohesion: 0.29 Nodes (7): audit_log SQLite table, apps/control/src/auth/allowlist, backupDatabase() (SQLite serialize snapshot), data/users//{project,assets} backup layout, scripts/allowlist.ts (allowlist CLI), scripts/audit-prune.ts, scripts/backup.ts -### Community 225 - "Card" +### Community 237 - "Dialog" Cohesion: 0.38 Nodes (5): container_leases table, clearContainerLeases(), rebuildWorkspaces(), rebuildWorkspaces test suite, Grafana ops dashboard screenshot: host VM, workspaces, runs, control-plane panels -### Community 226 - "Card" +### Community 238 - "Dropdown Menu" Cohesion: 0.33 Nodes (7): PathPlanner Pane Overview Screenshot, Sign-In Page Screenshot, Switch Project Dialog Screenshot, Team Import Progress Screenshot, Using CodeRunner - Ready State Screenshot, Using CodeRunner - Start State Screenshot, Workspace Shell Three-Pane Layout Screenshot -### Community 227 - "Card" +### Community 239 - "Dropdown Menu" Cohesion: 0.4 Nodes (4): { result }, { unmount }, VALID_STATUS, useContainerStatus() -### Community 228 - "Card" +### Community 240 - "Dropdown Menu" Cohesion: 0.33 Nodes (5): dockerfile, initScript, repoRoot, robotSettings, settings -### Community 229 - "Card" +### Community 241 - "Dropdown Menu" Cohesion: 0.33 -Nodes (6): code:bash (git add ), code:bash (bun run docs:build && grep -rl "vscodium-web-migration" webs), code:bash (git add docs/decisions/036-vscodium-web-migration.md docs/de), File Structure, Task 5: Full regression gate, Task 7: Record decision log 036 +Nodes (5): Background — verified findings, Follow-up disposition (closed 2026-08-26), Global Constraints, Out of scope, VSCodium-web Editor Migration Implementation Plan -### Community 230 - "Dialog" +### Community 242 - "Dropdown Menu" Cohesion: 0.33 -Nodes (5): Background — verified findings, Follow-up disposition (closed 2026-08-26), Global Constraints, Out of scope, VSCodium-web Editor Migration Implementation Plan +Nodes (6): code:bash (git add ), code:bash (bun run docs:build && grep -rl "vscodium-web-migration" webs), code:bash (git add docs/decisions/036-vscodium-web-migration.md docs/de), File Structure, Task 5: Full regression gate, Task 7: Record decision log 036 -### Community 231 - "Dialog" +### Community 243 - "Dropdown Menu" Cohesion: 0.33 Nodes (6): code:bash (docker exec cr-smoke sh -c 'cat /config/data/User/settings.j), code:bash (| (if $mode == "project" then . else ."security.workspace.tr), code:bash (bun run docker:build:workspace), code:bash (git add containers/code/root/etc/s6-overlay/s6-rc.d/init-frc), code:bash (docker rm -f cr-smoke), Task 4: Verify Java reaches Standard Mode; fix workspace trust only if it does not -### Community 232 - "Dialog" +### Community 244 - "Dropdown Menu" Cohesion: 0.33 Nodes (5): 020 - Workspace runtime provider boundary, Consequences, Context, Decision, Status -### Community 233 - "Dialog" +### Community 245 - "Dropdown Menu" Cohesion: 0.33 Nodes (5): 024 — Bound Per-Workspace Container Memory, Consequences, Context, Decision, Status -### Community 234 - "Dialog" +### Community 246 - "Dropdown Menu" Cohesion: 0.33 Nodes (5): 035 — Multi-Arch Images and CI/Release/Deploy Workflow Split, Consequences, Context, Decision, Status -### Community 235 - "Dialog" +### Community 247 - "Dropdown Menu" Cohesion: 0.33 Nodes (5): 034 — Demo mode portability on Docker Desktop, Consequences, Context, Decision, What was deliberately not done -### Community 236 - "Dialog" +### Community 248 - "Dropdown Menu" Cohesion: 0.33 Nodes (5): 025 — Detach the Simulation JVM from Gradle, Consequences, Context, Decision, Status -### Community 237 - "Dialog" +### Community 249 - "Dropdown Menu" +Cohesion: 0.33 +Nodes (6): RunJob (type), AppStorage, AppStorage.normalizeWorkspaceProjectPaths, RunJobRow (type), AppStorage.seedBootstrapAdmins, WorkspaceRow (type) + +### Community 250 - "Dropdown Menu" Cohesion: 0.53 Nodes (6): AdvantageKit Logger, robot-starter catalog lesson module, robot-starter Constants.java template class, robot-starter Main.java template class, robot-starter Robot.java template class, robot-starter RobotContainer.java template class -### Community 238 - "Dropdown Menu" -Cohesion: 0.4 -Nodes (6): Decision 039: PathPlanner integration, Deploy-files API (/u/:slug/api/deploy-files/...), pathplanner-dist.tar.gz packaging, PathPlanner (upstream), PathPlanner topbar tab (iframe beside AdvantageScope), pathplanner-web fork (mathewdunne/pathplanner-web) - -### Community 239 - "Dropdown Menu" +### Community 251 - "Resizable" Cohesion: 0.33 -Nodes (6): Decision 010: Gradle Project Cache Isolation for Sim and LSP, --project-cache-dir $HOME/.gradle-project-sim, stop-sim.sh, V1-10 three-user smoke test, non-destructive Gradle headless override (strips GUI, enables WS server), robot lesson kind - -### Community 242 - "Dropdown Menu" -Cohesion: 0.4 -Nodes (3): denied, GATED_PATHS, PUBLIC_PATHS - -### Community 243 - "Dropdown Menu" -Cohesion: 0.4 -Nodes (4): baBody, body, userRow, workspace +Nodes (6): apps/control/src/app.ts, apps/control/src/app/assets.ts, apps/control/src/config.ts (ControlConfig), createApp() (ControlApp factory), createPathPlannerDist(), pathplannerResponse() -### Community 246 - "Dropdown Menu" +### Community 255 - "Separator" Cohesion: 0.5 Nodes (4): dryRun, main(), _repoRoot, run() -### Community 248 - "Dropdown Menu" +### Community 257 - "Tabs" Cohesion: 0.4 Nodes (5): code:bash (# Seed Gradle cache on first run.), code:bash (# Fix ownership for /config and /workspace (linuxserver conv), code:bash (# Fix ownership of what this script created as root. The bas), code:bash (git add containers/code/root/etc/s6-overlay/s6-rc.d/init-frc), Task 2: Scope `init-frc-setup`'s ownership pass to what it actually creates -### Community 249 - "Dropdown Menu" +### Community 258 - "Tabs" Cohesion: 0.4 Nodes (4): Consequences, Context, Decision, Decision 014 — Better Auth Integration -### Community 250 - "Dropdown Menu" +### Community 259 - "Tabs" Cohesion: 0.4 Nodes (4): 029 — Lessons & Modules, Consequences, Context, Implementation decisions -### Community 251 - "Resizable" +### Community 260 - "Tabs" Cohesion: 0.4 Nodes (4): 040 — SELinux container mounts, Consequences, Context, Decision -### Community 252 - "Resizable" +### Community 261 - "Tabs" Cohesion: 0.4 Nodes (4): Consequences, Context, Decision, Decision 019: Keyboard Input Mode -### Community 253 - "Resizable" +### Community 262 - "Tabs" Cohesion: 0.4 Nodes (4): 039 — PathPlanner integration, Consequences, Context, Decision -### Community 254 - "Resizable" +### Community 263 - "Tooltip" Cohesion: 0.4 Nodes (4): 010 - Gradle project cache isolation for sim and LSP, Context, Decisions, Implications -### Community 255 - "Separator" -Cohesion: 0.4 -Nodes (5): Admin workspace backup action, Admin workspace restore action, createProjectArchive, restoreProjectArchive, runTar - -### Community 256 - "Sonner" +### Community 264 - "Tooltip" Cohesion: 0.4 Nodes (5): Decision 018 (gamepad unplug safety), listConnectedGamepads, makeLabel, useGamepad test suite, useGamepad -### Community 257 - "Tabs" +### Community 265 - "Tooltip" Cohesion: 0.4 Nodes (5): Better Auth (OAuth sign-in), isProxiedPath function, onRequest handler, serviceUnavailable function, Decision 014: Better Auth Integration -### Community 258 - "Tabs" +### Community 266 - "Tooltip" Cohesion: 0.4 Nodes (5): ascope-dist filesystem-only stage reused across arch, containers/control/Dockerfile multi-stage build, Decision 035: Multi-Arch Images and CI/Release/Deploy Workflow Split, Multi-arch build via native runners + digest merge, ci.yml / release.yml / deploy.yml split -### Community 259 - "Tabs" -Cohesion: 0.4 -Nodes (4): e2e/fixtures/runtime.ts, seedWorkspaceProject(), Switch Project picker (auto-opens on empty workspace), Switching projects discards workspace (by design) +### Community 267 - "Use Run Channel" +Cohesion: 0.5 +Nodes (5): AdminPage (page object), CLI Reference, containers/control/Dockerfile, scripts/fetch-dist.ts, PathPlanner 503 troubleshooting -### Community 260 - "Tabs" +### Community 268 - "Playwright.config" Cohesion: 0.4 Nodes (5): docs/ content directory, docs/decisions/ decision logs, Docusaurus site config, website/README.md, Docusaurus sidebars config -### Community 262 - "Tabs" +### Community 270 - "Verify Ascope" Cohesion: 0.5 Nodes (3): path, repoRoot, targets -### Community 263 - "Tooltip" +### Community 271 - "Community 271" +Cohesion: 0.5 +Nodes (3): 042 — Collapsible workspace panes, Decision, Validation + +### Community 272 - "Community 272" Cohesion: 0.5 Nodes (3): Active (V2 and post-V2), Archive, Decision Logs -### Community 264 - "Tooltip" +### Community 273 - "Community 273" Cohesion: 0.5 Nodes (3): Robot Starter, Running it, Where the code lives -### Community 265 - "Tooltip" +### Community 274 - "Community 274" Cohesion: 0.5 Nodes (3): Hello, World, Running it, Try stuff -### Community 266 - "Tooltip" +### Community 275 - "Community 275" Cohesion: 0.67 Nodes (4): defaultLogFormat, formatRecordJson, parseLogFormatEnv, logging.ts test suite (formatRecordJson, parseLogFormatEnv, defaultLogFormat) -### Community 267 - "Use Run Channel" +### Community 276 - "Community 276" Cohesion: 0.5 Nodes (4): Route templating for bounded metric cardinality, statusClass, templateRoute, metrics.ts test suite (templateRoute, statusClass) -### Community 268 - "Playwright.config" -Cohesion: 0.67 -Nodes (4): GamepadSessions (gamepad.ts), Gamepad release/close safety-disables joystick and driver station, GamepadSessions test suite, HalSimBridgeUnavailableError (halsim.ts) - -### Community 269 - "Image" +### Community 277 - "Community 277" Cohesion: 0.67 Nodes (4): persist middleware with partialize (inputMode only), UI store test suite, UIState interface, useUIStore Zustand store -### Community 270 - "Verify Ascope" +### Community 278 - "Community 278" Cohesion: 0.5 Nodes (4): Archive 004: Backend wiring for save and run, Custom WebSocket sender (no @fastify/websocket), Host backend plus Docker CLI (fixed argument arrays), Replaceable sim process under tini -### Community 271 - "Community 271" +### Community 279 - "Community 279" Cohesion: 0.83 Nodes (4): downloadAndExtract(), withScratch(), fetch-dist main(), fetchPathPlannerDist() -### Community 272 - "Community 272" +### Community 280 - "Community 280" Cohesion: 0.5 Nodes (4): Demo mode landing screenshot: CodeRunner IDE with demo-mode banner, Driver Station workbench screenshot: sim controls, mode, console output, Lesson catalog / Switch project modal screenshot, Lesson README opened screenshot: editor + AdvantageScope + README preview -### Community 277 - "Community 277" +### Community 285 - "Community 285" Cohesion: 0.67 Nodes (3): Auto-chooser NT4 bridge msgpack decoding, auto-chooser NT4 bridge test, encodeMsgPack test helper -### Community 278 - "Community 278" +### Community 286 - "Community 286" Cohesion: 0.67 Nodes (3): Badge, KindTag, SwitchProjectDialog -### Community 279 - "Community 279" +### Community 287 - "Community 287" Cohesion: 0.67 Nodes (3): Button, DialogContent, DialogFooter -### Community 280 - "Community 280" +### Community 288 - "Community 288" Cohesion: 0.67 Nodes (3): scripts/apply-ascope-patches.ts, scripts/build-ascope-lite.ts, scripts/clean.ts @@ -1521,9 +1565,9 @@ Nodes (3): scripts/apply-ascope-patches.ts, scripts/build-ascope-lite.ts, script scripts/typecheck.ts · relation: conceptually_related_to ## Knowledge Gaps -- **1883 isolated node(s):** `console`, `cookie`, `consoleErrors`, `leaseErrors`, `dangerous` (+1878 more) +- **2003 isolated node(s):** `workspace`, `frame`, `victimWorkspace`, `attackerWorkspace`, `console` (+1998 more) These have ≤1 connection - possible missing edges or undocumented components. -- **86 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes. +- **85 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes. ## Suggested Questions _Questions this graph is uniquely positioned to answer:_ diff --git a/graphify-out/graph.html b/graphify-out/graph.html index 7186343d..a338e7d7 100644 --- a/graphify-out/graph.html +++ b/graphify-out/graph.html @@ -61,12 +61,12 @@

Communities

-
4094 nodes · 5929 edges · 362 communities
+
4297 nodes · 6239 edges · 370 communities