Web app
The web app is Orbit’s live view of the fleet. It is a static single-page app that reads the Gateway API and follows realtime events. Its TypeScript API schema is generated from the Gateway OpenAPI document (docs/openapi.json) with bun run types in apps/web. Operation descriptions and request-field comments in that schema follow the OpenAPI document, including argument and option text that API reference generation reads from the CLI command classes. Regenerate the schema after an OpenAPI change and commit it with the app. To install, run, and check the app, see Bun and Rust projects.
The generated API schema includes PATCH /api/v1/nodes/{node}/wireguard-key, with a wireguard_public_key input and the standard Node response. These types describe the unmanaged peer key contract; they do not add a web key editor. Regenerate them with bun run types when the operation changes.
The task group schema includes nullable vm_power: running, stopped, or destroyed, separately from task status. The Tasks page keeps its status-based grouping and detail view when power is present, null, or omitted. VM power defines the state mapping.
The generated API schema also includes the Project development deploy step operations and their required boolean. These types describe the API contract; they do not add web controls or start deployments.
The generated API schema includes show and update operations for Project Documents storage on the Gateway, with their redacted DocumentStorage response. Credential fields are update inputs only, never response properties. These types describe the storage contract; they do not add storage settings controls to the web app.
The generated Instance response includes nullable annotator_port and annotator_url fields. The URL points to /__orbit/annotator on the Instance Route when a port and Route exist. The generated Process create description also lists the annotator preset. These API schema fields add no web UI control; see Annotator Process.
GitHub feedback fixups use the existing subtask fixup_problem and brief fields. Regenerating the API types confirms that feedback consumption adds no response field or web merge control. Source provenance is task data, not a rendered GitHub instruction or permission to merge.
The generated Instance rename request exposes optional branch and domain fields. The Gateway still requires at least one; optional TypeScript properties do not replace API validation. This API operation adds no web UI control.
The Project schema includes task_workspace_routed, which describes routing for new task workspaces; an existing workspace keeps its recorded mode. The Gateway serves it from its own origin.
The generated Process schema includes the installed PATCH update operation, nullable working_directory, and Docker environment maps for create and update. A null directory means the image default, not an empty path. These types describe Process updates; they add no web editing controls.
The generated Process schema includes nullable user, the explicitly selected account or null for the derived account. Process create accepts user only for a Node systemd Process without a preset. Processes and schedules owns the account validation and working-directory defaults.
Extension navigation follows the Gateway’s enabled set: each page and its route depend only on their own extension. Tasks pages need only the tasks extension. The Quota page needs only proxycli, and it shows provider quota whenever proxycli is enabled and its collector is configured, whether or not Tasks is enabled. Tasks and ProxyCLI links and routes are absent while their extension is disabled. The Gateway API remains authoritative, so a stale direct request still receives extension.disabled rather than granting access.
The Route create request type distinguishes an app Route with instance_id from a custom proxy Route with node_id and an upstream or Process; it has no app Route creation form with project_id or a targetless scope.
The generated API types model Gateway rollback as a code switch without a force input. Database restoration is a separate operator step described in Gateway recovery.
Open the app
Openhttps://gateway.orbit from a machine on the Orbit WireGuard network. The browser must trust the Orbit root certificate, which orbit gateway:trust installs. The Gateway identifies the browser by its WireGuard address, so there is no login. Pages on other origins cannot call the API, as Browser access to the Gateway describes.
How the Gateway site routes requests
The Gateway’s Caddy site sends each request to one of these places.
A path without a file returns the release’s
index.html, and the app’s router shows the page. Files under /assets/ carry content hashes, so browsers cache them as immutable. A missing file under /assets/ returns 404 with Cache-Control: no-cache, never index.html. Every other web response has Cache-Control: no-cache, so a new release shows on the next load, and open pages move to it.
/grafana/* checks the browser’s address with GET /api/v1/metrics/grafana/authorize, removes the /grafana prefix, and forwards the request to metrics.orbit. For PHP requests and that check, Caddy resolves the checkout link for each request (resolve_root_symlink), so a Gateway release switch reaches every request at once. The Metrics publication owns that site and its Grafana upstream. When Metrics is disabled, the app shows — for Node metrics.
The app connects to Reverb at the URL that GET /api/v1/realtime returns.
Node and Instance lists
The dashboard and the Nodes page list Nodes with roles, with their meters, apart from roleless Nodes, which only join the network. On the dashboard, these are the Nodes with roles pane and the roleless Nodes pane. Instance lists show whether each Instance’s placement is development or production, from its Node’sapp-dev or app-prod role. Concepts defines both terms.
Node detail pages
A Node detail page has the same section menu pattern as an Instance: Overview, Tools, and Firewall where supported. Overview holds identity, roles, metrics when supported, Instances, and Node Processes. Firewall has its own page section on Linux, with the existing operator rules, Orbit rules, live rules, and missing-state comparisons. macOS tool support does not enable firewall management. The menu chooses the URL. Overview is/nodes/<id>. Tools is /nodes/<id>/tools. Firewall is /nodes/<id>/firewall on Linux. On a narrow screen the menu is one row, so the list keeps the width of the page. A macOS Firewall URL returns to Overview and does not request Linux firewall rules.
Tools
Tools always shows every registered Tool for that Node, including failed tools and records on an unreachable machine. Managed rows show manager, package, recorded version, observed version, version constraint, status, and failures. On a narrow screen, the observed version, the constraint, and the failure wrap onto a second line so a phone still shows them, and Update and Remove sit on that line. The recorded version is the last operation’s result. A live scan supplies the observed version beside it: a newer install,absent when a completed scan did not see the package, not scanned when that manager is outside the inventory, and unavailable when no observation is loaded.
A separate detected-unmanaged group shows installed Homebrew formulae, casks, and Vite+ globals from GET /api/v1/tool-inventory (tool:scan). It labels package kind, observed version, dependency status, and the support reason. A null scan version, including a non-SemVer formula revision or cask version, is shown as unreadable. A formula and a cask that share a name stay on separate rows. Discoveries are informational and do not affect Node health.
The page shows inspection time and status and has an explicit Refresh action. Refresh sends only another inventory read. An active Node is read when the section opens. An unreachable Node does not start that read, and Refresh stays disabled so it cannot start one later. The inspection says no scan was started, and the registered rows stay.
A failed refresh keeps the last observation and marks it stale, or says the inspection is unavailable when there was none. Neither case is shown as an empty healthy inventory. A manager that is incomplete, absent, unsupported, or conflicting is named, and its empty package array is not an inventory.
Each supported unregistered package has an Adopt action. Unsupported packages, including dependencies and unsupported casks, show the block reason and have no enabled Adopt action. Adopt opens an ownership form that names the Node, manager, and package, and asks before it sends POST /api/v1/tools/adopt. The optional constraint is omitted when the field is empty. The Gateway rechecks the live installation before creating intent. A revalidation error stays on that form.
Success refreshes registered Tools and drops only that package from the unmanaged group. It does not install or update the package. Update sends POST /api/v1/tools/<id>/update with an empty object and shows whether the Tool changed, stayed current, or was blocked by its constraint. Remove asks before it sends DELETE /api/v1/tools/<id>. The question names the package, the manager, the Node, and that the package is uninstalled and the record deleted. Success reports status removed and drops the row, so the list never shows removed. A failed removal stays on that question and leaves the row. No scan, refresh, or page load adopts or updates a package.
Phone navigation and actions remain reachable without compressing the package list beside a full desktop sidebar. The web verification covers phone and desktop layouts, successful adoption, and offline or failed scan states.
Live Node and Process state
The app subscribes topresence-node.{id} for every active Node, next to the orbit channel. The Node agent publishes there.
A Node that the Gateway is updating shows
updating in blue instead, whatever the agent or Prometheus reports. Its updating field is set while the fleet rollout visits the Node, or while a Gateway release runs on the Gateway Node. Its status dot pulses slowly between full and half opacity over 2 seconds. When the system asks for reduced motion, the dot stays solid blue. The hover text and the accessible label name the cause: updating — fleet rollout or updating — Gateway release. A node.updated event sets and clears the value. Demo mode shows one Node in a fleet rollout.
A Node without an agent always uses the last row. A macOS tool-only Node has no agent or Metrics exporter in this slice; the page shows unavailable live telemetry rather than treating that absence as a failed Linux service. An example is a Node without roles that has no pinned SSH host key.
CPU and memory come from process.usage events. The app writes each sample into its cached Process list. While realtime is live, it reloads the Process list only when no sample arrived for 60 seconds.
Live tasks
The generated task cancellation description follows the cancel contract: cleanup removes eligible pending or failed exclusive Routes withsource_resolved workspaces and retries unattached leftovers. Active or shared Routes still block cleanup. This description changes no response fields and adds no web controls.
The generated task comment list operation accepts optional type and limit query inputs. These types describe the comment list contract; they add no web controls and do not change comment bodies. A question_closed comment is labeled Question closed.
The generated task schema keeps watched_pr_url, watched_pr_number, and watched_pr_state apart from pr_url. The watched fields describe the pull request found on the task branch while subtasks are open; pr_url still identifies the reviewed pull request Orbit opened. The branch watch owns that distinction. Regenerate the web schema after these response fields change, and keep typed test fixtures current. A task with no watched pull request has null watched fields; do not copy pr_url into them.
The generated subtask schema includes topology, a workload-role array, and the create and update inputs accept the same declaration. Response fixtures retain [] for subtasks without a declaration. Regenerate these types together with OpenAPI after a topology contract change. The compute driver validates declarations and gates dispatch on native readiness. These API types add no topology controls to the task board.
When the Gateway reports Tasks enabled, the app keeps the task board, each task, its agent threads, its comments, and the extension status current from task events. When disabled, it hides task navigation and task routes; enabling the extension makes those views available again without removing stored task records.
A card for a task that asks for direction says Needs your direction, and the task page shows that question first. A card for a failure says Needs attention. The board does not answer either request. The operator answers through the CLI, MCP, or the API. Direction requests define the two kinds.
The app waits 100 milliseconds after a task event and refetches each named query once. A refetch that an event starts replaces a request that is still running. An active task’s duration counts forward on the page. Token and line counts change with the next event, and the agent thread stream shows live tokens.
Task definitions
The Tasks page at/tasks lists task definitions with tasks:definition:list. Each Project page at /projects/{project} lists the definitions for that Project. The definition view is /projects/{project}/task-definitions/{name}. The lists and the view are shown only while the tasks extension is enabled. While it is off, they are absent, as the other Tasks routes are. A stale direct request still receives extension.disabled. The lists are read-only. Creating or changing a definition is an API call, as Task definitions describes. Opening a definition does not start a task.
The lists and the view load when the page opens. They have no realtime event and do not poll. A change another client makes with tasks:definition:update appears the next time the page opens.
Drawing
The definition view draws that task definition on a canvas from the livetasks:definition:show response.
The main path runs down the middle. A detour or a failure path sits in a side column, and each of those paths ends in its own complete or fail node. The drawing opens at full size, so the card text stays readable. On a narrow screen, pan to reach a side path.
Each subtask shows its kind, its models, and its routes with their outcome labels. A default route to an end stays hidden. Routes defines the defaults.
Each phase is one card. Opening the card shows a frame around that phase’s subtasks. The canvas label says to open a phase, and after a phase is open it says to collapse a phase.
The drawing also shows the stages the engine always runs around the definition. Before the first subtask it shows the workspace start. Inside each agent subtask it shows the implementer, the handoff check, and the reviewer. After the last subtask it shows the pull request, the merge, and the cleanup. A person merges unless the definition has a merge subtask.
A schedule is shown in words, such as “Weekly on Monday at 03:00 UTC”.
The view reports a finding when every option of a decide subtask leads to the same subtask. The Gateway refuses a definition that contains a subtask no path reaches, so the view does not report that finding for a stored definition.
The view reports a model that no driver can run when the ProxyCli model list is available. Task agents run on Pi only. A model is known when ProxyCli offers it through a provider Pi runs. Claude models and models listed under the claude or anthropic provider are unavailable for task agents. Annotations still use the operator’s T3 threads, as Agent annotation describes. When that list is missing, empty, or refused, the view says that the model list is unavailable and reports no driver findings.
Live Activity
The Activity page is/activity in the main navigation, after Tasks. It is one log, newest first. Opening a row goes to /activity/{id}, which always loads the row with activity:show.
The log loads 50 rows at a time and loads the next page when you scroll near the end. The next page sets before_id to the oldest loaded id. A page with fewer than 50 rows is the end. The page filters by status, command, caller Node, and target Node. The filters live in the URL, and before_id never does. Changing a filter loads the newest matching rows. On a narrow screen, one Filters button opens a sheet with the same four filters.
While realtime is live, the page writes Activity notices into the loaded log without a refetch:
activity.createdinserts a matching row at the top. A null caller or target does not match that filter.activity.updatedpatches a loaded row in place. A row that stops matching the filters stays until the next refresh.- A row added above the screen leaves the visible rows in place. A
N newcontrol then scrolls to the top. activity.updatedfor the open row refetches it withactivity:showafter 100 milliseconds.
Polling
Realtime events keep the fleet lists, the deployment history, and the annotation list current, so those views do not poll while the page is live. When the page goes live after a time without realtime, it reloads them once, because events sent in that time are lost. While realtime is down, those views poll with a backoff. The first poll comes 30 seconds after the loss. The delay then doubles up to 5 minutes. A reconnect resets it. The footer showslive updates paused next to the Gateway name. Clicking it reloads every view and restarts the backoff. A hidden tab never polls. A tab that becomes visible reloads the views whose data is older than 30 seconds.
These views have no event. A view with an interval polls while the tab is visible. A view loaded when opened does not poll.
The firewall list reads every Node’s rules with one request,
GET /api/v1/firewall-rules. It returns only the Nodes that the browser’s Node can reach.
Live logs
The Instance and Process log panes follow their log through a live log stream while realtime is live. A pane opens a stream with the lines it shows, 500 for an Instance and 100 for a Process. It renews the stream every 20 seconds and closes it when the pane closes. It shows[orbit] N lines dropped and [orbit] N MiB skipped where the stream reports them.
A pane polls the one-shot read every 10 seconds instead when the Gateway refuses the stream, when the stream ends, or when realtime is down. A production Instance pane always polls. For a reason that passes on its own, such as agent_not_joined, the pane tries a new stream every 30 seconds while it polls. Live logs lists the reasons.
Installed app on iPhone and iPad
Added to the home screen, the app opens full screen below an opaque black status bar. index.html setsapple-mobile-web-app-status-bar-style to black and the viewport to viewport-fit=cover. iOS reads these tags only when the icon is added, so add the app again after a release changes them.
The app shell is a position: fixed box with an opaque page background that fills the web view. iOS 26 blurs the top edge of an installed web app with its glass edge effect unless a fixed, opaque box covers that edge. The shell keeps the header sharp.
In standalone display mode, the shell pads the top, left, and right edges by their env(safe-area-inset-*) values. The top value is 0, because the web view starts below the status bar. Bottom padding depends on the window width. Pages never add their own safe-area padding.
On a phone, the footer is hidden because the header already shows the Gateway’s live status and the footer’s keyboard hints are not useful there. It appears only for a message or paused live updates, and then sits above the home indicator. From the
md breakpoint up, the footer is always shown.
The Menu drawer shows one support line with the display mode, the window and screen sizes, and the four insets. Web verification checks a phone-sized viewport.
Updates to open pages
Each build carries its id, the commit it was built from. CI andbin/web-deploy build one exact commit. A build outside a Git checkout uses a hash of its sources instead. The build writes the id to version.json at its top, as {"build": "<id>"}. The Gateway serves that file with Cache-Control: no-cache, like every web file outside /assets/.
A built page reads /version.json past the browser cache and compares it with its own id. It reads at most once a minute and never in the first minute after the page loaded. It reads at these times:
- A client-side navigation starts.
- A hidden page becomes visible again.
- The browser restores the page from its back-forward cache.
The page does not load a new build over a draft that the app protects or over a focused text field. A protected draft, such as an unsaved Project Document, has a router blocker that asks before the page unloads. While such a draft exists, or while a text field has focus, also inside the annotation overlay, the page waits. A later navigation loads the new build in full. When that navigation leaves the draft, its blocker asks first, and the full load follows once the draft is gone.
Typed text in a form without a blocker is not protected once its field loses focus, so a page that shows again can reload over it.
The page loads each newer build only once. Before the full load, it marks that build in
sessionStorage, or in memory when the browser refuses storage. The new build clears the mark when it starts, and a mark older than ten minutes counts as a load that never arrived. A page that still runs an older build keeps the mark and does not load that build again, so a Gateway that serves an old index.html cannot cause a reload loop; the page tries again at most every ten minutes. It still loads a later build.
When a script or style of the running build fails to load (vite:preloadError or a failed import()), the page reloads once for that build, unless it holds a protected draft or a focused text field.
Added to the home screen of an iPhone or iPad, the app stays in memory and navigates only client-side. These checks move it to a new release. The id is the commit, so every Gateway release moves open pages once, also a release that does not change the web app. vp dev and bun run demo do not check, because Vite reloads the modules there.
Web directory
The web directory is/home/orbit/web. ORBIT_GATEWAY_WEB can name another direct child of /home/orbit. The directory belongs to the orbit user and the caddy group.
orbit:bootstrap creates the directory and publishes the site. After a Gateway deploy that changes the site, run php artisan orbit:gateway-web in the Gateway checkout. It runs these steps:
- It installs Caddy from the pinned source.
- It grants Caddy access to the checkout’s
publicdirectory and creates the web directory. - It publishes the Gateway certificate with the public root certificate.
- It writes the Gateway PHP-FPM pool and runs
systemctl reload-or-restart php8.5-fpm, which can restart the workers that serve the API. - It publishes the site.
- It converges the runtime hibernator and the agent view subscriber.
current. A Gateway release installs its own web build in releases/ and switches current after the release verified.
Release a build
Each Gateway release ships the web app of its commit. CI buildsapps/web on every push to main and uploads it as the artifact web-dist-<sha>. While it prepares the release, the Gateway downloads that artifact through its GitHub App and installs it as releases/<commit>. It switches current after the release verified, and switches it back when the release switches back. The Gateway needs no Node or Bun for this. Web build describes the checks, and Deploy a release the order.
When the Gateway prunes a release, it removes that release’s web build too. After each verified release it also removes every build that belongs to no retained release, a bin/web-deploy build of another commit included. It never removes the build current names.
Both builds hold version.json at the top, next to index.html, so open pages find the release that current serves.
bin/web-deploy stays for manual use: a build of a commit CI did not publish, or a Gateway that does not release itself. Run it from a clean checkout of the commit to release.
apps/web/.env.local and VITE_* variables never reach a release. It installs the locked dependencies of apps/web, builds apps/web, uploads the build to releases/<commit>, and switches current in one rename. It keeps the three newest releases. It never removes the release current serves, or the build of a retained Gateway release, one whose releases/<id>/REVISION exists in the Gateway releases directory. So a manual run never removes a build that a Gateway rollback needs.
Roll back
Switchcurrent to a retained release without a build.
<release> is the first 12 characters of the commit, as in releases/. The command exits with status 2 for any other value and refuses a release that is not retained. The Gateway serves the older release on the next request.
Run against a topology
Use a disposable topology for web UI development that can change Processes, Schedules, databases, or firewall rules. Do not test those actions on the live fleet. An Orbit task group gets a topology only when a subtask declares one, and Orbit then prepares it nested in the group’s task VM. Incus topologies describes how to acquire one by hand. From the task workspace on beast, run:operator guest. It never acquires a topology implicitly. The operator is a small Incus system container on the topology network, registered as a roleless Node with a Gateway grant and its own WireGuard peer. It trusts the topology’s Orbit CA and mounts the worktree live at /home/orbit/orbit, so host edits are live in the dev server.
web runs vp dev in the operator’s apps/web, on guest TCP port 5173 with strict-port behavior. If that guest port is occupied, startup fails rather than selecting another port. The session pins the Gateway URL and trusted CA to the selected topology. Gateway, realtime, and metrics traffic stay in that topology; inherited endpoint overrides cannot redirect them to the live fleet. Missing topology or operator configuration fails clearly, with no fallback to a live profile, real Gateway, or another user’s credentials.
After readiness, the command prints the actual beast IPv4 loopback URL and host TCP port. The host port is an available OS-assigned ephemeral port, not necessarily 5173. Only 127.0.0.1 is published. There is no new DNS or Caddy Route, and topo-<issue>.orbit.test is not part of this contract.
Open it from a Mac
Keepweb running on beast. When it prints host port P, open another terminal on the Mac and run this command, replacing P with the printed numeric port:
http://127.0.0.1:5173 on the Mac. Keep both the web command and SSH running. If Mac-local port 5173 is occupied, choose another free local port in the first part of -L and open that port instead. The destination port remains the printed beast port.
web runs in the foreground and streams dev-server output. Ctrl-C, command termination, startup failure, and topology release stop the session’s dev-server process and remove its loopback publication. Stop SSH separately with Ctrl-C. Stopping the web session does not release the topology. Only one web session may run per topology; a second invocation fails clearly without disturbing the first. Incus topologies owns the full command contract.
Browser tests
Browser tests assert on the page, the URL, and the requests that the demo Gateway received. They never write tracked files, so a passingbun run test in apps/web leaves the working tree unchanged.
Why it works this way
These reasons explain the design. Check them before you propose a change.One live fleet view
The web app is the only live fleet view, and the CLI has none. A terminal screen is rejected: it would need its own copy of the metrics queries, and it would have to follow every change to shared requests and realtime events. Without a browser, use the list commands andrealtime:tail.
The Gateway origin
The Gateway identifies a caller by its WireGuard address. A proxy in front of the API would replace the browser’s address with its own. A separate hostname such asapp.orbit would make every API call cross-origin. On the Gateway origin, the browser already trusts the certificate, and the Gateway already knows the caller. The app adds no authority, because every request it makes is an API request from the browser’s own address.
Disposable UI work through an operator guest
The development proxy runs inside the selected topology because the Gateway identifies callers by WireGuard address. Beast is already in the real fleet, and topologies reuse its WireGuard address range. Joining a topology tunnel in beast’s network namespace would mix live and disposable traffic. A separate roleless operator container gives the proxy a topology identity without turning it into a workload host. Pinning both the URL and CA prevents an inherited live profile from sending destructive UI actions to the real fleet. Loopback publication plus SSH forwarding gives Mac access without a public listener or a new DNS/Caddy route. An ephemeral host port avoids collisions between groups; the strict guest port makes a conflicting server fail visibly. One foreground session per topology makes process and publication cleanup part of the same lifecycle.Releases outside the checkout
Files copied into the Gateway checkout’spublic directory would follow Gateway deploys, and a deploy that cleans the checkout would remove them. A separate web directory lets a web release and a Gateway deploy happen independently. A rollback only moves current.
A build from CI
Building on the Gateway would need Node and Bun on the control plane only for this step. CI already builds the web app for everymain commit whose run is not replaced by a newer push, and a Gateway release ships only such a commit, so it installs that build. bin/web-deploy builds on the operator’s machine for a commit CI did not publish.
A version file instead of a service worker
Inertia sends an asset version with each response and loads the page in full when it differs. This app gets its data from the API, not from page responses, so it reads the version from a static file of the release instead. The file moves withcurrent, so it always names the build that a full load would get.
A service worker could update the app too, but it adds an install lifecycle and a cache that can serve an old app shell, and the app is useless without the Gateway anyway. A timer would wake hidden tabs and phones. A read on navigation and on resume costs one small request at most once a minute, at the moments when a full load interrupts nothing.
The id is the commit and not a hash of the assets, so a page names the Gateway release it came from. The cost is one full load after a release that did not change the web app, and a new entry chunk for each commit.
A missing asset is an error
current serves one build. A page of any earlier release can ask for its own files, and current does not hold them. With the index.html fallback, such a request got HTML with the immutable header, so a browser could keep HTML as that script for a year. A 404 with no-cache fails the request instead and caches nothing.
Grafana through the Metrics site
The Metrics publication rewrites themetrics.orbit site when Metrics moves. A direct proxy to the Metrics Node would copy its address into the Gateway site, which that publication does not rewrite.
Notices patched into the Activity log
Each notice carries every list column. A refetch per notice would reload the pages that the operator reads, and a burst would still cost a request. The page keeps the visible rows in place, so the operator never loses their place. A reconnect merges one page by id instead of refetching every loaded page.An opaque status bar on iOS 26
With a translucent status bar andviewport-fit=cover, iOS 26 starts the web view under the status bar and makes it shorter by the top inset. Every CSS height and innerHeight then leaves a dead band at the bottom, and the edge blur covers the header. WebKit bug 301108 records the fault. The opaque black style starts the web view below the bar and lets it reach the bottom edge.
The opaque status bar alone does not stop the glass edge effect. iOS 26 still blurs about 38 points below the top of the web view unless WebKit can take a flat colour from a fixed, opaque box covering that edge. No CSS property or meta tag turns the effect off. Padding the header down would leave an empty band, so the shell itself is that fixed box.
Instance dependencies
An Instance has a Dependencies section with separate Composer and npm tables. Each table shows package names, installed versions from the saved scan, and runtime or development scope. The page readsGET /api/v1/instances/{instance}/dependencies and refreshes the saved inventory using the normal polling fallback. Opening this section does not start a scan or update packages.
The page shows when each snapshot was observed and distinguishes unscanned, absent, stale, failed, and empty inventories. A failed scan can still show the previous snapshot, labelled with the failure. Dependencies remains available when the Tasks extension is disabled.
The Instance overview shows the latest 25 deployments, in the order returned by the Gateway. The database Tables panel shows a query error when a table read fails, instead of reporting an empty database. MySQL inspection requires the MySQL PDO driver in the Gateway PHP runtime.