Skip to content

Add a Scripts tab for listing, running and writing diagnostic scripts - #90

Merged
bburda merged 9 commits into
mainfrom
feature/scripts-tab
Aug 22, 2026
Merged

Add a Scripts tab for listing, running and writing diagnostic scripts#90
bburda merged 9 commits into
mainfrom
feature/scripts-tab

Conversation

@bburda

@bburda bburda commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Pull Request

Summary

Adds a Scripts tab to the entity view: list the diagnostic scripts available on an entity, run one with parameters, follow its status, stop or force-kill it, upload a script file or write one in the browser, and delete scripts that are no longer needed.

The tab appears only when the gateway reports capabilities.scripts in GET /, and only on apps and components, which are the entity types the gateway registers script routes for.

Three commits, each of which builds on its own: the domain layer with the gateway helpers and the store wiring, the user interface, and one unrelated fix explained below.

Notable behaviour, because the gateway shapes it:

  • The gateway has no endpoint listing executions, so the UI remembers the ids it starts and one interval in the store polls them. Execution history is in-memory: reloading the page during a long run loses the ability to stop it. Trimming that history never drops an execution that is still active, since losing its id would orphan the process for good.
  • Parameters reach an uploaded script as JSON on stdin, never as arguments. Only manifest entries declaring args get command-line arguments. The starter template in the editor shows this, because it is the least guessable part of writing a first script.
  • A script's output appears when it finishes, not while it runs, and the gateway discards stdout entirely when a script exits non-zero, leaving stderr and the exit code.
  • A stopped execution is not a failure. The gateway fills in an error message on a successful stop, on a force-kill and on a timeout, so the card renders those as stopped rather than as an error.
  • The uploaded file name selects the interpreter: .py runs under python3, .bash under bash, anything else under sh. That is why the file name is a required field in the editor and why the hint sits under it.

Limitations worth knowing before review:

  • src/lib/api-dispatch.ts carries two documented as unknown as casts. The gateway's OpenAPI spec declares the start-execution body as a bare type: object and the multipart upload body as a free-form object, so the generated client types them as Record<string, never> and an index signature. The runtime payloads are correct; fixing this properly means correcting the spec in the gateway.
  • Stopping a script sets the status to terminated as soon as the signal is sent. A script that traps SIGTERM keeps running, and the gateway rejects further control actions on an execution it already considers finished, so the interface cannot offer a way out. This is a gateway-side limitation.
  • Scripts written or uploaded from the browser cannot carry a parameters schema, so they always get the raw JSON editor rather than a generated form. The wire format supports it; the interface does not expose it yet.
  • Marking an execution as no longer tracked is narrowed to the gateway saying the execution is gone, not the entity. An entity that disappears permanently therefore keeps its execution polled once a second and shown as running. It is bounded to background requests, pauses while the tab is hidden, and clears on reload; closing it needs an eviction policy that belongs in its own change.
  • npm run typecheck checks nothing in this repository: the root config has "files": [] and tsc --noEmit ignores project references. npm run build runs tsc -b and is the real gate. This is now stated in CONTRIBUTING (in the follow-up).
  • Playwright ships as a development dependency here, ahead of the follow-up that adds the end-to-end harness.

src/App.tsx carries a fix unrelated to scripts, kept in its own commit: the stored server URL was passed to connect() from inside a setTimeout, and Strict Mode's mount-cleanup-remount cycle cleared that timer before it fired, so a persisted URL never reconnected in development. It surfaced while building the end-to-end harness, which depends on that reconnect.


Issue


Type

  • Bug fix
  • New feature
  • Breaking change
  • Documentation only

Testing

576 unit tests, 28 files. The domain layer, the polling cycle, the dispatch helpers and all five components are covered, including the cases that are easy to get wrong: a progress bar at zero, a stopped execution rendered as stopped rather than failed, output that is only stdout versus output that is structured, the raw-JSON fallback for a schema that cannot become a form, and a parameter form that survives the once-a-second re-render while a script runs.

End-to-end coverage against a real gateway comes in the follow-up pull request, which builds on this branch. It exercises this feature: running a script and reading what it received on stdin, a failing script's exit code and stderr, stopping a long one, and writing a bash script and a python script in the browser and running them.

Manually: point the app at a gateway with scripts.scripts_dir configured. Without it the gateway reports no capability and the tab stays hidden, which is the intended behaviour.


Checklist

  • Breaking changes are clearly described (and announced in docs / changelog if needed)
  • Linting passes (npm run lint)
  • Build succeeds (npm run build)
  • Docs were updated if behavior or public API changed

Copilot AI review requested due to automatic review settings July 28, 2026 15:12

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds end-to-end “diagnostic scripts” support to the SOVD entity UI by wiring new scripts endpoints into the domain layer/store, adding polling for script executions, and introducing a new Scripts tab (gated by capabilities.scripts) on apps/components with UI for listing, uploading/writing, running, and controlling executions. Also includes a small App.tsx reconnect fix and test-environment hardening around localStorage.

