Skip to content

feat(lab-envs): UI for backend-agnostic ephemeral lab envs - #2

Open
devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/1777387900-lab-envs
Open

feat(lab-envs): UI for backend-agnostic ephemeral lab envs#2
devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/1777387900-lab-envs

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Apr 28, 2026

Copy link
Copy Markdown

Summary

Adds the web surface for the backend-agnostic ephemeral lab env API introduced in concave#1. New route: /lab/envs. Lists, launches, extends, and archives ephemeral JupyterLab environments (Docker today; Slurm/Proxmox drivers plug into the same UI in follow-ups).

Changes

  • src/types.ts: LabEnv, LabStorage, LabEnvsResponse types.
  • src/lib/labEnvs.ts: API helpers (fetchLabEnvs, launchLabEnv, extendLabEnv, archiveLabEnv, updateLabStorage, setActiveDriver) and countdown helpers.
  • src/views/LabEnvsView.vue:
    • Hero panel — active driver + hot/cold tier summary strip.
    • Launch drawer (developer+) — image, display name, driver picker, TTL hours, GPUs, CPU request, memory request.
    • Env list — live per-row TTL countdown (1s tick / 15s poll), open-Jupyter link, +1h TTL button (developer+), Archive now button (operator+).
    • Storage config (admin) — hot / cold tier paths.
    • Driver selection (admin) — picks the active driver.
  • src/router/index.ts: /lab/envs route, viewer-gated.
  • src/components/layout/AppShell.vue: nav entry between Gradient Lab and Teams.
  • test/labEnvs.test.ts: unit tests for formatCountdown and remainingSeconds.

Gradient-specific touches

  • Role gating in lockstep with server-side gates — no client-only "hide button" security.
  • Displays the mesh-addressable cold-tier path the manifest will reference, so a user knows where a peer could restore the archive.
  • Polls on 15s cadence + local 1s tick so countdowns look live without hammering the API.

Review & Testing Checklist for Human

  • Pair with concave#1 locally (concave serve + concave web). Confirm /lab/envs renders, Launch spawns a Docker container, and the TTL countdown ticks down in real time.
  • Log in as each role (viewer / developer / operator / admin) and verify the launch drawer, extend/archive buttons, and storage/driver panels appear only for the correct roles.
  • Archive an env and confirm the status transitions to archived and the tarball + manifest show up under the configured cold tier.

Notes

  • No new dependencies.
  • Follow-up: a restore panel that takes peer_id + env_id to restore an archived env from a mesh peer. Deferred until the Slurm driver PR (#4).

Link to Devin session: https://app.devin.ai/sessions/5d19efa113054ca4953d9ed9309ce705
Requested by: @ElFariss


Open in Devin Review

Adds a /lab/envs page that lists, launches, extends, and archives
ephemeral JupyterLab environments against the backend-agnostic
/api/v1/lab/envs API introduced in concave.

- types.ts: LabEnv, LabStorage, LabEnvsResponse types.
- lib/labEnvs.ts: API helpers + countdown formatting.
- views/LabEnvsView.vue: launch drawer, env list with live TTL
  countdown, per-row extend/archive/open-jupyter actions, admin-only
  storage and driver configuration panels.
- router + AppShell: new Lab Envs nav entry.
- test/labEnvs.test.ts: unit tests for countdown/remaining helpers.

Role gating matches the server: any viewer can list, developers can
launch and extend, operators can archive-now, admins can reconfigure
storage tiers and the active driver.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment on lines +56 to +57
storageForm.value = { ...payload.storage }
activeDriverForm.value = payload.active ?? ''

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Background poll overwrites admin configuration forms every 15 seconds

The refresh() function at lines 56-57 unconditionally overwrites storageForm and activeDriverForm with fresh server data. Since refresh() is called every 15 seconds by the poll timer (frontend/src/views/LabEnvsView.vue:135), any in-progress edits an admin makes to the "Storage tiers" or "Active driver" forms will be silently discarded. The admin may not notice the reset and either give up or accidentally submit stale values.

Prompt for agents
In LabEnvsView.vue, the refresh() function (called on mount and every 15s via setInterval) overwrites storageForm and activeDriverForm with server data on every call (lines 56-57). This destroys any in-progress admin edits in the Storage tiers and Active driver forms.

Approach: Only populate storageForm and activeDriverForm on the initial load, not on subsequent poll refreshes. One way is to add a boolean flag (e.g. initialized) that is false initially and set to true after the first successful refresh. On subsequent refreshes, skip the lines that overwrite storageForm and activeDriverForm. Alternatively, separate the initial data fetch from the poll refresh logic so that display-only data (envs, storage, drivers, activeDriver) is updated on poll, but the form-bound refs (storageForm, activeDriverForm) are only set once on mount.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

image: form.value.image,
display_name: form.value.display_name || undefined,
driver: form.value.driver || undefined,
gpus: form.value.gpus || undefined,

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 gpus: 0 silently converted to undefined due to falsy OR check

At line 77, gpus: form.value.gpus || undefined uses a falsy check to omit the field. Since 0 is falsy in JavaScript, explicitly requesting 0 GPUs (the form's default value, set at frontend/src/views/LabEnvsView.vue:33) results in gpus being omitted from the JSON payload. This means the server never receives an explicit gpus: 0 — it receives no gpus field at all, and will apply its own default, which may not be 0.

Suggested change
gpus: form.value.gpus || undefined,
gpus: form.value.gpus != null && form.value.gpus > 0 ? form.value.gpus : undefined,
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant