Skip to content

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

Open
bburda wants to merge 8 commits into
mainfrom
feature/scripts-tab
Open

Add a Scripts tab for listing, running and writing diagnostic scripts#90
bburda wants to merge 8 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

bburda added 3 commits July 28, 2026 17:09
…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.
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 on lines +15 to +20
/**
* The gateway picks the interpreter from the uploaded file's extension:
* `.py` runs under python3, `.bash` under bash, and everything else - including
* no extension at all - under sh. `languageForFilename` mirrors that split so
* the editor's syntax highlighting matches what will actually execute.
*/
@bburda bburda self-assigned this Jul 28, 2026
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.

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;

* A leading dot alone (`.bashrc`) does not count: the gateway needs a real
* suffix to pick an interpreter, not a hidden-file marker.
*/
export function hasExtension(filename: string): boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

major - the write-mode file name is never validated as a basename, and this is the only validator in the repo.

handleWriteSubmit (ScriptUploadDialog.tsx:167) gates on hasExtension and then does new File([writeContent], trimmedFileName) / form.append('file', file, file.name). Nothing strips path separators. Verified on node 22 with the same undici FormData the browser uses:

new File(['x'], '../../etc/cron.d/evil.sh').name === '../../etc/cron.d/evil.sh'
Content-Disposition: form-data; name="file"; filename="../../etc/cron.d/evil.sh"

So .. and / reach the gateway verbatim, on the one endpoint whose job is writing an executable file onto a robot. Whether the gateway joins that onto its scripts dir is not verifiable from this repo, but the client passes it straight through.

Second hole in the same function: extensionOf splits on the last dot with no awareness of separators, so my.dir/check reports extension /check and hasExtension returns true. The check that is supposed to guarantee the gateway can pick an interpreter passes for a name that has no extension at all.

Reject anything that is not a plain basename (no /, no \, no ./.. segments), and take the extension from the last path segment only.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in f347df7. Both holes were real. extensionOf now takes the last path segment before looking for the dot, so my.dir/check reports no extension instead of dir/check, and a new isPlainBasename rejects empty names, ., .. and anything containing a forward or back slash. handleWriteSubmit runs the basename check before the extension check, so a path is rejected on its own terms rather than incidentally. Covered by tests for my.dir/check, ../../etc/cron.d/evil.sh, backslashes and the bare dot names, each verified to fail against the previous code.

Comment thread src/components/ScriptUploadDialog.tsx Outdated
>
<DialogContent
className="max-h-[80vh] overflow-y-auto"
onEscapeKeyDown={(e) => submitting && e.preventDefault()}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

minor - written script content is discarded on a stray click or Escape.

onOpenChange, onEscapeKeyDown, onPointerDownOutside and onInteractOutside all block dismissal only while submitting. Once a user has typed 80 lines into the editor and is not mid-upload, one click on the overlay or one Escape closes the dialog, the !open effect at line 78 clears writeContent, and the script is gone with no undo and no warning.

contentTouchedRef already holds exactly the bit needed: when mode === 'write' && contentTouchedRef.current, require a confirmation before closing (or preserve the content and restore it on reopen).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 651bbbd. All four dismissal paths now go through one guard: when the mode is write and contentTouchedRef is set, dismissal asks for confirmation before the reset effect runs. Declining leaves the dialog open with the content intact.

Comment thread src/lib/store.ts Outdated
const capsController = new AbortController();
const capsTimeout = setTimeout(() => capsController.abort(), 5000);
const { data: root } = await client
.GET('/', { signal: capsController.signal })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

minor - the capability probe stalls the entity tree for up to 5s, which is what the comment above says it avoids.

The timeout bounds the wait, it does not remove it. This await sits between set({isConnected: true, ...}) and await get().loadRootEntities(). A gateway that answers /health and then hangs on / leaves the UI showing "connected" with an empty tree for a full 5 seconds, on every connect and reconnect.

Run it concurrently with loadRootEntities (Promise.all), or fire it without awaiting - the get().client === client guard already makes a late result safe.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4711b3d. The probe no longer sits between marking the session connected and loading the tree - it runs concurrently, and the existing client-identity guard keeps a late result from writing into a newer session. Connect no longer waits on it at all.

Comment thread src/lib/store.ts Outdated