Changes:

  • Introduces scripts domain/types, API dispatch helpers, and Zustand store actions/state for script listing, upload, execution start/control, and execution polling/history.
  • Adds new Scripts UI (tab + panels/cards/dialog/editor) with capability-based gating and comprehensive unit tests.
  • Adds Playwright test scripts/deps (ahead of an e2e follow-up) and fixes dev auto-reconnect in React Strict Mode.

Reviewed changes

Copilot reviewed 31 out of 32 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/test/setup.ts Stabilizes test localStorage/sessionStorage when Node’s experimental globals are broken.
src/lib/types.ts Adds script-related type re-exports and UI/store-facing script types.
src/lib/store.ts Adds scripts capability flag, execution history, store actions, and polling interval management.
src/lib/store-scripts.test.ts Tests refreshScriptExecution store wiring around the lost flag behavior.
src/lib/scripts.ts Adds scripts domain helpers (error typing, status helpers, output/failure parsing, history reducers).
src/lib/scripts.test.ts Unit tests for scripts domain helpers and reducers.
src/lib/scripts-polling.ts Implements a testable polling “single tick” for active script executions.
src/lib/scripts-polling.test.ts Unit tests for polling behavior and error-code handling.
src/lib/script-language.ts Adds filename→language mapping and editor templates for write mode.
src/lib/script-language.test.ts Unit tests for language detection, extension validation, and templates.
src/lib/schema-utils.test.ts Adds coverage for JSON Schema → TopicSchema conversion behaviors used by script parameter forms.
src/lib/api-dispatch.ts Adds scripts endpoint wrappers (list/get/upload/delete/start/control/delete execution).
src/lib/api-dispatch.test.ts Tests scripts endpoint dispatch path/params/body wiring.
src/components/ScriptUploadDialog.tsx Adds dialog to upload or write a script in-browser, with validation and inline errors.
src/components/ScriptUploadDialog.test.tsx Tests dialog behavior for both upload and write modes (validation, submit, errors).
src/components/ScriptsPanel.tsx Adds scripts list panel, error/empty states, and upload/reload orchestration.
src/components/ScriptsPanel.test.tsx Tests scripts list loading, aborting stale requests, reload triggers, and execution filtering per row.
src/components/ScriptRow.tsx Adds per-script expandable row with params form/JSON fallback, run/delete controls, and execution cards.
src/components/ScriptRow.test.tsx Tests ScriptRow form/JSON behavior, run/delete wiring, and rerender stability during polling.
src/components/ScriptExecutionCard.tsx Adds execution status/progress/output rendering plus stop/force/remove/refresh controls.
src/components/ScriptExecutionCard.test.tsx Tests execution card rendering and control actions (including “lost” handling).
src/components/ScriptEditor.tsx Adds CodeMirror-based editor with language switching and dark-mode awareness.
src/components/ResourceTabs.tsx Adds scripts as a tab id and a SCRIPTS_TAB config for app/component panels to append conditionally.
src/components/ResourceTabs.test.tsx Tests scripts tab ID handling and scripts tab content rendering rules.
src/components/EntityResourceTabs.tsx Extends loaded-tab tracking to include scripts.
src/components/EntityDetailPanel.tsx Conditionally appends Scripts tab based on scriptsSupported, and resets active tab if capability disappears.
src/components/EntityDetailPanel.test.tsx Tests Scripts tab gating and content routing in component view.
src/components/AppsPanel.tsx Conditionally appends Scripts tab based on scriptsSupported, with fallback if capability disappears.
src/components/AppsPanel.test.tsx Tests Scripts tab gating and rendering for apps.
src/App.tsx Fixes auto-connect behavior under React Strict Mode by removing setTimeout deferral.
package.json Adds CodeMirror and Playwright dependencies and e2e test scripts.
package-lock.json Locks newly added dependencies.

Comment thread src/lib/script-language.ts
@bburda bburda self-assigned this Jul 28, 2026
@bburda
bburda requested a review from Copilot July 28, 2026 19:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 31 out of 32 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

src/lib/scripts.ts:136

  • This doc comment says "Trim to MAX_EXECUTION_HISTORY", but the implementation can return more than MAX_EXECUTION_HISTORY entries when many executions are still active (since active ones are never dropped). It would help future maintainers if the comment matched that behavior explicitly.
/**
 * Trim to MAX_EXECUTION_HISTORY, dropping the oldest *inactive* records first.
 * Dropping a running execution would orphan the process: the gateway has no
 * endpoint to list executions, so its id could never be recovered.
 */

src/lib/scripts.ts:128

  • The comment says this is an "Upper bound on tracked executions", but trimHistory intentionally never drops active executions. If > MAX_EXECUTION_HISTORY executions are active concurrently, history can exceed this value, so the comment is misleading (and could hide a potential memory-growth scenario).

This issue also appears on line 132 of the same file.

/** Upper bound on tracked executions per entity; a record can hold a full stdout dump. */
export const MAX_EXECUTION_HISTORY = 20;

