Skip to content

API Endpoints

This page is generated at build time from the route handlers in packages/allocator/src/lablink_allocator_service/routes/ — the docstring on each handler is the documentation you read here. To change it, edit the docstring.

Authentication

LabLink uses four gates. All bearer credentials go in Authorization: Bearer <token>.

Gate Credential Guards
HTTP Basic app.admin_user / app.admin_password from config.yaml /admin/* pages and the operator JSON APIs
Client secret A per-client secret minted when that client registers, stored as an argon2 hash client → allocator telemetry (/api/heartbeat, /api/vm-status, …)
Register token One deployment-wide bootstrap token, also stored hashed POST /api/v1/clients/register only
Signed cookie lablink_session, minted by /api/request_vm GET /desktop

A handful of endpoints are deliberately unauthenticated: / and /api/request_vm (participant-facing), /api/health and /api/unassigned_vms_count (health/monitoring), and /internal/* (reachable only from the allocator's own nginx, never exposed publicly).

Secrets are per-client, so a leaked one compromises a single machine. The register token is the only deployment-wide credential, and it can do nothing but register.

Participant Endpoints

What a workshop participant touches: the landing page and the seat-claim endpoint. The participant supplies nothing but an email address.

GET /

Authentication: None

Landing page.

Renders index.html, the email form a participant submits to claim a seat.

POST /api/request_vm

Authentication: None — this is the only unauthenticated state-changing endpoint in the allocator.

Claim a seat for a participant and redirect them to their desktop.

Request Body: application/x-www-form-urlencoded

  • email (string, required): The participant's email address.

What it does:

  1. Idempotent rejoin. If this email already owns a running seat, it keeps that seat rather than consuming a second one.
  2. Atomic claim. Otherwise assign_vm claims a free seat with SELECT … FOR UPDATE SKIP LOCKED, so concurrent requesters cannot collide on one VM. An empty pool raises and returns 503 with no_seats.html.
  3. Per-session prep. Mints a session_id and browser_token, then rotates the KasmVNC password on the assigned client through that client's local agent. The claim has already committed by this point, so a rotation failure is compensated for, not rolled back: the seat is released and the VM flagged Unhealthy.
  4. Cookie + redirect. Signs a lablink_session cookie bound to the session_id and redirects to GET /desktop.

Success Response:

  • Code: 303 See Other → /desktop, with the lablink_session cookie set.

Error Response:

  • Code: 503 Service Unavailable — no_seats.html when the pool is empty, or rotation_failed.html when the assigned client could not be reached. On a rotation failure the seat is released and the VM flagged Unhealthy so the participant isn't wedged on a dead machine.
  • Code: 200 OK — index.html re-rendered with an error if email is missing.

The participant supplies nothing but an email address.

GET /api/unassigned_vms_count

Authentication: None (health/monitoring).

Get the number of available (unassigned) VMs.

Returns the current count of VMs that are running and not yet assigned to a user.

Success Response:

  • Code: 200 OK
  • Content:
    {
      "count": 5
    }
    

Client Usage: Not used by the client service. Intended for external monitoring or UI components on the allocator to display the number of available VMs.

Participant Desktop

GET /desktop

Authentication: The signed lablink_session cookie.

The participant desktop.

Renders the noVNC viewer page. Reads the cookie minted by POST /api/request_vm, looks up the assigned VM by session_id, and configures the viewer from the persisted browser_ws_url and browser_credential. If the cookie is missing, invalid, or the bound VM is no longer running, it redirects to / so the participant can submit their email again.

Internal Proxy Authorization

auth_request subrequest targets for the allocator's own nginx. Never routed publicly.

GET|POST /internal/proxy_auth

Authentication: None — an auth_request subrequest target for the allocator's own nginx, never routed publicly.

nginx auth_request gate for the desktop WebSocket proxy.

nginx calls this before proxying desktop bytes, to check that the requesting session is entitled to the client it is asking for: the signed lablink_session cookie must resolve to the VM whose browser_token appears in the original URI.

GET|POST /internal/tunnel_auth

Authentication: None

nginx auth_request gate for the reverse-tunnel WebSocket upgrade.

The path prefix identifies which client is attaching; the bearer token is what authenticates it. Both must agree, so a client cannot attach under another client's prefix even holding a valid secret of its own.

This check is the ONLY binding between a secret and an identity. tunnel_manager's restrictions are keyed by path prefix, so they grant whatever alias the presented path claims -- they constrain a client to one alias, they do not prove who the client is. Do not describe them as a second authentication layer.

Health

GET /api/health

Authentication: None — this is what load balancers, lablink deploy and lablink status poll.

Readiness check.

Returns a structured readiness report rather than a bare 200. The checks map always covers database, scheduler and reboot_service, and gains a tailscale entry when the configured connectivity needs one, or a tunnel entry for reverse-tunnel deployments.

Success Response:

  • Code: 200 OK when every gating check is ok
  • Content:
    {
      "status": "healthy",
      "checks": {
        "database": "ok",
        "scheduler": "ok",
        "reboot_service": "ok"
      },
      "uptime_seconds": 123.4
    }
    

Error Response:

  • Code: 503 Service Unavailable with "status": "starting" when a gating check fails.

A dead client tunnel does not make the allocator unready: unattached client tunnels are reported but deliberately don't gate readiness — a registering client is unattached until its tunnel comes up. The allocator's own dependencies (tunnel server, database) do gate.

GET /api/health/connections

Authentication: Admin (HTTP Basic)

Report PostgreSQL connection usage.

Reports usage against the server's configured maximum, for checking headroom before scaling a deployment up.

active_connections counts client backends only — the things max_connections bounds — excluding the background-worker rows pg_stat_activity also carries. It includes the connection serving this request. max_connections is read from PostgreSQL, so it tracks whatever is actually configured.

idle_in_transaction distinguishes a leaked pooled connection from ordinary load; a non-zero value that doesn't fall back to zero is worth investigating. It counts idle in transaction (aborted) too — an aborted-but-open transaction pins locks just like a live one.

The same numbers appear on the /admin panel, which escalates to a banner above 90%.

Success Response:

  • Code: 200 OK
  • Content:
    {
      "status": "ok",
      "active_connections": 61,
      "idle_in_transaction": 0,
      "max_connections": 300,
      "utilization_percent": 20.3,
      "level": "ok"
    }
    

level is ok, warning above 80% utilization, or critical above 90%. High utilization still answers 200 — busy is not broken.

Error Response:

  • Code: 503 Service Unavailable with {"status": "unavailable"} when the numbers can't be read — the database isn't initialized yet, or the query failed (including a pool with no free connections). Never means the pool is merely busy.

Client VM Telemetry

Client VMs report upward through these; they are never told about an assignment. The allocator pushes assignments the other way, by calling the client's local agent (see POST /api/request_vm and Database). All telemetry writes require that client's own secret as a bearer token — see Authentication.

POST /api/update_inuse_status

Authentication: Client secret (Bearer)

Update the in-use status of a VM.

Called by the client VM to indicate whether a user is actively using it.

Request Body: application/json

{
  "hostname": "lablink-vm-prod-1",
  "status": true
}

Success Response:

  • Code: 200 OK
  • Content: {"message": "In-use status updated successfully."}

Error Response:

  • Code: 400 Bad Request if hostname or status is missing.
  • Code: 500 Internal Server Error on failure.

Client Usage: Called by the update_inuse_status service, which is started by start.sh and runs continuously. The service monitors for the presence of the research software process (e.g., sleap). When the process starts, it sends status: true; when it stops, status: false. This lets the allocator know if a user is actively using the VM.

POST /api/gpu_health

Authentication: Client secret (Bearer)

Update the GPU health status of a VM.

Request Body: application/json

{
  "hostname": "lablink-vm-prod-1",
  "gpu_status": "healthy"
}

Success Response:

  • Code: 200 OK
  • Content: {"message": "GPU health status updated successfully."}

Error Response:

  • Code: 400 Bad Request if hostname or gpu_status is missing.
  • Code: 500 Internal Server Error on failure.

Client Usage: Called by the check_gpu service, which is started by start.sh and runs continuously. The service periodically runs nvidia-smi, determines the GPU status (Healthy, Unhealthy, or N/A), and reports whenever the status changes. The value lands in the Healthy column — the auto-reboot service decides what to do about an unhealthy VM.

POST /api/heartbeat

Authentication: Client secret (resolved from the vm_id field).

Record a client-VM liveness heartbeat.

Periodic liveness ping. Lets the allocator detect silent failures — a dead container, a broken network, a hung host, an OOM, or an out-of-band VM termination — that no other endpoint would reveal. The body also carries cheap health signals for early warning.

Request Body: application/json

{
  "vm_id": "lablink-vm-prod-1",
  "boot_id": "…",
  "disk_free_pct": 62
}

Client Usage: Sent by the client's heartbeat service every HEARTBEAT_INTERVAL_SECONDS. A client that stops heartbeating for longer than the staleness window becomes eligible for automatic recovery.

POST /api/vm-status

Authentication: Client secret (Bearer)

Update the overall status of a VM.

Called by the client VM during its startup sequence to report its current status (initializing, running, error, rebooting).

Request Body: application/json

{
  "hostname": "lablink-vm-prod-1",
  "status": "running"
}

Success Response:

  • Code: 200 OK
  • Content: {"message": "VM status updated successfully."}

Error Response:

  • Code: 400 Bad Request if hostname or status is missing.
  • Code: 500 Internal Server Error on failure.

Client Usage: Not called from the packages/client code. It is called by the user_data.sh script during the client VM's initial boot sequence (cloud-init), which reports initializing, running, and error statuses so the allocator can track the VM's progress before the client service container has started. An error trap in user_data.sh automatically sends an error status if any command fails, which can then trigger the auto-reboot service.

GET /api/vm-status

Authentication: Admin (HTTP Basic)

Get the status of all VMs.

Returns a JSON object mapping each VM hostname to its current status. Used by the admin dashboard.

Success Response:

  • Code: 200 OK
  • Content:
    {
      "lablink-vm-prod-1": "running",
      "lablink-vm-prod-2": "initializing"
    }
    

Error Response:

  • Code: 404 Not Found if no VMs are found in the database.
  • Code: 500 Internal Server Error on failure.

POST /api/vm-logs/<hostname>

Authentication: Client secret (Bearer)

Receive and store logs pushed from a VM.

Receives batched log lines pushed from a VM by the log_shipper.sh script, which tails the VM's cloud-init output and the client container's Docker logs.

URL Parameters:

  • hostname (string, required): The hostname of the VM.

Request Body: application/json

{
  "log_group": "/aws/ec2/lablink",
  "log_stream": "lablink-vm-prod-1",
  "messages": ["log line 1", "log line 2"]
}

Success Response:

  • Code: 200 OK
  • Content: {"message": "VM logs posted successfully."}

Error Response:

  • Code: 400 Bad Request if required fields are missing.
  • Code: 404 Not Found if the VM does not exist.
  • Code: 500 Internal Server Error on failure.

Client Usage: Not called directly from packages/client code. The log_shipper.sh script, installed on each VM by the OpenTofu user-data script, tails the VM's cloud-init output log and the client container's Docker logs, batches new lines, and POSTs them here. ANSI escapes are stripped and each log type is capped at 1MB.

GET /api/vm-logs/<hostname>

Authentication: Admin (HTTP Basic)

Get the stored logs for a specific VM.

Used by the admin log viewer page.

URL Parameters:

  • hostname (string, required): The hostname of the VM.

Query Parameters:

  • errors_only (boolean, optional, default false): When true, both cloud_init_logs and docker_logs are reduced to their error lines — lines carrying an uppercase level token (ERROR, CRITICAL, FATAL, plus cloudflared's ERR/FTL and postgres' PANIC), together with any traceback that follows them. Matching is case-sensitive so the client startup script's --retry-all-errors and status=error lines are not treated as errors, and a short denylist suppresses client reporting-path failures that do not affect the VM's usability (GPU-health and in-use-status report failures, and session-metrics push failures). A stream with no error lines becomes "", not null.

Success Response:

  • Code: 200 OK
  • Content:
    {
      "hostname": "lablink-vm-prod-1",
      "cloud_init_logs": "Starting cloud-init...\n...",
      "docker_logs": "[start] ALLOCATOR_HOST: ...\n...",
      "logs": "Starting cloud-init...\n..."
    }
    

Error Response:

  • Code: 404 Not Found if the VM is not found.
  • Code: 503 Service Unavailable if the logs are not yet available because the VM's log shipper has not started reporting yet.
  • Code: 500 Internal Server Error on failure.

POST /api/vm-metrics/<hostname>

Authentication: Client secret (Bearer)

Receive and store startup metrics from a VM.

Called by the client VM to post timing metrics for cloud-init and container startup.

URL Parameters:

  • hostname (string, required): The hostname of the VM reporting metrics.

Request Body: application/json

{
  "cloud_init_start": 1678886400,
  "cloud_init_end": 1678886460,
  "cloud_init_duration_seconds": 60
}

Success Response:

  • Code: 200 OK
  • Content: {"message": "VM metrics posted successfully."}

Error Response:

  • Code: 404 Not Found if the VM does not exist.
  • Code: 500 Internal Server Error on failure.

Client Usage: user_data.sh posts cloud-init timings during boot, and the client container's start.sh entrypoint posts its own start/end times at the end of its startup sequence. This helps in monitoring startup duration.

Provisioning and Operations

Creating and destroying the VM pool. Long-running OpenTofu work runs asynchronously: launch/destroy enqueue a job in the operations table and the operations endpoints expose its progress.

POST /api/launch

Authentication: Admin (HTTP Basic)

Launch new client VMs.

Takes a number of VMs to create and enqueues an async apply operation; the background worker generates an OpenTofu variables file and runs tofu apply to provision the new instances. Poll GET /api/operations for progress. Rejected with 405 when the provider cannot provision hosts (manual).

Request Body: application/x-www-form-urlencoded

  • num_vms (integer, required): The number of new VMs to launch.

Success Response:

  • Code: 202 Accepted with {"job_id": 7, "status": "queued"} for JSON clients; browsers get 302 → /admin/instances?job=<id>.

Error Response:

  • Code: 409 Conflict when another operation is already in progress; the body carries the in-flight job_id.
  • Code: 400 Bad Request for a missing or non-positive num_vms.

POST /destroy

Authentication: Admin (HTTP Basic)

Destroy all client VMs.

Enqueues an async destroy operation: the background worker runs tofu destroy to terminate all client EC2 instances and associated resources, then clears all records from the vms table. This is a destructive action. Driven by lablink client destroy, and by lablink destroy as its first teardown step. Open session-metrics rows are sealed (best-effort) before teardown so final sessions keep a duration.

Request Body: None

Success Response:

  • Code: 202 Accepted with {"job_id": 8, "status": "queued"} for JSON clients; browsers get 302 → /admin/instances?job=<id>.

Error Response:

  • Code: 409 Conflict when another operation is already in progress.
  • Code: 405 Method Not Allowed when the provider cannot destroy hosts (manual).

GET /api/operations

Authentication: Admin (HTTP Basic)

List recent operations.

Returns recent operations with their op_type, status, timestamps, and resources_completed / resources_total progress counters. ?status=in_progress returns just the currently running operation, if any.

GET /api/operations/<int:operation_id>

Authentication: Admin (HTTP Basic)

Get one operation.

Returns one operation, including its captured output and error. This is what the CLI polls while client launch runs.

Error Response:

  • Code: 404 Not Found if the operation does not exist.

Client Registration API

Used by bring-your-own (BYO) client machines to enrol themselves under the manual provider. AWS-provisioned clients also register, with credentials supplied by OpenTofu.

Registration is what the CLI's lablink client register drives — see Bring-Your-Own Clients for the operator-facing walkthrough, and Configuration for the manual.* settings it depends on.

POST /api/v1/clients/register

Authentication: Register token (the one deployment-wide bearer credential).

Register a client machine and mint its credentials.

Request Body: application/json

Field Required Description
hostname yes The client's self-declared hostname, which becomes its client_id. Must start with a letter or digit and contain only letters, digits, dots, dashes and underscores (max 253 chars).
machine_identity yes Stable machine identifier, unique across the deployment. Re-registering the same identity replaces the old row rather than adding a seat.
provider no Defaults to aws. BYO clients send manual.
provider_metadata no Shape must match the deployment's configured manual.connectivity — lan_ip for lan_direct, overlay_hostname for mesh_overlay, reverse_tunnel: true for reverse_tunnel. A mismatch is rejected here rather than failing opaquely at assignment time.
endpoint_url, gpu_present, gpu_model no Reported by the client; all auto-detected by the CLI.

Success Response:

  • Code: 200 OK
  • Content: the client's credentials and the configuration it needs to start:
{
  "client_id": "gpu-box-3",
  "client_secret": "…",
  "agent_token": "…",
  "allocator_url": "https://lab.example.org",
  "connectivity": "lan_direct",
  "client_image": "ghcr.io/talmolab/lablink-client-base-image:latest",
  "repository": "https://github.com/talmolab/sleap-tutorial-data.git",
  "subject_software": "sleap",
  "register_token": "…",
  "startup_script_b64": "",
  "startup_on_error": "continue",
  "startup_max_attempts": 3,
  "startup_base_delay_seconds": 30,
  "startup_success_check_b64": "",
  "monitoring": { "enabled": false }
}

client_secret is returned once and stored only as an argon2 hash. Reverse-tunnel registrations additionally receive tunnel_url, tunnel_path_prefix and tunnel_bind_addr, all minted by the allocator.

Error Response:

  • Code: 400 Bad Request — missing/invalid hostname or machine_identity, or a provider_metadata shape that doesn't match the configured connectivity.
  • Code: 401 Unauthorized — bad or missing register token.

GET /api/v1/clients/<client_id>/status

Authentication: Bearer client_secret (that client's own secret).

Get one client's status.

Lets a registered client confirm the allocator still knows about it.

DELETE /api/v1/clients/<client_id>

Authentication: Bearer client_secret (the secret minted at register time).

Best-effort caller-driven deregistration.

Hard-deletes the row, even when useremail is set — the BYO operator is voluntarily withdrawing the box, and the student's session is already broken because the local container is going away.

GET /api/v1/clients

Authentication: admin HTTP Basic — same gate as /admin/instances.

List registered clients for operator status views.

Returns only operator-safe columns (no secrets, no log blobs).

POST /api/overlay-hostname

Authentication: Client secret (Bearer)

Record the overlay hostname Tailscale actually assigned this client.

The name captured at registration is only what the client asked for. tailscale up --hostname=X exits 0 even when an existing (possibly offline) node already holds X, in which case Tailscale appends a numeric suffix instead. The allocator dials the recorded name, so an unreconciled rename sends every call to a dead node and the client is marked Unhealthy forever (lablink#404).

Deliberately its own endpoint rather than a field on /api/vm-status: that handler and the client's send_status are shared with the AWS path, where a lost status POST is unrecoverable (see start.sh's own comment and assign_vm's status='running' requirement). Nothing here runs for an AWS or lan_direct client — start.sh only calls it inside its TAILSCALE_AUTHKEY gate.

Lives in this module rather than vm_telemetry because this is where the overlay_hostname contract is already enforced (see register_client's expects_overlay check). Flat /api/ prefix to match the sibling client-VM writes (/api/vm-status, /api/gpu_health, /api/heartbeat) and to stay out of /api/v1/clients/<client_id>'s path space.

Scheduled Destruction API

Lets an operator schedule tear-down ahead of time — useful for capping the cost of a workshop deployment. Schedules are persisted in the scheduled_destructions table.

POST /api/schedule-destruction

Authentication: Admin (HTTP Basic)

Create a new scheduled destruction.

Request JSON: { "schedule_name": "Friday Tutorial End", "destruction_time": "2025-12-05T17:30:00Z", "recurrence_rule": null // or "FREQ=WEEKLY;BYDAY=FR;BYHOUR=17;BYMINUTE=30" }

Returns: Response: JSON with schedule_id and success status, or error with status code.

GET /api/schedule-destruction/<int:schedule_id>

Authentication: Admin (HTTP Basic)

Get details of a scheduled destruction.

GET /api/schedule-destruction

Authentication: Admin (HTTP Basic)

List all scheduled destructions.

Query parameters: status (optional): Filter by status (scheduled, executing, completed, failed, cancelled)

Returns: Response: JSON with list of schedules, or error with status code.

DELETE /api/schedule-destruction/<int:schedule_id>

Authentication: Admin (HTTP Basic)

Cancel a scheduled destruction.

Session Metrics API

Populated only when monitoring.enabled is true.

POST /api/session-metrics/<hostname>

Authentication: Client secret (Bearer)

Report session metrics.

The client's monitoring sampler posts its accumulated per-session counters (time-in-software, GPU activity, training progress).

The 200 response echoes session_started_at — the VM's authoritative assignment time (ISO-8601, null while the seat is free) — which the client writes to its session anchor, healing an anchor lost to a container restart mid-session.

Error Response:

  • Code: 400 Bad Request if counters is missing.
  • Code: 404 Not Found if the VM does not exist.
  • Code: 409 Conflict if the session's row is already sealed.

GET /admin/session-metrics

Authentication: Admin (HTTP Basic)

Render the cohort summary + per-VM table for Tier 1 monitoring.

GET /api/session-metrics/summary

Authentication: Admin (HTTP Basic)

Get the cohort summary.

Returns the aggregate view model — participation funnel plus cohort totals. Both the admin Session Metrics page and lablink stats render this same payload, so the two can never disagree.

GET /api/export-metrics

Authentication: Admin (HTTP Basic)

Export metrics.

Per-VM metrics as CSV or JSON (controlled by ?format=). Backs lablink export-metrics --client.

Allocator Logs

GET /api/allocator-logs

Authentication: Admin (HTTP Basic)

Get the allocator's own logs.

Returns a redacted tail (last 2000 lines) of the allocator's own container output, read from the file start.sh writes at /var/log/lablink/allocator.log. Backs the /admin/allocator-logs page. Values of PASSWORD/TOKEN/SECRET/KEY assignments are masked before the response leaves the process.

Query Parameters:

  • errors_only (boolean, optional, default false): As on GET /api/vm-logs/<hostname> — reduces docker_logs to its error lines. error still reports only a genuinely missing log file; a log with no error lines returns docker_logs: "" with error: null.

Success Response:

  • Code: 200 OK
  • Content:
    {
      "cloud_init_logs": null,
      "docker_logs": "2026-08-03 12:00:00 - Starting nginx on :5000...",
      "error": null
    }
    

The response mirrors /api/vm-logs/<hostname>'s keys so the shared logs template's JavaScript works unchanged. cloud_init_logs is always null: the allocator host's cloud-init output lives outside the container and is out of scope. When no log file exists, docker_logs is null and error explains why — the response is still 200.

GET /admin/allocator-logs

Authentication: Admin (HTTP Basic)

Render the shared logs page pointed at this blueprint's API.

Admin Session Actions

Per-VM operator actions on the Instances page: preview a desktop, join a participant's session, release a seat, clear a health lockout.

GET /admin/instances/<hostname>/peek

Authentication: Admin (HTTP Basic)

View (read-only) a VM already assigned to a participant, without touching useremail or rotating credentials — opens a second WS viewer onto the same live KasmVNC session.

POST /admin/instances/<hostname>/connect

Authentication: Admin (HTTP Basic)

Connect (full control) to a VM not currently assigned to anyone, for admin troubleshooting. Reserves the VM out of the assignable pool (AdminReservedAt) and mints a real session via the same prepare_browser_session path /api/request_vm uses — but never sets useremail.

POST /admin/instances/<hostname>/release

Authentication: Admin (HTTP Basic)

End an admin troubleshooting session, returning the VM to the assignable pool. Posted to by both the dashboard row's Release button and the /desktop wrapper page's Release form.

POST /admin/instances/<hostname>/clear-unhealthy

Authentication: Admin (HTTP Basic)

Clear an allocator-set Unhealthy flag by hand.

A rotation timeout marks a client Unhealthy and assign_vm skips those rows, so the pool can read as empty while the client is perfectly fine. The client cannot clear it itself: check_gpu only reports on change, and on a GPU-less box it posts 'N/A' once and then exits its loop entirely. A successful Admin Connect clears it automatically (see admin_connect_vm); this is the escape hatch for when the box is known good but connecting to it isn't wanted. Before either existed, raw SQL was the only way out (lablink#404).

Admin Pages

Operator HTML pages, all behind HTTP Basic Auth. They are walked through in the Workshop Guide, and /admin/byo-onboarding in Bring-Your-Own Clients.

GET /admin/create

Authentication: Admin (HTTP Basic)

Render the VM creation page (drives POST /api/launch).

GET /admin

Authentication: Admin (HTTP Basic)

Render the admin panel: entry links, pool counts, connection usage.

GET /admin/byo-onboarding

Authentication: Admin (HTTP Basic)

Render the ready-to-copy lablink client register command for BYO clients.

The register token rotates on each allocator restart, so this page is dynamic — re-render to get the current token. Behind admin Basic auth (same gate as the rest of /admin); no new privilege boundary.

The rendered flags depend on the configured connectivity, because register_client rejects a shape that doesn't match it with a 400 (see its expects_overlay/expects_tunnel checks) — a single lan_direct command would be guaranteed-broken copy-paste on the other two modes. Read off client_connectivity.name rather than cfg.manual.connectivity so this page and that check can never disagree.

GET /admin/instances

Authentication: Admin (HTTP Basic)

Render the instances table with per-VM session actions.

GET /admin/instances/fragment

Authentication: Admin (HTTP Basic)

Instances table as an HTML fragment, for the page's auto-refresh.

GET /admin/instances/delete

Authentication: Admin (HTTP Basic)

Render the destroy-confirmation page (drives POST /destroy).

GET /admin/logs/<hostname>

Authentication: Admin (HTTP Basic)

Get the logs for a specific VM.

GET /admin/scheduled-destruction

Authentication: Admin (HTTP Basic)

Render scheduled destruction management page.