Inspect dashboards
Find app capabilities in the local and Cloud dashboards and interpret plugin inventory, metrics, standing observations, checks and agent tools.
The local dashboard and Cloud app inspector share the same task navigation and transport-independent pages. Choose an app first; Cloud keeps the selected environment when changing views.
Shared workspace design
Both dashboards use the same Signal masthead, Voltro wordmark and product badge,
centered content width, workspace headings, app identity header, controls and
light/dark theme tokens. The local dashboard opens with Overview at /; app selection lives at /apps.
It exposes Overview, Apps, Logs, Metrics and Settings in its context navigation.
Inside an app, a persistent sidebar lists every task by group; on smaller screens
the Navigation button opens the same list. Charts use the Signal violet spectrum, with red reserved for errors. The request
trend leads the overview; status shares and ranked endpoints supplement latency,
error-rate and cache plots. Missing latency samples and undefined rates appear as
gaps or “—”, never as measured zeroes. Empty plots wait for real stream events.
All 40 shared inspection areas have routes in both hosts. The local app overview adds the combined route/RPC/live-event summary. Cloud data branches depend on managed provisioning; organizations, membership, subscriptions and managed hosting remain Cloud functions. App-plugin billing inspection remains available locally when the billing plugin is installed. Plugin views stay listed even when the plugin is not installed; their availability is marked in the sidebar.
To build another workspace, import SignalWorkspace, SignalWorkspaceHeader,
SignalWorkspaceItem and SignalHeader from @voltro/ui-shadcn, alongside its tokens.css and
signal.css. Pass host-specific branding, navigation and utilities as slots;
the shared shell owns the background and control styling. AppInspectorHeader
from @voltro/devtools-ui supplies app identity above AppInspector. The host
still owns routes, credentials, transport and environment selection.
Select replicas
The API app header offers All replicas, any explicit subset, or a single replica. Apply the selection once; it remains in the URL across inspector views, table changes and record links. All replicas includes newly discovered processes. An explicit subset keeps its IDs even when a process becomes unavailable.
Logs combine records with their replica of origin. Traces retain the origin of each span. Additive metric counters can be combined; compatible histogram buckets are merged before calculating quantiles. Precomputed percentile windows remain separate per replica, and incompatible histograms show an unavailable quantile. Other process observations are compared in separate replica sections rather than added together. Missing answers remain visible in the observation completeness.
Shared-store records have one table and one editor. Reads use one selected responder, trying another selected responder if necessary; an explicit singleton never silently falls back to another process. Writes execute once, without replica fan-out. Shared SQL change notifications remain a single feed, while process event streams connect to the specifically selected process. The local overview shows interruptions explicitly, retains that process’s last events during reconnect, and clears them when the selected process changes.
Routes and data
Web routes form an expandable URL tree. Parent pages remain selectable alongside
nested and dynamic routes. Select a page to see its file, render mode, hydration,
cache settings and inherited boundaries. Expand or collapse the complete tree
with the toolbar. The local app overview reuses the same RouteExplorer and adds
its live route metrics to the selected page.
Data explorer remains visible for frontend apps. A frontend has no database
of its own: select an API data source to open the existing table editor. The local
dashboard marks API relationships declared by the frontend; Cloud offers the API
apps in the current project and preserves the selected environment. The shared
DataSourcePicker accepts host-provided links and never handles credentials.
The data editor has a table sidebar, filters, sorting, pagination and row actions. Click an editable scalar cell or focus it and press Enter to edit it; Enter saves, Escape cancels. Write access and table/column restrictions still apply. System tables are grouped separately and read only. On mobile, the table list scrolls inside a bounded area so the selected table stays reachable.
Find an app capability
The navigation groups app configuration, data and storage, automation, product services, and health and activity. All groups are expanded in the sidebar. On a narrow screen, open Navigation to choose a task. API and web views follow the inspected app kind; Data explorer, environment variables, metrics and logs remain available for both kinds.
The plugin inventory marks which views have an installed provider. Views remain listed so the task catalog is discoverable. An unavailable inventory is not evidence that a plugin is absent.
Plugins
The Plugins view lists configured instances, activation and install-hook status, canonical package identity and the actual inspect mount. Expand an instance to see its permissions, hooks, procedures, schema contributions, services, templates and declared dashboard extensions. It does not disclose configuration values. A declared extension is metadata; listing it does not execute its bundle.
Every plugin panel resolves its instance from this inventory. With several instances, choose one before any plugin read or write runs. The choice survives Cloud's Refresh action; changing instances clears old data and action state. Cloud verifies package identity, effective mount and declared endpoint method before forwarding. An absent plugin and an unavailable inventory are separate states.
Rate limits, under Health & activity, shows ordered rules, the RPC gate, the independent HTTP IP shield and actual process-local limiting decisions. Multiple limiter instances require an explicit selection. Store errors and unbound-store bypasses are separate from checked-and-allowed requests; unknown bucket counts are not shown as zero. See Rate limiting for scope and configuration details.
Standing observations
| View | What to inspect | Read endpoint |
|---|---|---|
| Data expectations | Holding, violated or unknown rules; measured value, threshold and last violation cause | /_voltro/inspect/expectations |
| Compute budgets | Per-tenant usage, warning/limit, observe/suspend behavior and cost attribution | /_voltro/inspect/budgets |
| Experiments | Per-variant exposure, metric, baseline, holdout and relative/absolute difference | /_voltro/inspect/experiments |
| Invariant checks | Pass, fail or unavailable, concrete findings and suggested next step | /_voltro/inspect/checks |
| Agent tools | Admitted tools, app-enforced approval and policy exclusions | /_voltro/inspect/agent/tools |
Development and production read the same standing registries. Their observations are process-scoped, including when the underlying data resides in a shared database. A capture time identifies the snapshot; it does not establish that every replica has observed the same writes.
Compute budgets measure framework cost units, not currency. Attribution is cumulative since boot or reset; a budget may have its own rolling window. Experiments count assigned rows, not unique people. Undefined lift is displayed as a dash. These results provide no significance test or confidence interval.
An unavailable check is not a pass. Source checks can be unavailable in a built production artifact; read the reason and run the check where its inputs exist. Agent tool inventory grants no execution permission. Calls require an app subject credential and follow the app's authorization and approval policy.
Metrics
The app Metrics view accepts the endpoint's two actual formats. API apps report a bounded invocation window (normally 60 seconds) with measured latency percentiles in milliseconds. Web apps report cumulative registry samples; invocation percentiles are estimated from histogram buckets. The page labels the time basis and lets you filter or sort by calls, errors and p95.
Below the invocation table, All metric series reads
/_voltro/inspect/metrics/registry for both API and web apps. Search metric names,
descriptions and labels, or filter by counter, gauge, histogram, summary or
frequency. Expand a series to inspect its description, labels, values, cumulative
buckets, quantiles or outcome counts. This includes recorded database,
partial-prerendering, queue, AI and custom series; absence means the process has
not registered that series, not that the feature has no activity elsewhere.
This is a process snapshot, not a historical chart or fleet sum. Counters reset
with the process. Histogram boundaries use +Inf for all observations; -Inf
and NaN readings remain explicit instead of becoming zero or null. Use
metrics export for retained history
and aggregation. Cloud project Metrics links to each connected app’s measurements.
The Cache view keeps its hit rate unmeasured until the first cache request. Zero hit/miss counters with “No cache requests yet” differ from a measured zero-percent hit rate after requests that all missed.
The app context uses a compact row: the return link, app name, address and status stay together above the inspector. Dashboard tabs sit directly below the masthead, keeping the working area close to the top of the viewport.
Working with records
Dashboard checkboxes use the shared Checkbox from @voltro/ui-shadcn, including filters, confirmations, access controls and Cloud settings. The shadcnWidgets.checkbox form renderer uses the same component, with consistent checked, mixed, disabled and keyboard-focus styling. Native input semantics and label interaction are preserved.
Updates are always automatic. Live · updates automatically is a connection status; Data source details only expands observation metadata. Changed cells briefly highlight in violet, rows newly entering the visible page in green, and CDC-confirmed deletions in red for 500 ms before disappearing. Page, filter, search and sort changes establish a fresh baseline without flashing. A row leaving a filtered or paginated result is not proof of deletion. After a stream gap or a resync without row identities, snapshots recover the current data without inventing red deletion signals. Interrupted connections reconnect automatically, with a visible reconnecting status and snapshot refreshes every 15 seconds during downtime. Reduced motion uses a static confirmation tint; active cell editors retain their draft and focus during updates. If the edited record leaves the result, editing ends with a notice and the controls unlock.
Drag the divider beside the table list to resize it independently. Hover a table name briefly to see its full name, or focus it with the keyboard. Column boundaries have visible resize handles; drag them or use the arrow keys. Double-click a handle to restore its default width.
The shared Data explorer in local Devtools and Cloud uses compact rows, a sticky column header and row actions on the left. Resize a column by dragging its edge or focusing the separator and pressing the arrow keys. Use Columns to find and hide fields; the final visible column cannot be hidden. On narrow screens, choose a table from the selector and scroll the grid horizontally.
Row search waits briefly while you type. Filter edits a local draft: complete
the conditions, then choose Apply filters. Resetting filters restores the
unfiltered result. Clicking a scalar cell opens an inline editor; Enter or the
save control commits, Escape cancels. Finish that edit before changing the view.
Empty text and explicit null are different values.
The row action opens a searchable editor with complete values and typed fields. On creation, Use database default leaves that column out of the request so the database assigns it. Required fields and malformed JSON prevent submission. Reference fields can search and select a record from the referenced table; the grid's reference link opens that table filtered to its primary key. Save errors retain the draft. Closing with changes asks whether to discard them, and deleting one or selected rows requires confirmation. Read-only capabilities continue to hide writes, and the server remains the authority for access.
Refresh requests a new snapshot through the host. Hosts may expose an
optional DataSource.refresh callback; successful edits also request it. Source
and capture details are available below the toolbar; incomplete observations
remain visible without opening the details.
Last recorded row change
Open a row in Data explorer to inspect its last recorded change. The panel
shows the audit actor and timestamp, plus a trace identifier when available.
Missing audit fields, an unknown actor and a row that no longer exists have
separate states. It reads /_voltro/inspect/provenance with the same field mask
as the row explorer. Cloud requires an owner or admin for row and provenance
reads; resolving a cached row still checks app ownership and current membership.
This is evidence from the row's latest audit fields, not a complete history or an attribution for each individual field. The Time travel view remains the place to inspect retained row-history snapshots when that plugin is installed.
Failed reads and custom hosts
A failed read must remain an error, distinct from a successful empty snapshot.
Custom @voltro/devtools-ui hosts supply DataSource<T> hooks and enforce
authorization in their transport. The shared CDC, flags and governance pages
require the dead-letter, audit and subject-graph hooks respectively. Preserve
all response fields rather than reducing them to the original summary rows.
For Mail, preserve process/declaration observation metadata and provide
Capabilities.canPreviewMail only when the operator may render a template with
the app's inspect write credential. A stored message is not evidence of a live
connection, and a pending or failed read must not be rendered as an empty outbox.
Inspect access and refresh
The local data explorer keeps one live connection for the selected app. Changing tables, filters or pages cancels obsolete row requests without reconnecting the stream or resetting the scroll position. Stream reconnects reconcile the current table; failed handshakes do not trigger extra reads. During an interruption, snapshots continue every 15 seconds. For loopback targets, both snapshot and stream proxies resolve the selected app's read token from the runtime registry; an explicitly supplied read token takes precedence.
The Inspect access view stores the inspected app's read token and a separate
write token. They must match VOLTRO_INSPECT_TOKEN and
VOLTRO_INSPECT_WRITE_TOKEN in that app. GET requests send only the read
credential; actions also send x-voltro-inspect-write. A valid read token
without the write token remains a read-only connection.
In Cloud, only an organization owner can change these credentials. The browser receives configuration status, never saved token values. Members cannot perform inspect actions; admins can operate services, while schema, row-deletion and bulk-run actions require an owner. The server enforces these checks. Leaving a field empty keeps its saved value; removing it requires the explicit checkbox. Removing the saved copy does not revoke the credential in the inspected app.
The local dashboard stores credentials in this browser's app registry. A
deployed dashboard holds both token and writeToken for its targets in
VOLTRO_DASHBOARD_APPS and attaches them itself; the browser learns only that
the pod holds them, behind the dashboard's own lock (VOLTRO_DASHBOARD_TOKEN,
the login page). Its proxy forwards the separate write factor and never
adds local write authority to an explicitly supplied read credential.
Cloud's external inspect queries are snapshots. Use Refresh to fetch current values. A subscription to the Cloud app row does not subscribe to the external app's in-memory counters. Workflow and schedule records with a dedicated change-feed bridge update through that bridge.
Expand Snapshot · Read only beside Refresh for the access explanation and the Inspect access entry. The local dashboard uses the same Read only disclosure when credentials are missing. The current process and capture time remain visible with each observation.
The RPC view includes queries, mutations, actions and streams, including their
JSON schemas and declared scope, resource-policy or open-access decisions.
Stream schemas describe each element; consume them with useAgentStream in the
app. Inspect credentials do not grant an application subject.
Local inspectors derive action availability from the registered app's read and write credentials. A read-only connection keeps inspection available but hides write actions. For deployed dashboards, runtime configuration reports each server-held factor separately; secrets remain on the server. Credential availability does not replace validation by the inspected app.
RPC inspection
The RPC inspector searches procedure names and source files within the selected kind. Procedures are grouped by namespace and can be opened with the keyboard. Select a procedure to inspect its input, output and error JSON schemas and access rules. Missing schemas are identified explicitly rather than shown as empty objects. Cloud and local inspectors use the same explorer.
Workflow detail, trace and back links within the dashboard retain the active replica selection. External trace-provider links remain unchanged.
Process-specific replica comparisons initially open two observations. Open additional replicas as needed; closing an observation unmounts its data providers. Shared data editors and combined logs/metrics retain their existing selection semantics.
For mutations and actions, the RPC inspector offers execution when the viewer has inspect write access. Enter JSON, choose a tenant or explicitly omit the tenant header, then confirm the operation. Execution targets the connected app endpoint, not a combined replica selection. Application guards still apply. Results and errors are displayed separately.
Workflow recovery uses the connected Retry, Resume and bulk recovery actions. Integrations supply useRetryRun, useResumeRun and useBulkRuns for these operator workflows.
Bulk replay counts an action as successful only when redrive reports redriven: true or retry returns a non-empty execution ID. A declined redrive retains its reason as a per-run failure while other eligible runs continue. These counts describe recovery actions, not successfully completed workflows; inspect the run status for execution outcomes.
Both bulk action surfaces distinguish accepted recovery actions from completed workflow executions. Per-run failures retain their full identity and reason, wrap long text, and remain accessible in pages of 20; the report does not discard errors after the first five.
Retry currently resubmits the original (or edited) payload using the workflow’s declared idempotency key. It does not force a fresh execution: the same key can resolve the existing failed run without executing its body again. Bulk retry reports this unchanged identity as a failed recovery action with an explanation, not as success. Use redrive for recovery from a failed journal; changing a payload only creates a different execution if it changes the declared identity. Durable admission of explicitly fresh retry attempts remains unfinished.
Bulk workflow actions apply only to the run IDs returned as eligible by the preview; newly matching runs are not included. Eligibility is checked again when applying, so runs that have since completed can be skipped. While a preview or action is pending, its filters and operation are locked and duplicate submissions are prevented. Changing any field discards the preview. After a failed request, review a fresh preview before applying again; the entered filters and reason remain available.
The same confirmation rules apply to checked workflow rows. Changing the selected run IDs resets their confirmation; a delayed response for an earlier selection cannot authorize the new one. Reordering the same IDs preserves the preview. Changing the reason invalidates it, and an empty eligible set cannot be applied.
Bulk cancellation returns targets (the eligible run/workflow/execution identities) and controls (structured control receipts). The dashboard saves per-run request identities before dispatch, forwards them as request controls, and restores their observation after reload or a lost response. An accepted cancellation is neither a confirmed success nor a failure; its receipt is tracked in the workflow control section. Only confirmed cancellation contributes to succeeded. A changed execution identity is refused before dispatch. Custom useBulkRuns providers must preserve both fields and the supplied request identities. Standalone BulkRunActionsPanel and SelectionBar integrations provide prepareControls and observeControl; WorkflowsPage connects its existing journal automatically. The journal retains at most 128 references; a batch exceeding available capacity is refused before any write is sent. Review completed receipts or choose fewer runs. A storage failure prevents dispatch and removes the newly written references from that preparation attempt.
Every run has its own page (…/workflows/runs/<runId>; the run rows link there): a header with status, source, version, parent, trace, absolute start and live duration; only the actions that apply to the run's state — Cancel and Signal / Update for a running run, Resume for a suspended one, Retry, Retry with… and step recovery for a terminal one; and a timeline in body order on a time axis — steps with their attempts stacked, durable sleeps, signal waits and child runs — each selectable (mouse, ↑/↓, j/k) into a side panel with input, output, error, stack trace and retry policy. The run's payload, output and lifecycle events sit below. There is no Suspend button: an api refuses every external suspend, so the only honest suspension is the workflow's own awaitSignal / sleep.
The definitions tab starts a workflow with a JSON payload ("Start run…", also offered when a workflow has no runs yet), through the same inspect start endpoint voltro workflows start uses; the start runs under the operator identity and through the workflow's own guards. Cancel, Resume and Retry show action errors on the affected run. While a request is pending, competing actions are disabled. A completed request is reported separately from the live workflow status; failures can be retried.
The definitions tab is a table: per workflow its declaration as chips — event triggers (*.trigger.tsx), schedules, flow control (concurrency 4 / key, debounce 5.00s, throttle, rateLimit, singleton, batch, timeouts), version with patch count, and access (the scopes a caller must hold, internal, or open with its reason) — beside a 24-hour sparkline of its runs (an hour turns red when one failed), the window's started and failed counts, and the runs in flight. Everything on that row is read off the definition the api registered, so the page cannot describe a workflow the api does not run; an older api that reports no declaration shows name and file only.
A workflow's own page adds a Declaration card — the payload fields with their types and optionality, the triggers with their cron, flow control, the signals and updates it accepts, version, compatibility and patches, access — and a Steps over the window table from the same stats endpoint: per step the runs it appeared in, retries, failures, p50 and p95, with a bar relative to the slowest step. The stat tiles show the window's p50, p95 and maximum duration from the server (latency on /_voltro/inspect/workflows/stats) rather than from the loaded page. "Start run…" opens on a payload built from the declared schema — required fields first, default and example honoured, placeholders by type — with the schema's one-line summary beneath the editor.
The runs tab opens with a Needs attention block: failed, stuck (running longer than three times the window's p95, never under five minutes), waiting (suspended or paused) and running, each a tile that applies the matching status filter. The filter bar has time presets (15 min, 1 h, 24 h, 7 d) beside the custom range, and every active filter is a chip with its own × under the bar.
In the local dashboard the run list, the stats, the run page's steps and events refetch when the api's inspect stream reports a change on workflowRuns / workflowSteps / workflowEvents (one refetch per burst); the four-second poll remains as the fallback for an api without the stream. The change stream follows the shared store rather than the selected process set, so selecting several replicas does not disable notifications. Every successful stream handshake, including reconnection, triggers a coalesced refresh to recover changes missed during the interruption.
The sparkline is drawn from the server's per-workflow bucket series at the time preset's resolution — five-second buckets over the last fifteen minutes, twenty seconds over an hour, five minutes over a day, an hour over a week — as an area line with a red marker per bucket that had a failure and a per-bucket tooltip (time to the second and counts) on hover. The throughput chart uses the same window and buckets.
The in-page console's Workflows scope is live the same way: it opens the api's inspect stream through the dev proxy (workflowRuns, workflowSteps, workflowEvents) and re-reads the moment a row changes; its poll is a ten-second fallback. The cloud dashboard mirrors runs, steps and events into its own tables from each app's stream (server-side), so those views are push-updated there too, and its stats and flow-control reads refresh when the mirrored runs change.
A workflow's page carries its admission at the top: admitting or paused, how many starts are waiting, holding a slot or dead-lettered, the controls it declares, the queued intents with their "19 starts folded into this one" — and Pause (with a reason) / Resume right there, the paused state visible. A Versions over the window table follows when the api answers byVersion: runs, success rate, failures, p50 and p95 per recorded workflow version, newest first — the after-deploy comparison. The Needs attention block gains a Dead-letter tile from the admission read; it opens the Flow tab. The placement input on the Flow tab sits behind an "Operator" toggle.
The events tab has Send event… and every logged event a Replay (the same modal, prefilled with that event's name and payload): the name is limited to what the api knows — declared triggers and logged names — and the receipt lists every workflow the event started. A delivery whose run is loaded links to the run's page. Ticking run rows shows a selection bar: Cancel (with a reason) or Replay over exactly those rows, previewed as a dry run first (eligible vs. skipped, with the reason), then committed by run id. "Start run…" takes an optional tenant; the run then executes as a service account in that tenant with operator authority. Saved views are chips in the filter bar.
The run page shows the run's admission row when the ledger has one ("19 starts collapsed into this run (debounce) · waited 4.2s"), a nondeterminism warning when the recorder flagged a replay that reached a step other than the recorded one (the step name selects the timeline entry), and for a retried step an attempt diff in the side panel — input and output of this attempt against the previous one, line by line. Operator actions keep their receipts under the header (request id, time, result), filtered to this execution.
Cloud account settings show your sign-in email and let you edit or clear your display name (up to 120 characters). Saving updates only the signed-in user; it does not change the email or login method. Failed saves retain your input for retry. Organization membership, API keys and billing are linked from the corresponding organization.
Procedure details follow the visible tab and search results. Switching away closes the execution form. A catalog read failure hides prior procedures and their actions. Query/stream details explain that these run as application-client subscriptions; missing write access or an unwired execution connection is stated explicitly.
Cloud profile saving treats the server-confirmed name as saved immediately, even before the profile subscription catches up. Whitespace-only differences do not enable another identical save.
When Cloud paid billing is not enabled, the organization billing page displays its launch status without mounting invoice, subscription, portal or seat-purchase providers. Organization member management remains available separately.
The table catalogue accepts every declared database column type, including real, decimal, bigint and arrays. Real and decimal fields use numeric editors and numeric range filters; arrays use JSON values. Binary, raw and bigint values are currently read-only in the grid until their lossless editing codecs are available.
When a selected replica has no reachable address, the inspector names that replica in the failure message. Cloud preserves the structured upstream diagnosis; an explicit single-replica read does not fall back to a different process.
Cloud environment selection reads the URL query independently of the pathname. Project and app navigation retain the selected environment. Removing variables, members, invitations or API tokens requires an in-app confirmation; failed actions remain open for retry. Local registry clearing uses the same confirmation component.
Cloud environment secrets are written server-side to the existing Secret Manager provider. The control-plane database stores only a server-only secretRef; list and mutation responses contain neither secret values nor vault references. Configure SCW_SECRET_KEY and SCW_PROJECT_ID on the API and worker. Both use SCW_SECRET_REGION (default fr-par) independently of the workload region. Missing configuration or provider failure rejects the write and keeps the form value for retry. Each write creates a separate vault reference: the database update activates the new value, so a failed database write cannot rotate the previously stored secret. References retained by existing deployments must not be deleted during rotation.
Container deployment resolves variables in this order: app/current environment, app/default environment, project/current environment, project/default environment. An unset project default means production. Plain values enter ServiceSpec.env; secrets enter secretEnvRefs and the RuntimeProvider resolves them. Ownership is checked and platform-managed keys cannot be overridden. A secret without a vault reference fails deployment explicitly.
Resending an organization invitation disables concurrent invitation actions until the request completes. Success and failure feedback identifies the recipient; a failed request can be retried.
When billing is enabled, changing the purchased seat count first requires a successful provider price preview. Review the quoted quantity and proration in the inline confirmation before applying the change. The quantity is locked during review; cancel to edit it. A failed preview never implies zero additional cost and does not change the subscription. An unavailable seat summary displays an unknown count rather than unlimited seats.
The data explorer preserves decimal values as strings when editing and filtering, including trailing decimal places. Decimal filter operands must be exact strings rather than JavaScript numbers. Memory-store queries, ordering and filtered updates/deletes compare declared decimal columns exactly, including equal values with different scales. Text columns retain text comparison. Reactive matchers use the same typed comparison for old and new rows, including values entering or leaving a decimal filter. Decimal conditions are evaluated exactly after candidate routing on other eligible constraints.
Integer edits require whole decimal digits within JavaScript’s safe integer range; fractional or rounded drafts are rejected. Vector edits require a JSON array of finite numbers. Invalid drafts show a field error before a write is attempted.
The inspect catalogue includes vectorDim for declared vector columns. Both dashboards validate this length in cell and record editors and show the required number of elements.
Invalid decimal drafts stay in the editor for correction without changing the record value. While an inline save is pending, Escape does not dismiss the editor.
Record submission revalidates field values using the same parser as cell editing. Values produced by controls such as the NULL toggle must also satisfy the declared type and vector length.
Graceful shutdown in development and production stops replica observation publishing, waits for an in-flight publication and removes that process’s observation records. Crashed processes still require membership/freshness diagnostics; a missing membership source cannot prove that a silent process has departed.
The storage plugin exposes POST /_voltro/inspect/plugins/storage/delete with { "refId": "…" } for inspect operators with write access. It delegates to the storage service, removing the reference and its grants and releasing bytes when no other reference uses them. Local and Cloud dashboards offer deletion after selecting a file, with an explicit confirmation naming the target. The dedicated canDeleteStorage capability controls availability; server write authorization still applies. Failed deletion stays open for retry and successful deletion refreshes supported data sources. Upload is not yet available in this interface.
Storage statistics aggregate the entire reference index by tenant and visibility, including inventories beyond 10,000 objects. DataStore-backed statistics use a grouped aggregate query rather than loading every object. Counts describe references, not unique deduplicated provider blobs.
Plugin inspect endpoints can return kind: "bytes" with contentType and byteStream, using the same lazy Web ReadableStream contract as plugin HTTP routes. Inspect authentication runs before the handler. Binary responses are not wrapped in JSON observation envelopes, even for JSON-file downloads. The API transport opens lazy sources only for the body, propagates download metadata and cancels the stream on disconnect. This transport capability does not by itself add a Storage download button or a Cloud binary proxy.
Storage exposes GET /_voltro/inspect/plugins/storage/download?refId=… to authorized inspect readers. It streams the referenced file as an attachment with a UTF-8 filename and no-store caching; it does not mint a public URL. Trusted server code can use StorageService.getStream(id, { tenantId }), which checks tenant scope like get and opens the provider lazily when consumed. Private end-user access still requires authorization before calling this method. Native provider streams are preserved by both inspect downloads and the ordinary serve route. Providers without getStream use a buffered fallback.
The local dashboard HTTP proxy streams inspect responses in both development and production start. Download metadata is preserved and disconnecting the client cancels the upstream request. When fetch decodes a compressed response, the proxy omits its compressed Content-Length. Target restrictions and per-app credentials are applied before forwarding; upstream cookies are not forwarded.
The Cloud API provides a binary storage-download REST handler at /apps/:appId/storage/:inspectSlug/files/:refId/download. It requires authentication and cloud:console scope, reuses app tenant/membership/role checks, and verifies the storage mount against the app manifest. File streams and attachment metadata are forwarded without exposing inspect credentials or upstream cookies.
The Cloud download route is explicitly registered in the API configuration. Missing apps or storage mounts return HTTP 404; denied tenant or membership access returns 403. Requests without authentication are rejected with 401.
Local browser download handoff
The dashboard exposes POST /api/dashboard/proxy/download with JSON { "target": "<storage inspect download URL>" }. The caller may supply its inspect bearer in the Authorization header; otherwise the existing server-held per-app credential rules apply. The response contains a same-origin URL valid for 30 seconds and one GET, bound to the current dashboard cookie. No inspect credentials appear in that URL, and no write factor is forwarded. Only allowed storage-download targets are accepted, with the target checked again on consumption. The dashboard session gate applies to both requests in dev and start. Pending handoffs are bounded; cross-origin browser issuance is refused.
Select a file and choose Download file in either dashboard. Preparation errors remain visible for retry, and competing actions are disabled during preparation. The status confirms browser handoff, not completed file transfer. Local preparation probes access before issuing the one-use URL; Cloud performs a HEAD check through its same-origin /storage-files proxy. The Cloud proxy targets VOLTRO_PUBLIC_CLOUD_API_ORIGIN, the configured external API WebSocket origin converted to HTTP, or the default local API at port 4100. Set the API origin when using another local API port. The browser streams the attachment without first buffering the entire file in JavaScript.
Storage inspect upload tickets
POST /_voltro/inspect/plugins/storage/upload-ticket requires inspect write access. Provide tenantId and ownerId explicitly (each a non-empty string or null for intentional unattributed scope), a key, contentType, positive integer size, and optional visibility (private by default). The response contains uploadPath, expiresInSec (300) and maxBytes. POST raw file bytes to that signed path on the same inspected app. The ticket cannot accept more bytes than declared; configured upload size limits are checked before minting. The ordinary storage upload route still enforces scanning, quota and normalization through StorageService.put. Tickets are temporary bearer capabilities and must not be logged or shared. This endpoint is an operator capability, not an anonymous ticket-minting route. The dashboard upload form and its host transports are not yet connected.
The Cloud API exposes apps.inspectStorageUploadTicket with the same file metadata plus appId and inspectSlug. It verifies app membership, write role and the selected storage mount before forwarding the request with the app’s inspect credentials. The resulting ticket alone does not provide a Cloud binary-upload transport.
Upload, finalize, resumable and multipart tickets bind the exact storage plugin instance. Handlers reject tickets issued by another instance before accessing its provider. Code that uses the signing helpers must supply instance, the value produced by pluginInstanceName for that storage configuration; missing-instance tickets are rejected. This binding does not itself disambiguate multiple instances’ HTTP route mounts.
Named or aliased storage instances use a distinct /_voltro/storage/i/<hex-instance-name> HTTP mount for serving, uploads and resumable uploads. Always consume returned URLs rather than constructing them. The unconfigured default instance uses /_voltro/storage. The plugin supplies its mount through StorageServiceOptions.httpPath so returned file URLs target the same instance.
Local dashboard write actions display the inspect endpoint’s reason and suggested fix when access is denied. If the dashboard session itself has expired, they return to the dashboard login gate.
Date fields accept valid calendar dates in YYYY-MM-DD form; impossible dates are rejected rather than rolled into another month. Timestamp editors preserve milliseconds and display local time while storing an ISO timestamp. Saving an unchanged inline draft performs no write, including during a repeated daylight-saving hour.
An open record editor keeps the snapshot from when it was opened. Live changes do not mark untouched fields as edits or send them back as stale patches. A row leaving the current page does not discard the open draft. Updates include the opening values of exactly the edited columns as expected. The Inspect endpoint compares these values and writes inside one transaction; a changed or deleted row returns HTTP 409 and the editor retains the draft. Close and reopen the record to review the current values before retrying. Single and bulk deletion use one POST /_voltro/inspect/data/rows/delete request with table and rows: [{ id, expected }]. Each expected contains the full row snapshot captured when selecting the row or opening its editor. The whole selection (at most 500 distinct rows) is checked and deleted in one transaction. A changed or missing row refuses the entire operation; no partial deletion is committed. The selection/dialog remains available after an error. Clear and reselect after reviewing current values to make a new deletion attempt. Conditional edits and deletions use the store’s transaction-scoped row-lock query, so a namespace-bound store locks and changes the same physical table.
Environment inspection read states
The environment panel distinguishes loading, failed reads, and a successfully loaded empty declaration. If a read fails, previous variable states and summary counts are hidden until a successful response arrives. It does not present stale configuration as current.
The environment inspect response includes declaration metadata and boolean states only. Declared defaults and examples are omitted, including for public variables; default satisfaction is still reflected in the status. The separate .env.example generator retains its existing declaration projection.
On narrow screens, environment names and descriptions wrap, with status badges on a separate line. Required, missing and default-satisfied states remain readable in both light and dark themes.
Shared inspection sections treat every error value other than undefined as a failed read, including null, false, 0, and an empty string. They hide section content during the failure and restore it after a successful read; these error values must not expose stale plugin details as current.
Plugin inventory reports whether activation completed during process startup. “Activated at startup” is not a health check and does not promise that external services remain available. Shutdown hooks do not reset this historical activation flag.
The plugin inventory count is shown only when a successful snapshot is available. Initial loading and failed reads omit the count; a successfully loaded empty inventory explicitly shows zero.
The plugin hook inventory includes change-event handlers, observability contributions, and REST/procedure discovery callbacks alongside interceptors and lifecycle hooks. Listing a hook never invokes it.
The agent tool view removes the previous capture time and source notice while loading or after a failed read, together with the tool details. Successful reads restore the snapshot metadata.
When external agent access is disabled, the tool panel shows the reason instead of an unusable search field or empty admitted/excluded lists. Once a new snapshot reports the surface enabled, the inventory returns.
Agent-tool search matches tool names, descriptions, procedure types and exclusion reasons.
Checks and the related inspection pages hide previous snapshot timestamps and provenance while loading or when reads fail. Source metadata returns only with an available successful snapshot.
Schema convergence is unavailable when the migration snapshot has no SQL inspection connection and therefore no live schema fingerprint. An empty placeholder snapshot does not count as a successfully compared database.
Migration inspection and schema-convergence checks use the active database dialect in both development and serve mode. Storage-equivalent representations, such as SQLite TEXT for timestamp and JSON fields, do not produce pending type changes solely because their physical SQL type differs from the declaration.
The migration view compares boot baselines using the same table ownership rules as the boot planner. Undeclared framework runtime tables can appear after migration without producing false drift. Explicitly declared framework tables and unexpected application tables remain part of the comparison. An unchanged full-catalog baseline from an explicit apply is also accepted.
The dashboard and voltro db drift share the same baseline comparison. When declared tables can be loaded, the CLI recognizes an unchanged boot-scoped baseline even if unrelated runtime tables have appeared. Genuine application schema drift still returns exit code 4; the read-only comparison does not accept or rewrite the baseline.
Migration history inspection uses database-specific pagination, including SQL Server OFFSET/FETCH. A successful history read allows the migrations panel and schema-convergence check to report the actual schema state instead of an unavailable result caused by incompatible SQL syntax.
Data expectations display null rates and their thresholds as percentages, preserving small nonzero rates. Freshness measurements and thresholds both include milliseconds. Unknown measurements remain a dash rather than a zero.
Expectation observations include the complete rule used by the evaluator. The dashboard shows both row-count bounds, identifies the inspected column, and separates a value-bounds rule’s allowed field range from its tolerated violation count or percentage. The scalar threshold still describes the reported metric; rule carries the complete condition.
When a store change does not provide the previous or new row needed to update an expectation, the evaluator reads the current matching rows again. This keeps row counts, scoped rules and value-bound violations correct for SQL changes with incomplete row images; the triggering write remains attached to a violation. Changes with complete row images continue to update the measurement incrementally.
Compute-budget usage, limits, warning thresholds and attributed amounts retain small nonzero values instead of rounding them to zero. Number formatting follows the dashboard language; the configured cost unit is preserved rather than assumed to be a currency.
Experiment conversion rates are displayed as percentages. Their absolute difference from the baseline uses percentage points (pp), while relative lift remains a percentage. Count, sum and average measurements retain their numeric scale. Small nonzero measurements and negative differences are preserved; an unavailable comparison remains a dash.
Running experiments reread their current population when a store change lacks a required row image. SQL updates and variant changes therefore replace the old contribution instead of counting both versions. Complete row images continue to update the experiment incrementally.
For an explicitly selected replica, a temporary metadata read failure does not remount the local inspector or reset its active filter. Failed observation reads still hide stale results; the selected view resumes automatically when access is restored.
In the all-replicas view, an unavailable fleet inventory keeps the last known replica panels mounted so filters and open-panel state survive. A notice distinguishes retained membership from fresh discovery; each panel still reports its own current read or error. A successful inventory refresh replaces the membership, including an empty list. Switching apps clears the retained membership.
Invocation metrics display — for latency percentiles when the cumulative registry has no usable duration observations (a missing or empty histogram, or no finite bucket boundary). Request counts remain visible. Sorting by p95 places measured latencies before unknown values; unknown latency is not a measured zero.
The cumulative invocation overview also includes plugin hooks. Each recorded hook duration contributes one invocation; the dedicated hook-error counter supplies failures. Duration histograms with the same hook label are combined before estimating percentiles, matching the RPC, HTTP and subscription presentation.
The route inventory distinguishes loading, failed reads and a confirmed empty result. During a pending or failed read, a retained route snapshot is not shown as current. The hierarchy returns when the read succeeds.
Route details show inherited layout, error, loading and not-found boundaries from the root directory down to the selected page. Filesystem route groups remain in this chain even when they do not contribute a URL segment; unrelated sibling directories are excluded.
The local app inspector requests plugin inventory only after the inspected app identifies itself as an API app. Web apps use their route inventory without repeatedly requesting the API-only plugin endpoint.
Inspect credential changes allow only one save at a time. While saving, token fields and removal controls are locked. A successful save clears the input fields; a failed save retains the edits so you can correct or retry them. “Configured” describes stored credentials, not a successful connection check. Configuration badges continue to follow fresh access observations after a save; changes made elsewhere do not discard unfinished token edits.
Analytics provider capabilities and recent activity are shown only for a successful current read. Loading and read errors have separate states; an unavailable snapshot is not presented as an empty or unconfigured sink. Once the read succeeds, the current provider and activity return.
The API analytics inspect endpoint is served by the common authenticated router in development and serve deployments. It reports the configured sink (including the default noop sink), its capabilities and recent analytics operations from that process. Missing analytics wiring produces an explicit unavailable response instead of an absent route.
Recent analytics activity counts completed executions of the configured sink’s track, aggregate, timeseries and topN operations, with their failures and durations. Other plugin hooks are excluded. Counts describe calls, not event delivery: the noop sink accepts track calls without storing events. Only fixed operation labels are recorded, never event names, subjects, tenant IDs or payloads.
Aggregate refresh indicators are hidden while a read is pending or has failed. Retained snapshots never keep the Live indicator visible during a read failure; the original error message remains available, including errors returned as text.
Aggregate inspection and manual refresh use the shared authenticated inspect router in development and serve deployments. Reading requires inspect read access; refreshing requires write access. An unavailable aggregate registry reports an explicit unavailable response rather than a missing endpoint.