Comment thread src/lib/script-language.ts
Comment thread src/components/ScriptUploadDialog.tsx Outdated
Comment thread src/lib/store.ts Outdated
Comment thread src/lib/store.ts Outdated
Comment thread src/components/ScriptsPanel.tsx Outdated
Comment thread src/components/ScriptRow.tsx Outdated
Comment thread src/lib/store.ts
Comment thread src/components/ScriptRow.tsx
Comment thread src/components/ScriptExecutionCard.tsx
bburda added 9 commits August 22, 2026 18:27
…e wiring

Types re-exported from the generated client, narrowing helpers for the
free-form execution result and error fields, pure reducers for the
client-side execution history, per-entity-type dispatch helpers for the
eight scripts endpoints, a unit-tested polling cycle, and the store state
and actions that tie them together.

The gateway has no endpoint listing executions, so the UI remembers the
ids it started and one interval in the store polls them. History trimming
never drops an execution that is still active, because losing its id
would orphan the process for good.
…iting

Adds the panel, the expandable script row with its parameter form, the
execution status card and the upload dialog, which can either take a file
or let the user write a script in a lazily loaded CodeMirror editor.

The tab appears only when the gateway reports capabilities.scripts, and
only on apps and components, which are the entity types the gateway
registers script routes for.

Playwright ships as a development dependency here as well, ahead of the
end-to-end harness that uses it.
…ct Mode

The stored server URL was passed to connect() from inside a setTimeout,
and Strict Mode's mount-cleanup-remount cycle cleared that timer before
it fired. The ref guard then blocked the second attempt, so a persisted
URL never reconnected in development. Calling connect() directly keeps
the guard's single-attempt behaviour without the cancellable deferral.
The comment claimed languageForFilename mirrors the gateway's three-way
interpreter split one-to-one, but it deliberately does not: an unrecognised
extension returns plain rather than shell, since guessing shell highlighting
for arbitrary content would be wrong more often than it would help, even
though the gateway still falls back to sh for those files at execution time.
Write mode built the upload's multipart filename directly from the typed
file name, so a value like ../../etc/cron.d/evil.sh passed through
unchanged into the request. hasExtension also read the extension from the
whole string rather than the final path segment, so a name such as
my.dir/check reported an extension it did not actually have. File names
are now required to be a plain basename (no separators, no "." or ".."),
checked independently of and before the extension check, and the extension
itself is now read from the last path segment only.

The dialog also now asks for confirmation before discarding write-mode
content the user actually typed, on every way it can be dismissed
(Escape, an outside click, the close button, and Cancel), instead of
silently dropping it.
…rrors

startScriptExecutionAction, the control action and refreshScriptExecution
all cast an openapi-fetch response straight to ScriptExecution. An empty
2xx body (legitimate for a 202) or any other malformed payload could end
up stored as a record with an undefined execution, which then crashed the
next poll (collectActiveExecutions reading its status) or the card's
render. All three now validate the payload through toScriptExecution and
throw before touching the store when it is not usable.

refreshScriptExecution also used to swallow every error behind a try/catch
that only logged to the console, so its "Failed to refresh" toast could
never fire. Only a resource-not-found response is treated as "the
execution is gone" now; everything else, including a bare disconnect, is
rethrown so the card can surface it.

The capability probe that gates the Scripts tab used to be awaited before
loading the entity tree, so a gateway that answered /health and then hung
on GET / left the UI showing "connected" over an empty tree for the full
5s timeout. It now runs without blocking connect(), still under its own
timeout and the existing client-identity guard.

Also caps rendered stdout to a head and a tail behind an explicit
truncation marker instead of trusting the gateway's response size, and
corrects the trimHistory comment to say what it actually guarantees: a
cap on inactive records, not on the list as a whole.
…lete

The raw JSON parameter textarea accepted any value JSON.parse could
produce, cast straight to Record<string, unknown>. An array, string,
number or null all parsed successfully and were sent as parameters,
earning a gateway 400 instead of the inline error this field already
shows for invalid JSON. Parsed values are now required to be a plain
object.

Deleting a script from the robot is irreversible and sits one click away
from Run in the same row. It now asks for confirmation first, matching
the pattern already used for the other irreversible delete in this
codebase (UpdatesDashboard).
The list was cleared and the loading spinner shown synchronously on
every reload, including a manual Refresh or the reload triggered after
an upload or a delete, not just an actual entity switch. That unmounted
every row on screen for the duration of the refetch, dropping whatever
the user was doing inside one, such as an expanded parameter form.

An entity-key ref now tells an actual entity switch apart from a
same-entity reload. Only the former clears the list and shows the
spinner; the latter keeps the current rows rendered until the refetch
resolves.
AppsPanel and EntityDetailPanel carry the entity lifecycle control, so their
tests need the store keys it reads and a TooltipProvider around the render, the
way App.tsx provides one. Without them the panels threw on mount and the
Scripts tab assertions could not run.
@bburda
bburda force-pushed the feature/scripts-tab branch from 4711b3d to 412e662 Compare August 22, 2026 17:01
@bburda
bburda merged commit 85de036 into main Aug 22, 2026
5 checks passed
@bburda
bburda deleted the feature/scripts-tab branch August 22, 2026 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Scripts tab

3 participants