Files
sencho/frontend
Anso c47b8eb8e9 perf(frontend): overlap stack list and status hydration requests (#1820)
* perf(frontend): overlap stack list and status hydration requests

Fire /stacks and /stacks/statuses concurrently while committing the list
first, so list_visible stays progressive and statuses overlap the list
fetch instead of serializing behind it.

The visible list now renders as soon as it commits (isLoading clears at
list commit, not at status completion), and an atomic HydrationEvidence
record derived at render time fails closed on node switches, list
changes, and partial status payloads: lifecycle actions across the
sidebar menu, editor toolbar, mobile editor, bulk bar, and deferred
dialogs recheck a ref-backed readiness predicate immediately before any
mutation.

Status payloads are validated (bulk object format with typed fields,
legacy string maps with recognized values); the legacy per-stack
fallback targets the captured node and tracks coverage, so a total
fallback failure escalates to a hard error instead of masquerading as
ok. Pending, error, stale, and incomplete hydration states render
distinctly in rows, filter chips, and the mobile masthead.

Measured on the SEN-531 workload contract (local node, 6 stacks, Dev
Mode ON): hydration gap 52.5ms median to 0ms, full hydration ~146ms to
~84-100ms (~42% improvement). 2590 frontend tests, visual regression
12/12, backend tsc clean.

* fix(frontend): recheck hydration readiness in deferred lifecycle executors

Deferred continuations (pre-deploy advisory proceed, external-network
continue, update-readiness proceed, service-update proceed) dispatched
against the node captured when the dialog opened, without rechecking
readiness at the actual mutation boundary. A dialog opened on node A
could therefore mutate node A after the operator switched to node B.

Each executor now rechecks the ref-backed readiness predicate before
starting any operation, releasing the pending deploy guard and surfacing
a toast when blocked. Plain Save stays independent of status readiness
(mobile editor and diff-preview plain-save mode are no longer gated);
only Save & Deploy / Save & Reapply requires authoritative runtime
evidence.

Adds two-node delayed-response tests proving late prior-node list and
status results cannot replace the current node's files, statuses,
evidence, or loading ownership, plus deferred readiness-loss regression
tests for the update and service-update dialogs.

* fix(frontend): bind deferred executor readiness to the captured node

The previous readiness rechecks validated the CURRENT node, but deferred
continuations (pre-deploy advisory proceed, external-network continue,
update-readiness proceed, service-update proceed) still dispatched with
the node captured when the dialog opened. A dialog opened on node A could
mutate node A after the operator switched to node B and B finished
hydrating.

The executor-boundary predicate now also requires the captured operation
node to equal the current active node (effect-updated ref), so a
fully-hydrated node switch still blocks the stale continuation.

Adds ready-after-switch regression tests for the deploy continuation,
stack update, and service update (switch to node B, hydrate, invoke the
stored proceed, assert no operation session starts), plus focused plain
Save vs Save & Deploy tests for the mobile editor and the diff preview.
Restores the unrelated frontend lockfile metadata drift.

* fix(frontend): guard external-network creation and reactive retry by node ownership

The missing-external-networks dialog's createAndContinue posted network
creation requests to the captured node before any ownership check, and
the reactive 409 retry callback started a deploy directly without the
executor guard. A dialog opened on node A could create Docker networks
or deploy on node A after the operator switched to node B.

Both paths now recheck hydrationReadyForNode before any mutation: the
proactive dialog closes with the pending guard released and a toast when
blocked, and the reactive retry refuses to start an operation session.

Adds ready-after-switch regression tests for both flows asserting no
/system/networks POST and no operation session after a switch to a fully
hydrated node B.

* fix(frontend): bind policy bypass, delete, and take-down confirmations to their opening node

Three deferred confirmation paths remained unsafe across node switches:
the policy-bypass retry validated current readiness but dispatched to the
policy block's captured node; Delete and Take Down dialogs stored only a
stack name and derived the operation node live at confirmation time, so
a dialog opened on node A would mutate node B's same-named stack after a
switch.

Delete and Take Down targets now carry the opening node id in the overlay
state (deleteTarget / takeDownTarget), and their confirm handlers verify
hydrationReadyForNode against that id before mutating. The policy-bypass
retry uses the same predicate against the block's captured node.

The reactive 409 regression test now reaches the reactive path: the
proactive preflight reports no missing networks, the deploy POST returns
the 409, and the reactive refetch opens the dialog, after which an
ownership change during network creation blocks the retried deploy.

Also restores the unrelated frontend lockfile drift again.

* fix(frontend): validate legacy fallback payloads and target stale refreshes at the current node

The legacy per-stack fallback counted any successfully decoded JSON body
as authoritative coverage, including a 200 non-array body or an array
with malformed entries, which could authorize lifecycle actions on
non-authoritative evidence. The fallback now requires a container array
whose entries satisfy the minimal container shape; malformed responses
fail closed and contribute zero coverage.

A refreshStacks callback captured on node A and invoked after the
operator switched to node B mixed a live list request (localStorage
target) with node A statuses and evidence. refreshStacks now reads the
current node from its render-synchronous ref, and the list request is
targeted explicitly so both requests always share one authoritative
node.

Adds malformed-fallback regressions (non-array, malformed-array,
partial-invalid) and a stale-callback-after-switch test asserting both
requests target the current node and its state stays authoritative.
2026-08-12 11:54:29 -04:00
..

React + TypeScript + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

React Compiler

The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see this documentation.

Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:

export default defineConfig([
  globalIgnores(['dist']),
  {
    files: ['**/*.{ts,tsx}'],
    extends: [
      // Other configs...

      // Remove tseslint.configs.recommended and replace with this
      tseslint.configs.recommendedTypeChecked,
      // Alternatively, use this for stricter rules
      tseslint.configs.strictTypeChecked,
      // Optionally, add this for stylistic rules
      tseslint.configs.stylisticTypeChecked,

      // Other configs...
    ],
    languageOptions: {
      parserOptions: {
        project: ['./tsconfig.node.json', './tsconfig.app.json'],
        tsconfigRootDir: import.meta.dirname,
      },
      // other options...
    },
  },
])

You can also install eslint-plugin-react-x and eslint-plugin-react-dom for React-specific lint rules:

// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'

export default defineConfig([
  globalIgnores(['dist']),
  {
    files: ['**/*.{ts,tsx}'],
    extends: [
      // Other configs...
      // Enable lint rules for React
      reactX.configs['recommended-typescript'],
      // Enable lint rules for React DOM
      reactDom.configs.recommended,
    ],
    languageOptions: {
      parserOptions: {
        project: ['./tsconfig.node.json', './tsconfig.app.json'],
        tsconfigRootDir: import.meta.dirname,
      },
      // other options...
    },
  },
])