const key = scriptEntityKey(entityType, entityId);
const record: ScriptExecutionRecord = {
execution: data as ScriptExecution,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

minor - data as ScriptExecution is a cast, not a check, and an empty 2xx body poisons the store.

openapi-fetch yields data === undefined for an empty body on a 2xx. This stores {execution: undefined, ...}. The very next statement calls startScriptPolling, whose collectActiveExecutions reads record.execution.status and throws. The bad record is already in the store, so ScriptExecutionCard destructures record.execution on render (line 61) and trips the ErrorBoundary, replacing the panel - while ScriptRow's catch shows "Failed to start the script" for a script that is in fact running.

A 202 with an empty body is a normal shape, and the comments in this PR say plugin backends are free-form. Guard it: if (!data || typeof data.id !== 'string') throw new ScriptsApiError(...). Same cast, same exposure, at lines 2094 and 2161.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d981893. All three sites now validate the payload before it reaches the store and raise a ScriptsApiError when it is unusable, so an empty 2xx body surfaces as a normal failure instead of a record whose execution is undefined. You were right that the polling loop is what turns it into a crash rather than a bad row.

Comment thread src/components/ScriptsPanel.tsx Outdated
// Clear whatever the previous entity (or previous attempt) rendered so
// it never lingers under the new heading while the fresh request is
// in flight.
setScripts([]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

minor - the reload path unmounts every row and drops whatever the user has typed.

setScripts([]) plus setIsLoading(true) run synchronously before the fetch, so the row list is swapped for the spinner and every ScriptRow unmounts, losing its local state.

Concretely: expand a script, fill in the parameter form (or the raw JSON textarea), then upload a second script (onUploaded -> reload) or press the panel's Refresh button. The row comes back collapsed and empty.

Keep the current list rendered while the refetch is in flight; clear only when entityId/entityType actually change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4711b3d. The list now stays mounted while a same-entity refetch is in flight; it is cleared only when the entity actually changes. The existing guarantee that a late response from a previous entity is never rendered still holds and still has its test.

Comment thread src/components/ScriptRow.tsx Outdated
const trimmed = jsonText.trim();
if (trimmed) {
try {
parameters = JSON.parse(trimmed) as Record<string, unknown>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

minor - any JSON value passes as parameters, not just an object.

JSON.parse(trimmed) as Record<string, unknown> is a cast. Typing [1,2], "hello", 42 or null parses fine, jsonError stays null, and the value is sent as parameters - which StartScriptExecutionRequest declares as Record<string, unknown>. The user gets a gateway 400 instead of the inline error this field already knows how to render.

After parsing, require a plain object (typeof === 'object' && !== null && !Array.isArray) and otherwise set jsonError and return.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 651bbbd. After parsing, the value must be a plain object - not an array, not null, not a scalar - otherwise the existing inline error path fires and nothing is sent.

Comment thread src/lib/store.ts
if (scriptErrorCode(error) === SCRIPT_ERROR_CODE.resourceNotFound) {
set({ scriptExecutions: markExecutionLost(get().scriptExecutions, key, executionId) });
}
return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

minor - Refresh can never report a failure.

Every error branch here returns normally and the surrounding catch swallows the throw path, so refreshScriptExecution never rejects. That makes ScriptExecutionCard.handleRefresh's toast.error('Failed to refresh the execution') unreachable.

A user clicking Refresh against a gateway returning 500 (or entity-not-found) sees the button flick busy and nothing else: no update, no error, no signal the request happened at all. Rethrow for anything other than resource-not-found so the card can surface it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d981893. Anything other than the gateway saying the execution is gone is now rethrown, so the card can surface it. The toast you flagged as unreachable is now reachable and has a test.

<Button variant="default" size="sm" disabled={isStarting} onClick={() => void handleRun()}>
Run
</Button>
{script.managed !== true && (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

minor - deleting a script from the robot is irreversible, one click away, and unconfirmed.

There is no confirmation and no undo, and the button sits in the same row as Run. The repo's other irreversible delete gates on it - UpdatesDashboard.tsx:85: window.confirm('Delete update "..."? This cannot be undone.').

Same gate before deleteScript would match the existing pattern.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 651bbbd, matching the UpdatesDashboard pattern you pointed at, wording included. Worth noting for the stacked pull request: this broke the end-to-end delete scenarios, since Playwright dismisses native dialogs by default. Handled there in c9ce8a7, which accepts the confirmation and asserts it actually appeared, so removing the guard later fails a test rather than passing silently.


{output?.kind === 'stdout' && (
<pre className="text-xs bg-muted rounded-md p-2 overflow-x-auto overflow-y-auto max-h-[200px] whitespace-pre-wrap">
{output.text}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

minor - script stdout is rendered with no cap.

scriptOutput returns the whole parameters.stdout string and it lands in one <pre> text node. max-h-[200px] limits the box, not the DOM. MAX_EXECUTION_HISTORY is 20, so up to 20 complete stdout dumps per entity are held in the store and rendered simultaneously.

A script that prints a few MB (a log dump, a find /, a stuck loop) is enough to lock the tab, and the content is entirely gateway-controlled. Truncate in scriptOutput - keep the first/last N KB behind a "truncated" marker - rather than trusting the response size.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4711b3d. scriptOutput now truncates, keeping a head and a tail behind an explicit marker, so the cap applies to what enters the DOM rather than only to the height of the box.

bburda added 4 commits July 29, 2026 09:20
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.
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