Commit Graph
9 Commits
Author SHA1 Message Date
Ruben FiszelandClaude Opus 5 689f5d7c75 fix: stop reading a parent-only fork item as deleted in the fork (#10467)
* fix: stop reading a parent-only fork item as deleted in the fork

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style: condense the deploy-direction helper comments

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep the ambiguous half of a one-sided diff out of bulk defaults

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: disable select-all on a removal-only list and cover the hidden source-only row

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep parent-only items out of the fork merge list entirely

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: count the fork banner's ahead/behind with the compare page's predicate

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: open the direction the fork banner's button offers

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: cache the sqlx query for the source-only visibility test

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: don't read an unloaded comparison as nothing to deploy

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: treat an in-flight comparison as unknown in the fork banner

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 18:14:41 +00:00
Ruben FiszelandClaude Opus 5 61f2d8dc6a feat: let the merge UI target an arbitrary workspace (#10417)
* feat: let the merge UI target an arbitrary workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep the target picker reachable when a comparison fails

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: address review findings on the arbitrary merge target

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: collapse app/raw-app conversions and offer a comparison retry

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: make a re-scan replace the candidate set and keep retry reachable

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: drop destructive rows from the selection when a recompute flips them

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep bulk selection and refreshes out of the removal opt-in

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: serialize a full scan against dev attachment on the same pair

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep the compare view reachable from drafts and prune stale selections

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: make the destination badge the target picker and reorder the settings

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: render the destination trigger as the same badge as the source

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:46:59 +02:00
+4 30eedf9ee1 feat: Add section to deploy projects to hub (#9332)
* feat: add Deploy to Hub workspace settings tab

* Init record logic

* Fix wordings

* Add publish-app drawer with per-app rate limit mock

- Publish drawer on raw_apps/apps exposes public URL, copy-iframe, unpublish
- Inline per-app rate limit config (req/min, burst, per-IP toggle)
- Rename workspace settings "Default app" tab header to "Apps" to cover both default app and public rate limiting

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Simplify publish drawer to show workspace-wide rate limit only

Drop per-app rate limit fields (req/min, burst, per-IP) — none of these
are supported by the backend. The drawer now shows the existing
workspace-level rate limit read-only with a link to edit it in
Workspace settings → Apps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Rename publish-app drawer wording to 'Share as iframe'

'Publish publicly' was ambiguous (publish to Hub vs make public URL).
Use 'Share as iframe' for the button and drawer title, and 'Generate
iframe' for the confirm action. Intro text now explicitly mentions
iframe embedding use cases (Hub, docs page, own site).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Wire DeployToHub to real workspace data

- Fetch apps, raw_apps, flows, scripts, resources via their services
- Fetch workspace rate limit via WorkspaceService.getSettings
- Share-as-iframe flips app policy.execution_mode to 'anonymous' via
  AppService.updateApp and resolves the real public URL via
  getPublicSecretOfApp + computeSecretUrl
- Detect already-public apps from listApps execution_mode field
- Filter out app_theme resources (noise, present in every workspace)
- Hub bundle/version push and recording remain mocked (no backend yet)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Wire recordings to real jobs with run-preview UX

- Recording flow now fetches the real schema, runs the job, and polls
  getCompletedJobResultMaybe to surface success/failure before saving.
- Drawer shows a sticky status box (loader / success / failure) with a
  result preview, a job link, and an in-context Save CTA.
- Only successful runs can be saved as a recording. Failures show the
  error and offer re-run.
- Filter cache/state/app_theme internal resource types (mirrors
  workspaces_export.rs filter).
- Added "What is a recording?" explainer banner above the items list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add draft/review state machine and submission gating

- Phases: predeploy → draft → under_review → live, with workflow
  step indicator and contextual footer actions per phase
- Bundle drawer collects name + readme before pushing the draft
- draftItems snapshot frozen at deploy time; workspaceItems keep
  refreshing without affecting the draft
- Folder MultiSelect lets users scope the bundle to one or more
  folders; empty = whole workspace
- Submit-for-review disabled until every script and flow in the
  draft has a recording (progress bar + counter)
- Recordings now run the real job and poll for success/failure;
  only successful runs can be saved
- under_review phase locks editing, sharing, and recording
- Dark mode variants on every coloured banner
- Steps card shows the full 3-step process always, highlighting the
  current step

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Make recordings optional, encourage them for discoverability

- Submit for review no longer gated on full recordings
- Footer hint now frames recordings as boosting approval speed and
  public Hub featuring, not as a hard requirement
- Progress card label switched from 'Recordings needed' to
  'Recordings recommended'
- Items without a recording display a yellow 'No recording' badge in
  every phase so the gap stays visible after submission

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Allow per-item selection inside the bundle scope

- Items in predeploy now have checkboxes (all selected by default)
- Select all / Deselect all act on the current folder filter
- manualDeselected resets when the folder filter changes
- Bundle button uses the selected count, disabled when zero
- Draft snapshot keeps only the selected items
- Checkboxes hidden in draft / under_review / live phases

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add diff button once approved by admins

* Small fix

* Nits

* fix(deploy-to-hub): paginate workspace list and cancel stale record polls

- loadWorkspace fetches all pages instead of capping at 100 items per kind
- pollJobUntilComplete now bails when recordRunSeq advances (new record
  target, re-run, or drawer close), preventing late completion of a
  previous run from overwriting current state

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* perf(deploy-to-hub): parallelize public-app URL resolution

resolvePublicUrl now runs once per anonymous app via Promise.all instead
of serially inside the items loop, removing N round-trips from initial
tab load.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(indexer): tell admins when ingress routes search to wrong pod (#9274)

* [ee] fix(indexer): tell admins when ingress routes search to wrong pod

When the IndexReader is absent on the pod handling a search request but
another pod is actively holding the indexer lock, the EE handler now
returns a tailored error pointing at the ingress/load-balancer
configuration instead of the generic "indexer not running" message.

The indexer status endpoint reads the DB lock so it reports "running"
from any pod, but search endpoints need the in-memory IndexReader that
only exists on the lock holder. In multi-replica deployments this looks
like the indexer is healthy but every search 404s.

Companion: windmill-labs/windmill-ee-private#TBD

Fixes WIN-1968.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to eb18d7b4c0e37fea3f6e1e2cc44e0fddd74ff817

This commit updates the EE repository reference after PR #586 was merged in windmill-ee-private.

Previous ee-repo-ref: 7dd43d1850813071cc18ba49ba090583e7321f4b

New ee-repo-ref: eb18d7b4c0e37fea3f6e1e2cc44e0fddd74ff817

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>

* feat(cli): add `wmill init prompts` and custom override slot (#9266)

* feat(cli): add `wmill init prompts` and custom override slot

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(cli): replace init prompts with refresh prompts + AGENTS.md/AGENTS.cli.md split

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(cli): dedupe claude skills via @-includes and add prompts freshness check

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(cli): drop migration-choice flags from `refresh prompts`

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(cli): add 'Running and previewing local changes' section to AGENTS.cli.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(cli): write full skill content to .claude/, drop @-include wrapper

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(cli): reconcile CLAUDE.md the same way as AGENTS.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(cli): address PR review nits — argv parsing, lazy import, comment detection, error propagation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: add yolo mode for ai chat tools (#9258)

* feat: add yolo mode for ai chat tools

* nit

* fix: align chat footer controls

* feat: add ai chat autonomy modes

* feat: add autonomy mode dropdown

* fix: highlight yolo autonomy icon

* fix: auto accept flow edits

* fix: hide unsupported autonomy modes

* fix: handle auto-accept flow editor races

* fix(debugger): add non-root user support to Dockerfile (#9277)

Mirrors the main Windmill Dockerfile pattern: creates a windmill user
(UID/GID 1000) and makes cache/work directories world-writable so the
image runs cleanly under Kubernetes securityContext.runAsNonRoot or
runAsUser: 1000 without permission errors on Bun, pip, or windmill
cache writes.

Fixes WIN-1969

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ai): enforce RLS and scope check on user-supplied X-Resource-Path (#9276)

* fix(ai): enforce RLS and scope check on user-supplied X-Resource-Path

The AI proxy handler accepts an X-Resource-Path header to override the
configured workspace AI provider. When supplied, the handler loaded the
resource value from the resource table using the root DB pool with no
resources:read scope check, so any authenticated workspace user could
point X-Resource-Path at a restricted AI resource (e.g. one in a folder
they cannot read) and the proxy would use that resource's provider
credentials for the outbound AI request.

For user-supplied resource paths, now require resources:read:{path}
scope and fetch the resource through user_db.begin(&authed) so RLS
enforces the same folder/group boundary as the resource API. The RLS-
scoped $var: resolution stays in place as defense in depth. The
admin-configured workspace/instance ai_config path is unchanged.

Fixes WIN-1971

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ai): regression test for X-Resource-Path RLS enforcement

Cover all four cases:
- non-admin pointing X-Resource-Path at a restricted resource is rejected
- non-admin pointing it at a resource they own still works
- admin can point it at any resource
- workspace-configured proxy flow (no X-Resource-Path) is unchanged

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: add userdraft listing primitives (#9268)

* feat: add userdraft listing primitives

* fix: cancel stale userdraft discard writes

* docs: remove global ai userdraft plan

* feat(nsjail): optional disk-backed /tmp via instance setting (#9272)

* feat(nsjail): optional disk-backed /tmp via instance setting

* test(nsjail): unit-test tmp mount resolver and narrow visibility

* refactor(nsjail): switch tmp backing to select + conditional UI

* ui(nsjail): make tmpfs the visible default in /tmp backing select

* fix(nsjail): refuse preexisting jail_tmp to block symlink escape

* fix(nsjail): allow jail_tmp reuse on sequential nsjail calls

Codex flagged that python/ruby/rust executors invoke nsjail twice per
job_dir (install then run). The previous resolver treated any preexisting
jail_tmp as hostile and silently fell back to tmpfs on the second call,
so disk-backed mode never reached the main script run for those langs.

Use symlink_metadata().is_dir() to distinguish a real directory left by
an earlier call in the same job_dir (safe to reuse) from a symlink or
other entity (still refused, as the codebase-tar escape requires).

Also loosen the frontend visibility predicate: only hide nsjail settings
when job_isolation is explicitly 'none' or 'unshare', so deployments
that enable nsjail via DISABLE_NSJAIL=false with no DB setting can
still see the controls.

* chore(main): release 1.706.0 (#9270)

* chore(main): release 1.706.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>

* fix(nsjail): gate unix-symlink test behind cfg(unix) for Windows build (#9280)

The disk_backed_refuses_preexisting_symlink_at_jail_tmp test calls
std::os::unix::fs::symlink directly, which doesn't exist on Windows
targets. Without a cfg gate, `cargo check --tests` fails on Windows
with E0433. Other symlink call sites in this crate (php_executor,
bun_executor, rust_executor, etc.) already follow this pattern.

Fixes WIN-1972

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Reduce slim image vulnerability surface (#9279)

* Reduce slim image vulnerability surface

* chore(docker): drop apt-get upgrade -y from slim images

apt-get upgrade hurts build reproducibility (same Dockerfile + same
commit at different times produces divergent images) and trips hadolint
DL3005. The freshness it buys is dominated by simply rebuilding against
the periodically-refreshed debian:bookworm-slim base image.

The --no-install-recommends and apt-list cleanup wins are kept.

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>

* fix(git-sync): bump to hub/28234 with stateless gpg.program wrapper (WIN-1974) (#9282)

* fix(git-sync): revert LATEST_GIT_SYNC_SCRIPT_PATH to hub/28230 to restore GPG-signed deploys (WIN-1974)

hub/28231 (PR #9230) is the "thin" script that hands the actual `git commit`
to the CLI's hidden `sync git-deploy`. The hub script still does the GPG
setup (import key into a fresh GNUPGHOME, dummy `gpg -bsau` to warm the
agent passphrase cache, then `git config user.signingkey` + `commit.gpgsign`
locally), but the commit no longer runs in the same `git_push` flow — it
runs minutes later inside the CLI after workspace API resolution, zip pull,
file extraction, and lockfile autofill. By the time the spawned `git commit`
asks gpg-agent for the cached passphrase, the cache state is no longer
reliable (or the spawned `gpg` ends up talking to a fresh agent), so signing
fails non-interactively with `gpg failed to sign the data`.

hub/28230 is hub/28217's in-script logic rebuilt with windmill-cli@1.703.3:
the GPG setup and the in-script `sh_run("git commit ...")` happen back-to-back
in `git_push`, so the cache is always fresh. It preserves wm_deploy / fork
branch behavior, the EE deployment-callback `main()` signature is unchanged,
and the only min-version check in EE (`is_script_meets_min_version(28103)`)
is comfortably below 28230 — so this revert is safe.

Forward fix (separate PR): publish a new thin script that, alongside the
existing GPG setup, writes a `gpg.program` wrapper using `--pinentry-mode
loopback --passphrase-file` so signing is independent of the agent's cache
state. Re-bump past 28231 then.

Fixes WIN-1974

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(git-sync): check in source-of-truth for the next hub script (gpg.program wrapper)

This is the script that will be published to hub.windmill.dev once verified
on a customer GPG-signed deploy. It replaces hub/28231's agent-cache
pre-warm (`gpg -bsau` with --passphrase) with a stateless gpg.program
wrapper + chmod-600 passphrase file. Every git-invoked gpg call goes
through the wrapper, which always uses --pinentry-mode loopback (and
--passphrase-file when a passphrase exists). Signing no longer depends on
gpg-agent having a cached passphrase by the time the CLI's `git commit`
runs — which closes WIN-1974.

Not wired in yet: LATEST_GIT_SYNC_SCRIPT_PATH stays on hub/28230 until this
script is uploaded and the new hub id is known. This file is checked in so
the diff is reviewable, future bumps have a source of truth, and a CLI
regression test can `cat` it for fixture parity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(frontend): skip format/pattern validation for $var/$res/$jsonvar references in ArgInput

A resource field with a `pattern` constraint (e.g. the gpg_key.private_key
field, whose pattern enforces a `-----BEGIN PGP PRIVATE KEY BLOCK-----`
prefix) rejects values like `$var:u/me/gpg-private-key` with an "invalid
format" error in the resource editor — even though `$var:`/`$res:`/`$jsonvar:`
are placeholders the backend resolves at runtime, not the actual string
that needs to match the regex.

Bail out of all format/pattern checks (email, ipv4, ipv6, uuid, custom
pattern) when the value is one of these references. Required/numeric
bounds/array checks still apply since they're shape-level, not regex.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(git-sync): bump LATEST_GIT_SYNC_SCRIPT_PATH to hub/28234 (gpg.program-wrapper fix)

hub/28234 is the forward fix for WIN-1974: replaces hub/28231's agent-cache
pre-warm (which became stale by the time the CLI's `git commit` ran) with
a stateless `gpg.program` wrapper that uses `--pinentry-mode loopback`
(and `--passphrase-file` when a passphrase exists) on every gpg invocation.
Bundled CLI is windmill-cli@1.705.0.

Verified via reproducer at /tmp/git-sync-diff/test-gpg-fix.sh: deliberately
killing gpg-agent between GPG setup and `git commit` reproduces the
customer's `gpg failed to sign the data` error verbatim under the old
flow, and the wrapper signs through it. Holds for passphrase-protected
keys, split-subkey [C]+[S] layouts, and unprotected keys.

Drops the local source-of-truth copy (`hub-scripts/`) — hub is canonical
now that 28234 is published.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(git-sync): drop verbose comment above LATEST_GIT_SYNC_SCRIPT_PATH

The git history (this PR) carries the why; the constant name + value carry
the what.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(cli): wmill sync git-deploy stops committing; caller owns commit+push (#9284)

Single contract for the deployment-callback path: the CLI does branch
checkout + pull, the caller (hub script in production, test in test)
does git add + commit + push. This restores the WIN-1974 invariant —
GPG setup and `git commit` run back-to-back in the same process, so
the agent's pre-warmed passphrase cache is still warm at sign time —
without needing a `--skip-commit` flag for the hub case and a default
"also-commit" for everything else. Same behavior in every call site.

Changes:
  - sync.ts: drop the gitSyncDeployPush call from pull()'s deploy path
    (both the onlyCreateBranch fast-return and the post-pull commit).
    `gitSyncDeployPush` stays exported for any caller that wants the
    same commit/push semantics — just not invoked by the CLI subcommand.
  - gitsync_promotion.test.ts: e2e test now does its own git add +
    commit + push after `wmill sync git-deploy`, mirroring what the
    hub script does in production. Same regression coverage
    (wm_deploy branch created in Case A, main untouched; main updated
    in Case B, no new wm_deploy).

CLI typecheck unchanged (two pre-existing TarAsZip errors at lines
2578/3307, present before this PR). All 743 unit tests still pass.

The accompanying hub script (option-C — CLI for branch+pull, script
for commit+push) lives at /tmp/git-sync-diff/sync-script-to-git-repo-windmill.option-C.ts.
Once published, a follow-up bumps LATEST_GIT_SYNC_SCRIPT_PATH to its id.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bump git sync to 28236

* fix: fork compare visibility for non-admins and stale-token superadmins (#9283)

* fix: use fork-scoped authed for fork visibility in compare_workspaces

* test: add EE end-to-end repro for fork rename visibility

* chore: restore concurrency_locks sqlx cache lost in cleanup

* test: add regression for stale-superadmin-token fork visibility bug

* chore: update sqlx cache for new test queries

* chore(main): release 1.706.1 (#9281)

* chore(main): release 1.706.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>

* feat: add wmill job rerun subcommand (#9275)

* feat: add wmill job rerun subcommand

* feat: add wmill job restart subcommand for flow restart-at-step

* chore(system_prompts): point plugin skills sync at plugins/windmill/ (#9287)

* chore(system_prompts): point plugin skills sync at plugins/windmill/

The plugin checkout's plugin folder is being renamed from
`plugins/windmill-code-plugin/` to `plugins/windmill/` to shorten the
slash-command namespace and align with the matching Cursor plugin
layout.

Paired with windmill-labs/windmill-claude-plugin#8. That PR must merge
first so the next sync run finds the new folder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(system_prompts): update plugin-dir example to plugins/windmill

Co-authored-by: centdix <centdix@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: centdix <centdix@users.noreply.github.com>

* fix(cli): wmill sync pull updates wmill-lock.yaml for raw apps (#9289)

* fix: flow recording teardown crash + rename package to @windmill-labs/components (#9288)

* fix: guard against null recording during FlowRecordingReplay teardown

Navigating away from a flow recording inside a workspace file-tree view
threw `TypeError: Cannot read properties of null (reading 'flow')` from
FlowGraphViewer once during the teardown tick.

Svelte 5 compiles child component props as live getters that close over
`$$props.recording.flow`. When `recording` flips to null on the parent's
navigation, an outer `{#if !recording?.flow}` doesn't stop those getters
from firing one more time as derived effects re-evaluate before the
unmount lands — so the getter dereferences null and throws.

Fix at the two layers where the deref actually happens:

- FlowRecordingReplay: use `recording?.flow` at the binding sites
  (FlowViewer + graph-snippet FlowGraphViewer) so the compiler emits an
  optional-chained getter, and guard the snippet branch with
  `{:else if recording?.flow}` so it doesn't mount when there's nothing
  to show.
- FlowGraphViewer: finish the optional chaining the rest of the file
  already used everywhere else (`flow?.value?.skip_expr`,
  `flow?.value?.cache_ttl`, `flow?.schema`). When the upstream
  binding returns undefined during teardown, the graph degrades to an
  empty frame instead of crashing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: rename package to @windmill-labs/components

- frontend/package.json: rename `windmill-components` → `@windmill-labs/components`
- frontend/publish.sh: drop the in-place sed rename dance; the checked-in name now matches what's published, so `npm run package && npm publish` is enough
- frontend/package-lock.json, system_prompts/auto-generated/prompts.d.ts: regenerated by `npm run package` under the new name

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(flows): restore Variables and Resources in flow editor prop picker (#9290)

The design system overhaul in 888837431c accidentally dropped the
fallback condition that displayed the Variables and Resources sections
in the prop picker by default. After that commit, these sections only
appeared when the user typed `variable.` or `resource.` in their
expression, which meant they effectively disappeared from the flow
editor's prop picker for most users.

Restore the previous behavior by showing the sections when no input
match is active (the equivalent of the old `!filterActive` clause).

Fixes WIN-1976

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(auth): tighten token-owner fallback for unscoped tokens (WIN-1978) (#9293)

* fix(auth): reject unscoped tokens with cross-workspace forged owners (WIN-1978)

An unscoped token (workspace_id IS NULL) whose `owner` field references a
user, group, or unprefixed value that is not present in the target
workspace must not authenticate. The previous fallback in the
`u/<username>` branch granted `(is_admin=false, is_operator=true)` when
no `usr` row matched in the target workspace, letting a token holder
who could mutate the `token` table cross workspace boundaries with
operator privileges.

The `g/<groupname>` branch likewise silently accepted any group name as a
"group user", and the no-prefix branch granted operator state from
arbitrary owner strings. Both are now rejected unless the owner matches
a real user/group membership in the target workspace.

Adds an integration regression covering all three forged-owner shapes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: drop integration regression for auth fallback

The test added in the previous commit relies on a sqlx::query! that
requires offline-cache regeneration; removing per code-review preference
to keep this PR scoped to the auth-layer fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ResourceEditor): don't reset state when `selected` reverts to undefined (#9295)

The bootstrap effect tracked `selected` via its early-return check, so any
time `selected` flipped back to `undefined` it would re-run and reinitialize
`states[effectiveWorkspace]` to empty — wiping user input. This happens in
the React SDK consumer: reactify re-syncs all Svelte props on every React
render, and since `selected` isn't passed through, `$props()` reverts it.

Move the `selected !== undefined` check inside the existing `untrack` so
the effect only tracks `effectiveWorkspace`. Bootstrap still runs once on
mount; subsequent `selected` flips no longer retrigger it.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(secret-backend): pass DB to Vault migrations + show failure details (#9292)

* [ee] fix(secret-backend): pass DB to Vault migrations + surface failure details

Companion to windmill-ee-private fix for WIN-1977. The HashiCorp Vault
migration always failed under JWT/OIDC auth because the migration
constructed VaultBackend without a DB, so every secret hit "Database
connection required for JWT authentication". Creating new secrets worked
because the runtime path passes the DB.

Frontend: when failed_count > 0, the toast and console now show the
per-secret failures (path + error, capped at 5 with "...and N more")
instead of just aggregate counts.

Fixes WIN-1977

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to 14315067c083d3361512de621b12e41dbe3b017d

This commit updates the EE repository reference after PR #587 was merged in windmill-ee-private.

Previous ee-repo-ref: 390ed6c851b1915f0b492897c663f8058477680f

New ee-repo-ref: 14315067c083d3361512de621b12e41dbe3b017d

Automated by sync-ee-ref workflow.

* fix(secret-backend): escape failure fields and use <br> in migration toast

Address CI review on PR #9292:

- P1 (cubic/codex): backend-supplied workspace_id/path/error are now
  HTML-escaped before being interpolated into the migration toast,
  which renders through {@html processMessage(...)} in Toast.svelte.
  This prevents stored XSS via secret paths or backend errors that
  contain markup. '/' is intentionally left intact so the toast's
  path-highlight regex still tags workspace paths.
- P2 (pi): swap '\n' for '<br>' so multi-line failure lists actually
  break in the toast instead of collapsing to a single run-on line.
- Extend the same per-secret failure surfacing (toast + console.error)
  to the Azure Key Vault and AWS Secrets Manager migration handlers
  via a shared reportMigrationFailures() helper so all six migration
  paths report identically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>

* nit react-sdk resource editor

* sdk_resource

* make `selected` resilient + snapshot args for React (#9298)

* fix(ResourceEditor): make `selected` resilient + snapshot args for React

Two issues surfaced via the React SDK (reactify wrapper re-spreads Svelte
props on every host re-render):

1. The bindable `selected` prop transiently resets to undefined on each
   re-spread, flipping `current` through undefined and unmounting the
   form (input loses focus on every keystroke). Rename the prop to
   `selectedProp` and derive `selected = selectedProp ?? effectiveWorkspace`
   so the fallback insulates the component without effects.

2. The onChange dispatch passed `current.args` (a `$state` proxy) directly,
   so React consumers diffing by reference or JSON.stringify saw the same
   value forever, and the effect only tracked the args reference (not
   nested mutations). Wrap with `$state.snapshot` to deep-track and emit
   a plain object.

The bootstrap effect is also restructured: it no longer writes `selected`
(the derived handles defaulting) and now guards on `selected in initialStates`
so workspace flips remain idempotent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ResourceEditor): declare effectiveWorkspace before use in selected

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* remove unused workflow

* feat(typescript-client): add deleteS3File + optional workspace arg on S3 helpers (#9300)

* feat(typescript-client): add deleteS3File + optional workspace arg on S3 helpers

Customer-requested ergonomics for the TypeScript SDK:

- New `deleteS3File(s3object, workspace?)` wrapper around the existing
  `HelpersService.deleteS3File` (backend endpoint is already there). Saves
  callers from having to either hand-roll `denoS3LightClientSettings()` +
  AWS SDK calls, or wire up `HelpersService` directly.
- `denoS3LightClientSettings`, `loadS3File`, `loadS3FileStream`, `writeS3File`,
  and the new `deleteS3File` all gain an optional trailing `workspace?: string`
  parameter that falls back to the `WM_WORKSPACE` env var via `getWorkspace()`.
  Mirrors the calling convention customers already expect from helpers like
  `getVariable` / `runScript`.

`build.sh` and `build.jsr.sh` are updated to export `deleteS3File` from both
the NPM and JSR entry points.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: regenerate system_prompts auto-generated for new S3 helpers

`python system_prompts/generate.py` after adding deleteS3File and the
optional workspace param to the existing S3 helpers, so the agent-facing
docs (CLI skills, TS SDK prompt, script skills) reflect the new signatures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(github-app): hide cloud-only UI on self-managed + admin assignment UI (#9299)

* feat(github-app): hide cloud-only UI on self-managed + admin assignment UI

Two related UX fixes for the GitHub App self-managed (GHES) integration:

1. On self-managed instances, the per-installation Export button and the
   "Import installation from other instance" section in the workspace UI both
   hide. Both round-trip a JWT carrying only {installation_id, account_id} with
   no github_base_url, so they would produce broken cloud-style installs on a
   self-managed instance. The previous Export attempt also failed with
   "No JWT token received from server" because self-managed installs store an
   empty JWT by design.

2. New "Workspace assignments" panel in instance settings (GhesAppSettings.svelte)
   that auto-discovers installations of the configured GHES App and lets the
   super-admin assign them to specific workspaces. Workspace users without
   GitHub permissions no longer need to install the App themselves — the admin
   provisions the link from instance settings. Admin-provisioned installs show a
   "Provisioned by admin" badge in the workspace UI and can only be removed by
   the super-admin from instance settings.

Backend support is in the EE companion PR
windmill-labs/windmill-ee-private#588.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to da5189cf69a453de3855057f41be0d84e5910707

This commit updates the EE repository reference after PR #588 was merged in windmill-ee-private.

Previous ee-repo-ref: d959b83ce413ad531e9cc28e0f8199cdecb73a31

New ee-repo-ref: da5189cf69a453de3855057f41be0d84e5910707

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>

* chore(main): release 1.707.0 (#9285)

* chore(main): release 1.707.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>

* feat(queue): per-workspace fairness cap on the shared cloud worker pool (#9303)

* feat(queue): cloud-only per-workspace fairness cap on the shared worker pool

On `app.windmill.dev` the cluster runs a single default worker group, so a
single workspace flooding the queue can degrade quality of service for
everyone else. This adds an opt-in mechanism that caps any single workspace
at a configurable share of the shared worker pool when it has been
dominating cluster activity for more than a configurable window.

Detection signal counts both currently-running jobs and jobs completed in
the rolling window, so it catches workspaces hogging slots with long jobs
**and** workspaces spamming many tiny jobs (where no individual job's
started_at is old, but throughput share dominates).

Refresh is coordinated cluster-wide via a single UPDATE on
`background_task_state`: the `WHERE updated_at < now() - interval` predicate
combined with row-level locking means only one process per refresh cycle
actually runs the aggregation, regardless of fleet size. Every other
process gets the freshly written value in the same round trip via
`UNION ALL ... LIMIT 1`. Heavy aggregation rate stays at ~0.2-0.5 qps for
the whole cluster.

Pull queries are split: the existing query string and its bind shape stay
bit-identical to today, so the planner keeps using the same indexes when
fairness is off or no workspace is currently capped. A separate
`WORKER_PULL_QUERIES_FAIRNESS` adds `AND workspace_id <> ALL($2::text[])`
and is only materialized while the feature is enabled.

Hard-gated to `CLOUD_HOSTED=true` + BASE_URL host == app.windmill.dev at
three layers: frontend `cloudonly: true`, API setter rejection in
`set_global_setting_internal`, runtime check in `fairness_active`. Settings
are exposed under Jobs in the instance-settings UI; defaults are off so
the change is a no-op for self-hosted.

Two-pass pull guarantees no worker idling: if every queued job belongs to
a capped workspace, the second pass uses the unmodified pull queries.
Cap re-asserts on the next refresh.

Fixes WIN-1982

* fix(queue): address CI review findings on workspace fairness

Six fixes from the four-reviewer cross-check on #9303:

1. **Aggregation evaluation (Codex P1).** The previous `INSERT ... ON CONFLICT
   DO UPDATE WHERE updated_at < ...` had the heavy `v2_job_queue ∪
   v2_job_completed` aggregation inlined into `VALUES`, which Postgres
   evaluates for every contender to build the proposed row — losing the
   "one heavy aggregation per cycle cluster-wide" property the design
   advertises. Split into three small statements: (a) cheap claim with
   constant `VALUES`, (b) winner-only `UPDATE ... SET value = jsonb_build_object('overloaded', <agg>)`
   (Postgres only evaluates `SET` per row matching `WHERE`, so losers never
   compute the aggregation), (c) read for everyone. Heavy query now truly
   runs ~0.2-0.5 qps cluster-wide regardless of fleet size.

2. **Numeric setting wraparound (cubic P1).** `u64 as u32` and downstream
   `u32 as i32` could silently flip sign and feed `make_interval(secs => -N)`,
   making `now() - interval` a future timestamp and disabling the
   completed-jobs half of the activity signal. Clamp `duration_secs` to
   [1, 86400] and `min_total_jobs` to [0, u32::MAX] before storing.

3. **`/instance_config` bypass (cubic/Claude/Codex P2).** Bulk config endpoint
   sidestepped `set_global_setting_internal`'s gate; a self-hosted superadmin
   could persist `workspace_fairness_*` rows via the bulk path. Mirror the
   per-key check in `set_instance_config` upsert flow.

4. **DB error coerced to false (Claude P2).** `load_workspace_fairness_enabled`
   collapsed `Err(_)` to `false` and unconditionally swapped the atomic — a
   transient DB blip during notify-event propagation toggled the feature off
   cluster-wide (and triggered a `store_pull_query` rebuild precisely when load
   is highest). Now propagates the error so the atomic stays at its prior value.

5. **Refresh failure cooldown (Claude P2).** Storing `0` removed the rate
   limit entirely; every subsequent pull spawned a new refresh task. Leave
   `LAST_REFRESH_MICROS` at `now_us` (already written by the CAS) so the
   natural interval acts as the cooldown.

6. **Visibility + duplication (Pi P2).** Mark `make_pull_query_fairness` as
   `pub(crate)`. Move the duplicated `BASE_URL host == app.windmill.dev`
   parser into `windmill-common::worker::is_cloud_production_host` and share
   it between the API setter and the runtime path.

Verified locally:
- `POST /api/settings/global/workspace_fairness_enabled` → 400 (per-key gate)
- `PUT /api/settings/instance_config` with fairness key → 400 (bulk gate)
- `cargo check --workspace --features=private,enterprise,quickjs` — clean

Refs WIN-1982.

* fix(queue): second round of CI review nits on workspace fairness

Three issues raised by the Codex/Claude re-review of commit 0b38ff2:

1. Non-cloud deletes were rejected (Codex P2). The cloud gate ran before
   the Null / empty-string deletion branches in both `set_global_setting_internal`
   and the bulk `set_instance_config`. A self-hosted instance that inherited
   stale `workspace_fairness_*` rows from a cloned cloud DB couldn't clear
   them through the API — the rows stayed in `global_settings` and continued
   to show up in the YAML export. Now the gate only blocks upserts; Null /
   empty-string deletes pass through on any host.

2. Deleted numeric knobs kept stale runtime values (Codex P2). When a
   cloud admin cleared `workspace_fairness_max_percent`, `..._duration_secs`,
   or `..._min_total_jobs`, the notify-event fired but the numeric loaders
   ignored `Ok(None)` and left the previous in-memory value pinned until
   process restart. Loaders now distinguish three outcomes:
     - `Err(_)`: transient — leave atomic alone (preserves the
       previous-round fix).
     - `Ok(None)` / `Ok(Some(invalid))`: reset to the documented default.
     - `Ok(Some(valid))`: clamp and store.
   Defaults are extracted to `WORKSPACE_FAIRNESS_*_DEFAULT` constants kept
   in sync with the `AtomicU32::new(...)` initialisers in
   `windmill-common/src/worker.rs`.

3. `fairness_active` was `pub` with no cross-crate caller (Claude nit).
   Tightened to module-private.

Verified locally on this non-cloud instance:
  POST .../workspace_fairness_enabled  body=null  → 200 (delete passes)
  POST .../workspace_fairness_enabled  body=true  → 400 (set blocked)
  PUT .../instance_config              {}         → 200 (no-op passes)
  PUT .../instance_config  with fairness key      → 400 (bulk set blocked)

Skipped the partial index on `v2_job_queue WHERE running = true` that
Claude flagged as a residual nit — queue stays under 50k rows per the
operator's measurement, so the seq-scan cost (~10 ms × 0.5 qps =
~0.5% of a DB core) is well below the noise floor and the index isn't
worth the maintenance cost on job transitions.

Refs WIN-1982.

* chore(main): release 1.708.0 (#9304)

* chore(main): release 1.708.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>

* feat: add copy button to Path component (#9311)

* feat: plug global chat drafts into userdraft (#9291)

* refactor: move global chat drafts to userdraft

* feat: share script and flow drafts with editors

* feat: share trigger drafts with editors

* feat: share raw app drafts with editor

* feat: share resource drafts with editors

* docs: rename global chat drafts copy

* feat: add global chat draft discard tool

* fix: resolve global chat editor draft paths

* fix: remove editor draft path resolver

* feat: track live editor drafts in userdraft

* fix: snapshot live userdraft reads

* chore: checkpoint pending global draft changes

* fix: address global draft review issues

* fix: defer raw app draft persistence

* docs: remove pr investigation docs

* fix: persist live global draft writes

* refactor: move bedrock proxy handling to windmill-ai (#9309)

* refactor: move bedrock proxy handling to windmill-ai

* docs: track ai refactor follow-ups

* fix(auth): filter resource/variable listings by token scope (WIN-1981) (#9302)

A token scoped to a single resource (e.g. `resources:read:u/alice/foo`)
could call `GET /api/w/{w}/resources/list_search` and receive `path` and
`value` for unrelated resources in the workspace. Route-level scope
checks only validate `domain:action`; per-resource handlers do a
`check_scopes` against the path, but the listing endpoints did not —
leaking integration credentials, API keys, and other secrets stored as
resource values to narrowly-scoped tokens.

Add `build_scope_path_predicate` to `windmill-api-auth` (mirrors
`check_scopes` semantics but parses the token's scopes once, suitable
for filtering many rows). Apply it to `list_search_resources`,
`list_resources`, `list_names` (resources) and `list_variables`
(non-secret value leak), so a scope-restricted token only ever sees the
paths it is authorized to read. Unscoped tokens and tokens whose only
scopes are `if_jobs:filter_tags:*` are unaffected.

Includes regression tests covering: unscoped, tag-filter-only,
single-resource, wildcard, wrong-domain, and write-implies-read.

Fixes WIN-1981

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* audit-log workspace-fairness cap transitions (#9306)

* feat(queue): audit-log workspace-fairness cap transitions

When the cloud per-workspace fairness mechanism adds a workspace to the
capped set or releases one, write `workspace_fairness.capped` /
`workspace_fairness.uncapped` audit-log entries to the affected workspace.
The cluster admin can review the full timeline from the `admins` workspace
audit view with `all_workspaces=true`; per-workspace owners see their own
events in their normal audit list.

Only the per-cycle refresh winner emits entries (matching where the heavy
aggregation runs), so a fleet of N workers does not produce N duplicates
per transition. The diff is computed against the value already in
`background_task_state` rather than the winner's in-memory cache, so a
freshly-restarted process winning the claim does not spuriously emit
"newly capped" entries for workspaces that were already capped before it
started.

Audit writes are best-effort: failures are logged via tracing and do not
abort the refresh cycle.

Fixes WIN-1984

* feat(queue): scope fairness audit to admins workspace + queue-metrics pane

- Write `workspace_fairness.capped` / `workspace_fairness.uncapped` to the
  `admins` workspace (was: per-affected-workspace) with the affected
  workspace_id moved to the `resource` field. Cluster admins now get the
  full timeline in one place without `all_workspaces=true`.
- Add `GET /workers/workspace_fairness_events` returning the last 100
  events. Cloud-gated (returns `[]` on non-cloud) and devops-only.
- Add a `WorkspaceFairnessEvents` Section to the Queue Metrics drawer,
  rendered only when `isCloudHosted()` is true. Shows time / event
  badge / workspace / parameters with a refresh button.

Fixes WIN-1984

* feat(ai-chat): expand chat question answers (#9310)

* feat(ai-chat): align footer bar + DropdownV2 mode/autonomy selectors (#9308)

* feat(ai-chat): align footer bar, use DropdownV2 for mode/autonomy selectors

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(dropdown): add `selected` item prop rendering a trailing check

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(ai-chat): add small spacing between chat input and footer bar

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ai-chat): always offer the 3 autonomy options in the auto-accept picker

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ai-chat): default autonomy mode to auto-accept on

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(ai-chat): use Button component for footer dropdown triggers

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(ai-chat): use a hand icon for the auto-accept-off autonomy state

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(ai-chat): use subtle Button variant for mode and model selectors

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(ai-chat): tighten spacing between input and footer bar

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ai-chat): reword autonomy levels as ask/auto-accept/bypass permissions

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(button): add 2xs unified size with tighter padding

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ai-chat): compact footer bar — 2xs buttons, AtSign context icon, short Yolo label, discreet model

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(ai-chat): widen the permission selector dropdown

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(dropdown): group shortcut + selected check to avoid ml-auto collision

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ai-chat): cover getPersistedAutonomyMode default; clarify default comment

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(raw_apps): tab-based editor surface with split-with-preview (#9273)

* feat(raw_apps): custom tab system for source / runnable / preview

Replaces the fixed split-pane layout with a tab bar inside the editor
area. Each frontend file is a tab, each selected runnable is a tab,
and the Preview is pinned to the right (non-closable). Tabs are an
alternative discoverability surface to the sidebar — both stay
functional, but tabs make navigation viable on small screens with
the sidebar collapsed.

A "Split with Preview" toggle in the tab bar's trailing slot pairs
the active tab with the preview side-by-side for wide-screen
multitasking. The toggle hides when Preview is already the active
tab.

The UI Builder, runnable editor, and preview iframe all stay mounted
across tab switches (toggled via `display`) — no bundler restarts, no
preview state loss, no editor remounts.

- New common/tabs/DraggableTabs.svelte: reusable tab strip with
  drag-reorder (@windmill-labs/svelte-dnd-action), pinned-left/right
  slots excluded from the drag zone, hover-revealed X close, middle-
  click close, keyboard navigation (arrows / Enter / Backspace),
  and a `trailing` snippet for inline toolbar add-ons.
- raw_apps/RawAppEditor.svelte:
  - Tab state (`tabs`, `activeTabId`, `splitWithPreview`) lives in
    Windmill. Persisted in localStorage keyed by workspace + app path.
  - Sidebar file clicks (`handleSelectFile`) and runnable selection
    (`selectedRunnable` via `bind:`) are mirrored into tabs via an
    effect — the sidebar interaction is otherwise untouched.
  - Listener augmented: `setActiveDocument` backfills tabs for files
    VS Code opens by itself; `setFiles` / `runnables` updates drop
    stale tabs.
  - Bundler / inspector / rebuild toolbar moves into the tab bar's
    trailing slot — always visible regardless of active tab.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(raw_apps): modern tab styling + resizable split-with-preview

Two polish passes on the new tab system:

DraggableTabs styling:
- Remove the bottom border on the tab strip + the accent-coloured
  border-b-2 on the active tab. The active tab now shares the
  surface background with the content area below it, so the
  boundary visually "disappears" — modern IDE-style tabs.
- Inactive tabs sit on the darker surface-secondary tab strip and
  get a subtle right separator so they don't blur into each other.

Split-with-Preview is now a real resizable Splitpanes:
- The content area is rendered as a Splitpanes (always), with the
  source/runnable slot on the left and the preview iframe on the
  right. The user can drag the divider to adjust the ratio when
  the "Split with Preview" toggle is on.
- Iframes never remount across single↔split toggles — pane sizes
  are driven reactively from (activeTabKind, splitWithPreview),
  not by adding/removing the Splitpanes itself.
- The user's preferred split ratio is remembered while they're
  dragging and reapplied next time split is enabled.
- The inner splitter is CSS-hidden in single mode so the toggle
  button stays the single canonical way to flip layouts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(raw_apps): split mode moves preview tab into the right pane

Cleaner mental model for split-with-preview. Instead of "split the
active tab + always keep the Preview tab around", the Split toggle
now physically moves the Preview tab out of the bar and into a
permanent right pane. When the user toggles split off, the Preview
tab reappears in the bar like any other tab.

- New `displayedTabs` derived: filters out the Preview tab when
  splitWithPreview is on, so the user sees only file/runnable tabs
  in the bar and a dedicated preview pane on the right.
- `toggleSplit` redirects the active tab to the most recent
  file/runnable when the user toggles split on with Preview active,
  so they don't end up staring at an empty left pane.
- Split toggle is now always visible — the user can flip both ways.
  The button label flips between "Pin preview to the right" and
  "Move preview back into a tab" to reflect what's about to happen.
- reorderTabs preserves the Preview tab in the underlying `tabs`
  array even though it's filtered out of the drag set in split mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(raw_apps): VS Code-style "Preview" header on the right pane

In split mode, the right pane now shows a small "Preview" tab-styled
header anchored at its top-left — making the layout read like a real
VS Code editor split, where each group has its own tab bar.

- Header appears only when `splitWithPreview && activeTabKind !== 'preview'`
  (i.e. when the right pane is meaningfully separate from the left's
  content). In single mode with preview active, the right pane is the
  only thing visible and the main tab bar already labels it.
- The header uses the same styling as an active tab: `bg-surface`
  on a `bg-surface-secondary` strip, h-8, text-xs, no border.
- An X button next to the label toggles split off — equivalent to
  closing the editor in VS Code's split view (preview goes back to
  living as a tab in the main bar).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(raw_apps): VS Code-style symmetric tab bars per pane

Restructure the editor area so each pane is a self-contained "editor
group" with its own tab bar at the top. The Splitpanes is now the
topmost element — the divider runs floor-to-ceiling, splitting both
the tab bars and the content.

Layout (left pane = source / runnable, right pane = preview):
- Left pane top: DraggableTabs (file/runnable tabs, Preview tab when
  split is off) + Split-toggle in the trailing slot.
- Right pane top: a custom preview header — "Preview" label styled
  like an active tab on the left + the preview-affecting toolbar
  (bundler, inspector, rebuild) on the right.
- Each pane independently sized via Splitpanes; iframes + the
  runnable panel stay mounted and toggled via `display` so state
  survives every transition.

Trade-off: in single-mode with Preview active (paneA=0), the left
tab bar is hidden along with the left pane. To switch back to a
file tab the user uses the sidebar — which is exactly the
discoverability surface tabs were meant to complement, not replace.

Button placement by semantic ownership:
- Layout control (Split toggle) — left side, with the editor.
- Preview-affecting controls (bundler, inspector, rebuild) — right
  side, with the preview. No close-X on the right; the Split toggle
  on the left is the canonical way to flip layouts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(raw_apps): keep tab bar visible when Preview is active in single mode

The "VS Code-style" restructure put the tab bar inside the left
Pane. When activeTabKind became 'preview' in single mode, the left
pane collapsed to width 0 and the entire tab bar disappeared with
it — leaving the user with no way to switch back to a file tab
except via the sidebar.

Move the main tab bar back above the inner Splitpanes (full width,
always visible). The preview pseudo-header stays inside the right
pane, carrying the bundler / inspector / rebuild toolbar. The
splitter only goes through the content area below the tab bar,
which is acceptable given how much friction the disappearing-tabs
edge case caused.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(raw_apps): per-pane tab bars with mirrored single-mode lists

Replace the single tab bar above the inner Splitpanes with one
DraggableTabs per pane. Splitter now goes floor-to-ceiling through
tabs AND content in split mode.

In single mode both bars mirror the full tab list, so the visible
pane always carries every tab — fixes the bug where activating
Preview hid the tab strip. Clicking Preview while in split mode is
a no-op (Preview is permanently visible in the right pane).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(raw_apps): polish tab strip and sync editor font to text-xs

* feat(raw_apps): move logs overlay onto the preview pane

* refactor(splitpanes): extract pixel-aware minSize helper

* fix(raw_apps): tab hydration loads correct file; closeTab in split mode

* fix(raw_apps): lazy-mount UI Builder iframe + add dev:ui-builder script

* feat(raw_apps): default split view, blue preview tab, fix dnd ghosting

* fix(raw_apps): remove 1px splitter sliver beside preview in single view

* fix(raw_apps): tab scrollbar on hover, fix thumb height + resize staleness

* refactor(raw_apps): don't persist tab/split layout in localStorage

* refactor(raw_apps): derive pane sizes + binding setter instead of effects

* style(raw_apps): trim verbose comments

* feat(raw_apps): accept appendLogs delta from the UI Builder iframe

* fix(raw_apps): exit inspect mode on Escape

* fix(raw_apps): Escape clears lingering inspector selection after pick

* style(raw_apps): accent-selected styling for active tab, bg-surface strip

* fix(raw_apps): address PR review nits (drop debug log, timer/reorder/pane-setter, dev script restore)

* fix(raw_apps): clear inspector overlay on the preview iframe, not the source

* style(raw_apps): neutral tab look (surface-tertiary/text-emphasis selected, text-hint idle)

* chore(raw_apps): bump bundled ui_builder to 61b6fdd

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(raw_apps): bump bundled ui_builder to b4f6219 (#9314)

* skip workspaced-route duplicate checks on cloud (#9305)

* fix(settings): skip workspaced-route duplicate checks on cloud

The pre-write validation hooks for `app_workspaced_route` and
`http_route_workspaced_route` query the DB for cross-workspace duplicates
and fail the save when any are found. On cloud both `custom_path_exists`
(apps) and `route_path_key_exists` (HTTP triggers) already scope lookups
by `workspace_id` regardless of these settings, so duplicates across
workspaces are expected and the validation has no runtime meaning. The
result was that any cloud super-admin attempting to save instance
settings with these toggles set to false received
`Duplicate HTTP route paths detected` even though the setting has no
effect on cloud routing.

Fixes WIN-1983

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(error): render JsonErr as readable text and return 400

`Error::JsonErr` previously rendered through `#[error("Error: {0:#?}")]`,
leaking Rust's `Debug` output (`Object { "error": String(...), "details":
Array [...] }`) into the HTTP response body, and was bucketed into the
catch-all 500 branch in `IntoResponse`. The result was a 500 status with
a wall of Rust debug syntax in the toast — confusing and user-hostile.

- Bucket `JsonErr` into 400 (Bad Request): every current call site
  (workspaced-route duplicate checks, OAuth client errors, etc.) is a
  client/validation issue, not an internal server fault.
- Add `format_json_err_message` which surfaces the `error` field as the
  headline, summarises `details` (with a `- key=value` per entry), and
  pretty-prints the rest as JSON for unknown shapes. The frontend toast
  now reads e.g.

      Duplicate HTTP route paths detected
      - route_path=a, workspace_id=admins, http_method=post
      - route_path=a, workspace_id=starter, http_method=post

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(toast): preserve newlines and escape HTML in multi-line errors

The toast renders via `{@html processMessage(message)}`, so server-side
error bodies that span multiple lines (e.g. the duplicate-route response
from the settings endpoint) collapsed into a single line because HTML
treats consecutive whitespace (including `\n`) as a single space.

When the message contains a newline, escape HTML first (defends against
injected markup in server error bodies) and convert `\n` to `<br />` so
multi-line errors stay readable in the toast.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fixup: address CI review feedback

- toast.ts: escape HTML unconditionally. The previous gate on `\n` left
  single-line server error bodies unsafe under {@html}, which cubic
  flagged as P0. The path regex below only inserts a `<span>` around a
  `u/...` or `f/...` capture that can't contain HTML metacharacters, so
  escaping the whole input is the simpler and correct fix.
- error.rs: add unit tests pinning the rendered shape of
  `format_json_err_message` (error+details, error-only, truncation cap,
  non-object fallback to pretty JSON).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(service-accounts): allow choosing role at creation time (#9307)

* [ee] feat(service-accounts): allow choosing role at creation time

Previously, service accounts were hardcoded to operator and could not be
used as the CLI sync user since they had no write access. They also only
counted as 0.5 seat each.

This change:
- Extends `NewServiceAccount` to accept optional `is_admin` / `operator`
  (defaults to `operator=true` for backward compatibility).
- Exposes a role picker in `AddUser.svelte` when creating a service
  account (Operator / Developer / Admin).
- Lets admins update a service account's role from the user list (it
  used to be locked to "Operator" with a tooltip).
- Updates the OpenAPI spec + regenerates the frontend client.

A developer/admin service account counts as 1 seat under the existing
seat-cap logic (operators stay at 0.5).

Companion PR on windmill-ee-private updates the `INSERT INTO usr` to
honour the chosen role.

Fixes WIN-1985

* [ee] feat(service-accounts): wm_deployers opt-in for Dev role

When creating a service account with role=Developer, surface a toggle
"Add to wm_deployers" (recommended). Members of wm_deployers can deploy
on behalf of other users — the typical setup when the service account is
used as the CLI sync / CI deploy identity.

- `NewServiceAccount` gains an optional `add_to_deployers` flag.
- Frontend defaults the toggle to on but only shows it under Developer
  (admins have it implicitly; operators can't deploy).
- Tooltip links to docs.windmill.dev "Run on behalf of".

Companion EE PR updates the handler to INSERT into usr_to_group for
wm_deployers when the flag is set.

Refs WIN-1985

* chore: update ee-repo-ref to 974ed42067d9f63acb42332b671b8c01ffd4b625

This commit updates the EE repository reference after PR #589 was merged in windmill-ee-private.

Previous ee-repo-ref: f7dbc3cc2ba21c396f4828881e3b9d9ab6f50c69

New ee-repo-ref: 974ed42067d9f63acb42332b671b8c01ffd4b625

Automated by sync-ee-ref workflow.

* [ee] fix(service-accounts): unhardcode role in superadmin user list

Two review issues from the merged #9307 / #589:

1. P1 — The global Users tab in #superadmin-settings still pinned every
   service account to "Operator". Now it shows the actual role
   (Admin / Operator / Developer), derived from the SA's usr row.

   - `list_users_as_super_admin`: replaced `true as operator_only` with
     the real `operator` value, and added `is_workspace_admin` from the
     row (NULL for password users since their admin status is
     per-workspace).
   - `global_whoami`: when the email belongs to a service account, look
     up its real `operator` / `is_admin` instead of pinning to operator.
   - `SuperadminSettingsInner.svelte`: drop the hardcoded "Operator"
     badge; render Admin / Operator / Developer using the new fields,
     matching the workspace-level view.

2. P2 — Regenerate the bundled `openapi-deref.{yaml,json}` so the
   `createServiceAccount` body (now exposing `is_admin`, `operator`,
   `add_to_deployers`) and the new `GlobalUserInfo.is_workspace_admin`
   field show up at runtime in `/api/openapi.{yaml,json}`.

Bumps `ee-repo-ref.txt` to the EE follow-up that adds the offline
seat-cap check on `create_service_account`.

Refs WIN-1985

* chore: update ee-repo-ref to b7a6068c1f3dc845e012959268b2426f0de4d697

This commit updates the EE repository reference after PR #590 was merged in windmill-ee-private.

Previous ee-repo-ref: 0b1307c21d1bfd6fb43a03c2ba39d2a8bf8e6470

New ee-repo-ref: b7a6068c1f3dc845e012959268b2426f0de4d697

Automated by sync-ee-ref workflow.

---------

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>

* fix(jobs): authorization bypass in only_result job updates (WIN-1980) (#9301)

* fix(jobs): enforce anonymous-only guard on `only_result` job updates

The `jobs_u/getupdate/{id}` and `jobs_u/getupdate_sse/{id}` endpoints
accept `only_result=true`. In that branch, `get_job_update_data` queried
the result solely by (workspace_id, job_id) and skipped the
`created_by == "anonymous"` check that the non-only_result path and
adjacent unauthenticated endpoints apply. An unauthenticated requester
who learned a private job UUID could therefore retrieve that job's
output.

Hoist the guard to the top of `get_job_update_data` so both branches are
covered.

Fixes WIN-1980

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: fold `created_by` check into existing only_result queries

Avoids the extra `SELECT created_by` round-trip per call by joining
`v2_job` once in the two queries that handled the unauth path and
checking inline. Behavior is identical to the prior commit; the SSE
polling loop now does one query per poll instead of two for
unauthenticated callers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: cache anonymous_verified across SSE polls

Replace the LEFT JOIN approach with an upfront `SELECT created_by`
guarded by a new `&mut bool anonymous_verified` parameter that mirrors
`early_return_suppressed`. The SSE polling loop now performs the auth
check exactly once per stream rather than per poll, and the data SQL
reverts to its original form so authenticated callers pay no extra
cost. `created_by` cannot change after job creation, so caching the
verification across polls is safe.

Cost matrix:
- Authed (any path): 0 extra queries
- Unauthed one-shot: 1 extra query (unavoidable)
- Unauthed SSE: 1 extra query at stream start, 0 per poll

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: scope anonymous check to only_result branch

The non-only_result branch already enforces the `created_by` check via
its main query, so a top-level hoisted check duplicated work for
unauthenticated default-path callers. Move the check inside the
`if only_result.unwrap_or(false)` block — exactly where the bypass
lives — and leave the non-only_result path untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(raw_apps): surface UI Builder build errors over the preview pane (#9316)

* feat(raw_apps): surface UI Builder build errors over the preview pane

Companion to the matching change in the UI Builder repo (see linked PR),
which stops rendering the build-error overlay over the VS Code editor
iframe and instead emits a `buildError` postMessage on every build
(message: undefined on success to clear).

Listen for that message on the existing window message handler (already
source-gated by the UI Builder iframe), store it in a `buildError`
$state, and surface it in two places:

* A red banner over the preview iframe, sibling to the existing logs
  overlay (`top-12 left-2 right-2 z-20` so it clears the tab bar) —
  failures appear right where the user looks for the rendered output.
* The Preview tab's icon and label tint red
  (`text-red-600 dark:text-red-400`, matching the existing error
  convention in raw_apps) — important in single-tab mode where the
  preview pane is collapsed to 0px and the banner would be hidden.
  Done by mapping `leftPaneTabs` / `rightPaneTabs` through a small
  `tintPreviewOnError` helper so the source-of-truth `tabs` array is
  untouched (DnD, ordering, fallback selection keep using the original
  previewTab object).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(raw_apps): use Alert component for the build-error banner

Replace the hand-rolled red div with the shared `Alert` component
(`type="error"`, `title="Build failed"`). The error text stays in a
`<pre>` child so multi-line bundler output keeps its formatting, with
`max-h-60` so a long error never takes over the whole preview pane.

The absolute-positioned wrapper (`top-12 left-2 right-2 z-20`) and the
`role="alert"` move to that wrapper so the Alert component itself stays
unstyled at the call site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(raw_apps): solid bg-surface backing behind build-error Alert

The Alert's error background is semi-transparent in dark mode
(`bg-red-900/40` in `common/alert/model.ts`), so the preview iframe
shows through when the banner is laid over it. Add a `::before`
pseudo on the Alert root with `bg-surface` (matched `rounded-md`,
`-z-10` so it sits behind the red bg) to give it a solid plate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(raw_apps): isolate banner stacking context, DRY tab tint chain

Two small follow-ups from review:

* Add `isolate` to the build-error banner wrapper so the `before:-z-10`
  pseudo's stacking context is pinned locally — it works today because
  `position: absolute` + `z-20` creates one, but `isolate` makes the
  dependency self-documenting and survives a future refactor that
  removes the explicit `z-20`.
* Extract `tintTabs = (ts) => ts.map(tintPreviewOnError)` so the two
  `$derived` blocks for leftPaneTabs / rightPaneTabs read identically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(raw_apps): trim build-error overlay comments

Per review feedback. Keep only the load-bearing facts (bg-surface backs
the Alert's translucent red, isolate pins the pseudo stacking, the
`message: undefined` clear convention) and drop the prose context that
duplicated what the code already shows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(raw_apps): bump bundled ui_builder to 00c9834

Brings in the postMessage emission from
windmill-labs/windmill-code-ui-builder#9 (merged) so this PR's host
listener actually receives `buildError` events. SHA verified against
the R2 artifact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(main): release 1.709.0 (#9312)

* chore(main): release 1.709.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>

* add cli-sync workspace snapshot/load scripts (#9322)

* feat(fixtures): add cli-sync workspace snapshot/load scripts

* fix(fixtures): address review nits (env var password, mktemp, dead refs)

* fix(fixtures): address CI review (SIGPIPE, JSON escaping, doc/code drift)

* feat(queue): stochastic admission + EE availability of workspace fairness algorithm (#9321)

* refactor: unify AI provider credentials (#9317)

* refactor: use provider credentials for worker builders

* refactor: resolve api proxy credentials directly

* fix: lazy load frontend eval modes

* fix(websocket-trigger): honor HTTPS_PROXY/HTTP_PROXY/NO_PROXY (#9324)

* feat(websocket-trigger): honor HTTPS_PROXY/HTTP_PROXY/NO_PROXY (WIN-1988)

`tokio_tungstenite::connect_async` opens a raw TCP socket and ignores
the standard outbound-proxy env vars, so deployments behind a forward
HTTP proxy can't reach the WebSocket endpoint and Test Connection
times out after 30s.

Add a small `proxy` module that resolves the right proxy URL for the
target host (HTTPS_PROXY for wss://, HTTP_PROXY for ws://, NO_PROXY
exclusions, ALL_PROXY fallback, lowercase variants), opens an HTTP
CONNECT tunnel when one applies, and hands the resulting TcpStream to
`client_async_tls_with_config` for the TLS + WS handshake. Direct
connect remains the default when no proxy env is set.

Unit tests cover NO_PROXY matching, proxy URL parsing (including IPv6
literals and basic-auth userinfo), and the CONNECT handshake itself
against an in-process fake proxy (success, basic-auth header, 407
rejection).

Fixes WIN-1988

* refactor(websocket-trigger): reduce blast radius and reuse existing logic

Follow-up to the proxy support change. Three things:

1. Skip the new code path entirely when no proxy is configured.
   `connect_async_with_proxy` now checks the env-var snapshots up front
   and delegates straight to `tokio_tungstenite::connect_async` if
   neither `HTTP_PROXY` nor `HTTPS_PROXY` is set. Same fall-through
   applies when proxy env is set but `NO_PROXY` excludes the host or
   the proxy URL doesn't parse. Non-proxied deployments now exercise
   exactly the previous code path.

2. Move the `NO_PROXY` / `HTTP_PROXY` / `HTTPS_PROXY` env-var snapshots
   from `windmill-worker::worker` into `windmill-common`. The worker's
   `PROXY_ENVS` static now reads from there, and the websocket trigger
   reads from the same source — one place reads the env, one source
   of truth for both call sites.

3. Replace the hand-rolled proxy-URL parser with `url::Url::parse`
   (already a workspace dep, used across the codebase). Half the LoC
   and handles edge cases (userinfo percent-encoding, IPv6 literals,
   path/query stripping) via the well-tested crate instead of by hand.

All 13 proxy unit tests still pass. `cargo check` is clean.

* fix(websocket-trigger): unbreak EE build + trim proxy tests

- Re-export `NO_PROXY` / `HTTP_PROXY` / `HTTPS_PROXY` from
  `windmill-worker::worker` (via `pub use windmill_common::...`) so the
  EE `otel_tracing_proxy_ee` module's `use crate::{HTTPS_PROXY, ...}`
  resolves like it did before. Fixes the `check_ee_full` / `cargo_test`
  CI failures from the previous commit.

- Trim the proxy tests to one un-ignored canary
  (`http_connect_tunnel_sends_well_formed_request_and_unwraps_stream`)
  that exercises the actual on-wire CONNECT handshake plus byte-perfect
  tunnel passthrough. The NO_PROXY-matching, URL-parsing, and edge-case
  tunnel tests are kept under `#[ignore]` for manual debugging
  (`cargo test -- --ignored`) since they're either delegated to
  `url::Url::parse` or trivial string matching — low ROI on every CI run.

* chore(main): release 1.710.0 (#9323)

* chore(main): release 1.710.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>

* fix: improve workspace fairness

* chore(main): release 1.710.1 (#9327)

* chore(main): release 1.710.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>

* prevent windows backend tests from running out of disk space (#9325)

* ignore flaky fairness regression tests in CI (#9328)

`fairness_ignores_zombie_running_rows` and
`fairness_ignores_concurrency_suspended_rows` panic intermittently in CI
(both Linux and Windows runs). Mark them `#[ignore]` until the
underlying flakiness is resolved.

* feat(cli): add object-storage commands and flow test-step (#9326)

* feat(cli): add object-storage commands and flow test-step

* docs(cli): clarify flow test-step doesn't recurse into aiagent tools

* fix(cli): correct failure step id in docs, handle bare flow.yaml path

* refactor(cli): fold flow test-step into flow preview --step (#9330)

* fix(queue): duration-weighted workspace fairness signal (#9329)

* fix(queue): bump EE ref to include worker_ping fairness signal

The current ee-repo-ref.txt pointed to 31cda7c (an unrelated merge
commit on the asset-graph-view-ee branch) instead of ddc9e80, which
contains the workspace-fairness fix that switches the active-share
signal from v2_job_queue.running=true to worker_ping. As a result
cloud was still computing overload off the legacy signal, so a
workspace with many in-flight/suspended flows (lancom01-prod, with
799 suspended flows × 3 v2_job_queue bookkeeping rows each = 2397
running-true rows) was flagged as 95% of cluster activity despite
consuming zero worker slots.

Bumping to ddc9e80 picks up the worker_ping-based signal, which
naturally excludes (a) suspended jobs (no worker pinging them),
(b) zombie running-rows from dead workers, and (c) flow/flownode
orchestration rows that never run on a worker in the first place.

* test(queue): seed v2_job rows + realistic durations for fairness helpers

The new duration-weighted fairness algorithm joins v2_job_queue and
v2_job_completed to v2_job for the `kind` filter (excluding flow
bookkeeping) and reads `duration_ms` for the completed contribution.
Update the test helpers to mirror that schema:

* `insert_completed` now inserts a matching v2_job row (kind=script)
  and writes `duration_ms = 1000` with a 1-second [started_at,
  completed_at] interval, so each completed row contributes ~1
  worker-second when fully inside the refresh window.
* `insert_queued` likewise pre-inserts v2_job, sets `started_at`
  to NOW() - 1s when running=true (so running rows contribute ~1
  worker-second by the time the refresh runs), and seeds
  v2_job_runtime.ping so the running side accrues real-time worker
  seconds (the algorithm bounds end-of-interval by ping).

The zombie/suspended insert helpers are intentionally left without
v2_job rows — the new algorithm's INNER JOIN excludes them, so they
still correctly contribute zero worker-seconds.

* chore(queue): bump EE ref to duration-weighted fairness algorithm

Companion to windmill-ee-private#<TBD>: switch the EE workspace
fairness aggregation from a count-based UNION (worker_ping snapshot
+ v2_job_completed count) to a worker-seconds aggregation sourced
directly from v2_job_queue and v2_job_completed, with kind/suspend
filters mirroring handle_zombie_jobs and per-row defenses against
zombie inflation on both halves.

* chore(queue): bump EE ref for fairness perf fix (inline window_start)

* chore(queue): bump EE ref for fairness perf rewrite (driver-side flip)

* update ee ref

* feat(hub-publish): add backend proxy routes for hub publishing

New workspaced router /api/w/:ws/hub/* forwarding to the Hub:
- POST /publish_draft → POST {HUB}/workspaces (slug/name/summary/readme)
- POST /scripts → POST {HUB}/scripts/add (workspace_slug + content)
- POST /flows | /apps | /raw_apps → corresponding hub endpoints
- POST /scripts/:ask_id/recording, /flows/:flow_id/recording → recording uploads

Auth uses HUB_DEV_TOKEN env var (dev shortcut). All bodies are
serde-typed; the helper forward_to_hub centralises the HTTP call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(deploy-to-hub): wire frontend to backend hub proxy

Replaces the mocked deploy flow with real backend calls:
- confirmBundle() POSTs /hub/publish_draft with sanitized slug,
  name, summary and readme.
- deployAll() pushes selectedItems one by one via pushItem(),
  fetching the live content (Script/Flow/AppService + raw_apps
  get_data) before forwarding to /hub/{scripts,flows,apps,raw_apps}.
- saveRecording() builds the replay-shaped payload expected by
  the Hub (initial_job + events with type: 'CompletedJob') and
  POSTs to /hub/{scripts,flows}/{hub_id}/recording.
- Adds bundleSummary state + TextInput in the drawer.

Hub item ids (ask_id / flow_id) returned by the create calls are
cached client-side to wire later recording uploads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(hub-publish): add /resources proxy route

Forward workspace resource stubs (path + type) to the hub's
/workspaces/{slug}/resources endpoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(deploy-to-hub): auto-detect resource dependencies from selection

Derive resource dependencies from the $res:/res:// references in the selected
scripts/flows/apps instead of a manual resource list, sync them as empty stubs,
and show them read-only (chip per type, hover for path + which items use it).
Aborts item publish if dependency sync fails to avoid broken fork references.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(deploy-to-hub): add project-bundle closure + path-rewrite logic

Pure, unit-tested module (projectBundle.ts) backing the "project = folder"
Hub bundle:
- extractScriptRefs / extractFlowRefs / extractAppRefs: structural detection
  of $res: references (code, static step inputs, script-by-path), hub refs
  classified separately.
- classifyPath / buildPathMap: relocate external u/.. and f/other/.. paths
  under f/<slug>/, with deterministic _2/_3 collision suffixes.
- rewriteContent / rewriteFlowValue / rewriteAppValue: rewrite every ref to
  its relocated path, leaving hub/.. untouched.
- buildProjectBundle: walk the transitive closure of a seed selection
  (scripts pulled in recursively, resources pulled as stubs), returning the
  rewritten items + resource stubs + unresolved list.

14 vitest cases cover classification, extraction, collision suffixing,
partial-match safety, deep-clone, and the closure orchestrator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(deploy-to-hub): publish as relocated project bundle + resource drawer

- deployAll now builds a self-contained project bundle (buildProjectBundle),
  pushing resource types, empty resource stubs at relocated f/<slug>/ paths,
  and the rewritten items — so a fork's references resolve inside the project.
- Resource-dependency detection is unified on the same bundle: the UI list
  (dependencyTypes) is derived from the bundle preview, guaranteeing what's
  shown matches what's pushed. Removes the duplicate in-component detection
  (extractResRefs/refsForItem/resolveResourceSet/typeForResource).
- Input-type deps (schema format: resource-<type>) are synced as types and
  conventional f/<slug>/<type> stubs alongside hardcoded ones.
- Replaces the hardcoded-path warning/fix/block machinery with a read-only
  "Resource dependencies" drawer: per-type usages tagged input vs hardcoded
  path, with an info popover explaining the portability tradeoff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(deploy-to-hub): gate hub publish endpoints + harden trigger detection

- Add ApiAuthed + require_admin to all hub publish handlers; previously any
  workspace-authenticated session could trigger Hub-side writes attributed
  to the URL workspace via the shared HUB_DEV_TOKEN.
- Track per-kind trigger fetch failures (triggerLoadErrors) so an EE-gated
  or transiently failing trigger service no longer silently maps to "0
  triggers"; UI surfaces an amber badge listing the missing kinds and a
  toast warns the operator before publish.
- Add workspaceLoadSeq cancellation so the parallel loadWorkspace +
  loadTriggers stop bleeding stale data when the workspace switches mid
  load.
- Drop the silent effectiveSlug fallback to sanitizeSlug(hubName) when
  the Hub response can't be parsed; abort the publish instead so items
  don't land under a slug the Hub never locked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(deploy-to-hub): thin /triggers proxy to forward trigger bulk-sync to Hub

Mirrors the existing /scripts, /flows, /apps thin proxies. Forwards
{ triggers, workspace_slug } to Hub's POST /workspaces/[slug]/triggers
bulk-replace endpoint, with the same require_admin + HUB_DEV_TOKEN
guardrails. Lets the frontend push trigger stubs in a single round-trip
after the items they reference have landed on the Hub.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(deploy-to-hub): push trigger stubs as the final bundle step

After scripts/flows/apps land on the Hub, pushTriggers() builds a
relocation map for the trigger paths, strips operational metadata
(workspace_id, edited_by/at, enabled, last_*/captured_*, capture data,
error_handler_path/args, permissioned_as*) from each config, resolves
script_ask_id / flow_id via the hubItemIds map produced by step 3, and
POSTs the whole set to /api/w/:wsp/hub/triggers. Triggers whose runnable
didn't publish are skipped with a warning rather than emitted as broken
stubs.

Also drops the per-kind trigger-load error surfacing: feature-gated
services (Kafka, NATS, ...) 404 on instances that don't enable them, and
the banner was lighting up on every load for nothing. Errors are
swallowed silently again, matching the pre-review behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(hub_publish): rename Hub-facing fields and URLs from workspace to project

Matches the windmillhub rename: every body now carries `project_slug`
instead of `workspace_slug`, the draft creation forwards to `/projects`,
and the resource_types/resources/triggers proxies hit
`/projects/{slug}/...`. `HubWorkspaceBody` becomes `HubProjectBody`. The
instance-side `Path(workspace)` extractor and the `workspace` URL
parameter stay because that's still the source tenant's identifier.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ui(deploy-to-hub): user-facing rename from "workspace" to "project"

The Hub-deploy surface now talks about *projects* (the bundle published
to the Hub) instead of *workspaces* (which still means the source
tenant). Tab is "Publish project", header copy mentions "project", the
Hub URL in the breadcrumb points to /projects/<slug>, payload field is
`project_slug`. Internal state names (`workspaceItems`, `workspaceStore`,
`WorkspaceService`, …) stay — they refer to the instance workspace the
items are read from, which has not been renamed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ui(deploy-to-hub): open-in-tab affordance on each dependency and trigger

Adds a small ExternalLink icon at the far right of every row in the
Resource dependencies drawer (script / flow / app / raw_app) and the
Triggers drawer (per trigger kind, opens the matching list page —
/routes, /schedules, /websocket_triggers, /kafka_triggers, …). Both
buttons open in a new tab scoped to the current $workspaceStore. Sized
to sit after the role badge so the dominant signal (input vs hardcoded
path, script vs flow) stays read first.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(deploy-to-hub): proxy raw app embed to the Hub

Add POST /w/{workspace}/hub/raw_apps/{id}/embed forwarding to the Hub so a
shared (public) raw app's external_embed_url can be set/cleared. null is
forwarded (not skipped) so unpublish clears the embed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(deploy-to-hub): bundle raw apps, share live iframe, folder-scoped bundles

- Detect modern raw apps (app table, raw_app=true) and push them to the Hub
  as raw apps: fetch source files + runnables + the compiled bundle (via the
  latest-version bundle secret) and shape them into the raw payload RawAppView
  expects. Fail loudly when no compiled bundle exists.
- Capture the Hub id for raw apps and wire "Share as iframe"/"Unpublish" for
  them (post-bundle, like recordings); re-sync the embed on re-bundle for
  already-public apps. Factor the publish/unpublish flow into setAppShared +
  pushRawAppEmbed helpers.
- Scope bundles to a single required f/<folder>/ (Select instead of MultiSelect)
  so relocated paths stay predictable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(hub): send item path when publishing a project to the hub

Include each item's newPath in the script/flow/app/raw_app publish payloads so
the hub can store the relocated Windmill path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(hub): accept path on publish and proxy project export

Add an optional path field to the publish bodies and a GET
/projects/{slug}/export route that proxies the hub export (admin-only,
authenticated with HUB_DEV_TOKEN).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(projects): add project install page

New /projects/install page pulls a hub project's export and re-creates it in
the selected workspace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(deploy-to-hub): let user pick target folder on project import

Add a FolderPicker to the project install page (defaulting to the
project slug, with create-new-folder support) and retarget every
`f/<slug>/` prefix in the bundle — item paths, $res:/script refs,
schedule runnable paths — to the chosen folder in one pass. Ensures
the target folder exists before creating items.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix wording

* fix(hub-publish): bind Hub publish/export to the trusted workspace via source_id

Hub publish endpoints ignored the {workspace} path and addressed the Hub
project purely by client-supplied project_slug, forwarding with an
instance-wide HUB_DEV_TOKEN. Any workspace admin could mutate or export
another workspace's Hub project by passing its slug.

Stamp the server-trusted workspace from the path onto every forwarded
request as source_id (body for mutations, query param for export) so the
Hub can enforce that the targeted project belongs to the calling
workspace. Requires the matching Hub-side source_id ownership check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(deploy-to-hub): reset draft/publish state on workspace switch

The workspace-switch effect only reset load-derived state, so phase,
draftItems, recordings, hub/bundle metadata, hubVersion, deploymentStatus,
effectiveSlug and hubItemIds survived a switch — a draft built in one
workspace could publish its items/slug under the next workspace's auth.
Reset the full publish session on switch. Also drop explanatory comments.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(deploy-to-hub): pull sub-flows referenced by type: flow steps into the bundle

extractFlowRefs only emitted refs for type: script steps, so a flow calling
an external sub-flow by path was never followed and the published project
was silently incomplete. Add a 'flow' RefKind, emit it for type: flow steps,
recurse on it in buildProjectBundle, and rewrite its path on relocation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(deploy-to-hub): fall back to slug when import folder is whitespace-only

(folderName || slug).trim() let a whitespace-only folder bypass the slug
fallback and trim to an empty target, producing invalid f//... paths and a
failed import. Trim first, then fall back.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(hub-publish): validate project slug before interpolating into Hub path

slug/project_slug are caller-controlled and were interpolated straight into
the Hub request path; a crafted value (e.g. ../../admin) could reach an
unintended Hub endpoint after URL normalization. Validate against the
frontend charset (lowercase alphanumerics + hyphens, 3-50 chars) in the four
handlers that put the slug in the path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(deploy-to-hub): use ?tab query param to link to the Apps settings tab

The "Edit in Workspace settings → Apps" link set window.location.hash, but
the settings page derives the active tab from ?tab=..., so the link was a
dead affordance. Navigate with goto('?tab=default_app') instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(deploy-to-hub): point Open in Hub link at the project slug, not the workspace

hubSlug was derived from $workspaceStore, so the Open in Hub link and badge
used the workspace id instead of the published project slug — navigating to
the wrong (or nonexistent) Hub project. Derive hubSlug from the actual
project slug (effectiveSlug, falling back to sanitizeSlug(hubName)).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Sync hub to instance

* feat(deploy-to-hub): rehydrate project state, wire review flow, bundle trigger resources

- Rehydrate the publish panel from the Hub by source_id on load (phase, slug,
  metadata, items, hub ids, recordings) so refresh no longer loses the draft.
- Map Hub project status to the draft/under_review/live phase; submitForReview
  now persists to the Hub instead of a local stub; drop the unused v{n} version
  display (status is the source of truth).
- Send source_path (original workspace path) per item for recording round-trip.
- Detect resources referenced by triggers, add them to the bundle closure
  (extraResourcePaths) so they appear in dependencies, get stubbed/relocated,
  and rewrite the trigger config path via the full bundle pathMap (no leaked
  private path); show trigger usages in the dependency drawer.
- Review fixes: Array.isArray guards on trigger topic/subject lists; snapshot
  relevantTriggers in deployAll to avoid a mid-deploy folder-switch race;
  index-key the usage list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(deploy-to-hub): keep item summary on rehydrated draft, drop placeholder diff button

Rehydrated draft items now carry their summary (from the Hub) so step 2 shows
the summary like step 1 instead of falling back to the path. Remove the
"Diff vs submitted" button: it only toasted add/remove counts with no view,
which read as broken.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(deploy-to-hub): New draft returns to the folder-picker step instead of erroring

In the live phase the folder picker is hidden, so startNewDraft's
selectedFolder guard always failed with "Pick a folder..." and the user had
no way to pick one. Now New draft goes back to step 1 (predeploy) with the
project's folder pre-selected (inferred from the item paths) so the user can
re-bundle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(hub_publish): return 500 not 400 when HUB_DEV_TOKEN missing

* fix(deploy-to-hub): keep internal subfolder paths identity-mapped when bundling

* fix(projects-install): never overwrite existing resources; isolate invalid raw app json

* fix(deploy-to-hub): route raw_app to apps_raw/get and guard openRecord schema race

* refactor(hub_publish): extract hub_token helper, drop duplicated env lookup

* refactor(projects-install): route raw-app and unsupported-trigger failures through record()

* fix(deploy-to-hub): refresh review status from Hub and use configured hub base url

* fix(hub_publish): return 400 not 500 when HUB_DEV_TOKEN is unset

Missing config is a client/config error, not a server fault. Restores the
BadRequest class lost when hub_token() was extracted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(hub_publish): scope Hub projects per folder via workspace:folder source key

* feat(deploy-to-hub): publish per-folder projects from the Folders page

* ui(deploy-to-hub): move phase CTA to the top-right header

* Fable review

* feat(hub_publish): forward the caller's token to the Hub instead of HUB_DEV_TOKEN

* style(windmill-api): cargo fmt fallout in build.rs and lib.rs

* Nit fixes

* Nit fix

* fix: structural project-ref rewrite and deploy-to-hub state fixes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: deterministic draft phase fallback when post-deploy rehydrate fails

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): skip EE-only native trigger calls on CE to avoid console 404s

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(deploy-to-hub): fork all trigger kinds on project install

The project install (fork) flow recreated only schedule triggers and
rejected every other kind with "not supported yet". Recreate all trigger
kinds instead, imported disabled (enabled: false → mode disabled).

Kafka, NATS, SQS, GCP and Azure require an Enterprise license, so they are
gated behind enterpriseLicense and reported as "requires Enterprise" on CE
rather than firing backend calls that 404. http, websocket, postgres, mqtt
and email are recreated on CE. The kind-specific config (with retargeted
resource paths) is spread into the create body; explicit path/script_path/
is_flow/summary/enabled win over it. Also carry the schedule summary through.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(deploy-to-hub): list CE trigger kinds without an Enterprise license

loadTriggers wrapped http, websocket, postgres, mqtt and email list calls
in eeList, so on CE (no enterpriseLicense) they resolved to [] and never
made it into deploy state — those triggers silently disappeared from the
Hub publish set. Only Kafka, NATS, SQS, GCP and Azure are EE; switch the CE
kinds back to safeList so they are always listed and published. Mirrors the
EE gating used on the project install (fork) side.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(deploy-to-hub): reload triggers when EE license hydrates late

loadTriggers captures enterpriseLicense at call time and the main reload
$effect only depends on workspace/folder, guarded by lastLoadedKey. When the
license store hydrates asynchronously after loadTriggers already ran, the EE
trigger kinds (kafka/nats/sqs/gcp/azure) stay empty until the workspace or
folder changes. Add a dedicated $effect that re-fetches triggers on the
license false→true transition, mirroring the sidebar's license-race handling.
prevHadLicense is seeded from the current value so a license already present
at mount doesn't trigger a redundant reload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(deploy-to-hub): token loadTriggers so a late EE reload can't be clobbered

The license-late reload calls loadTriggers with the same workspaceLoadSeq as
the original license-less load, so the workspace guard alone lets both assign
workspaceTriggers. If the earlier (EE-empty) request resolves last, it
overwrites the newer license-aware result and the EE trigger kinds disappear
again. Add a per-invocation triggerLoadSeq token and only let the latest load
assign (and toggle triggersLoading), so a slow earlier request is discarded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(deploy-to-hub): use mode 'disabled' for forked non-schedule triggers

Non-schedule triggers expose `mode` (TriggerMode), not the deprecated
`enabled` flag, in their create body. `enabled: false` happens to still map
to disabled today via the backend's legacy BaseTriggerData field, but relying
on a deprecated path is fragile. Set `mode: 'disabled'` explicitly so imported
http/websocket/postgres/mqtt/native triggers stay disabled. Schedules keep
`enabled: false` (NewSchedule uses the enabled flag).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(deploy-to-hub): snapshot workspace + key bundle by kind:path

Address three P1 review findings:

- Project install (fork) read the reactive `workspace` ($derived) across many
  sequential awaits, so a workspace switch mid-import could create the folder in
  one workspace and later items in another. Snapshot the target workspace once at
  the top of install().
- DeployToHub.deployAll re-read $workspaceStore after confirmBundle had already
  created the Hub draft bound to a specific workspace's source_id, so a switch
  during draft creation could publish items to a different workspace. Pass the
  workspace captured by confirmBundle into deployAll instead.
- buildProjectBundle keyed its fetched/queued maps by bare path, silently
  dropping one of two distinct-kind items at the same path (script vs flow). Key
  by `${kind}:${path}` and derive item paths from the fetched values, keeping
  path relocation separate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(deploy-to-hub): condense comments

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(deploy-to-hub): surface backend error body on failed project import

record() only showed `e.message`, which for API errors is the generic status
text ("Bad Request"). Prefer the ApiError `.body` (plain-text reason for
Windmill 4xx) so a failed import reports the actual cause — e.g. a path or
route_path collision — instead of a bare "Bad Request".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(deploy-to-hub): close mid-request workspace-switch races

Two follow-ups to the workspace snapshotting:

- confirmBundle captured `workspace` but read selectedItems / relevantTriggers
  / hubSlug only inside deployAll, after the publish_draft await. A workspace
  switch during that request resets those to the new workspace, so deployAll
  would push the new workspace's items into the old workspace's Hub draft.
  Capture workspaceLoadSeq before the request and abort (with a toast) if it
  changed before publishing.
- install() snapshotted `workspace` but still read the reactive `data` after
  the createFolder await; load() can replace `data` on a workspace switch, so
  retarget() could run against a different export than `folder` was derived
  from. Snapshot `data` up-front and use it throughout install().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(deploy-to-hub): guard stale load response and mid-publish status writes

- install load() assigned `data`/`folderName` unconditionally, so a slow
  /export for an old ?hub= could overwrite a newer project after navigation.
  Add a load token + captured slug/workspace and only assign if still current.
- deployAll wrote deploymentStatus/hubItemIds incrementally and only checked
  the workspace at the very end. Bail at the top of the per-item loop when the
  active workspace changed, so a mid-publish switch can't keep writing the old
  workspace's item statuses and Hub IDs into the new workspace's live view.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(hub-projects): generate and apply datatable migrations on project publish/install (#9977)

* feat: add datatable_migrations table

* feat: add route to run datatable migrations

* feat: sync datatable migrations as .up.sql/.down.sql files

* feat: add datatable migrate up/down commands and post-push run prompt

* feat: add datatable migrate new command to scaffold migrations

* feat: add datatable migrations management UI

* feat: prompt to create migration on DDL in datatable SQL editors

* feat: support running a single specific datatable migration

* feat: view migration content, run single migration, fix stacked modal

* feat: per-row revert button with out-of-order warning

* fix: avoid migrations list flicker on refresh after an action

* feat: generate initial datatable migration via pg_dump

* fix: surface datatable migration API error details in toasts

* fix: revert created migration if create-and-run fails to run

* fix: include postgres error detail in migration run/rollback failures

* feat: sync datatable migrations as files via the workspace export

* refactor: move datatable migrations to migrations/datatable/ path

* fix: drop redundant datatable_migration label in sync output

* fix: exclude datatable migration sql files from script metadata generation

* feat: run datatable migrations as user-permissioned labeled jobs

* feat: reject invalid datatable migrations on sync push

* feat: datatable migrate up/down default to all datatables, --datatable to target one

* fix: surface postgres error detail when datatable migrations fail to run

* chore: regenerate CLI docs for datatable migrate commands

* feat: default new datatable migration to a BEGIN/END transaction template

* fix: validate datatable migration name and datatable at the API boundary

* fix: ensure detected DDL ends with semicolon when wrapped in transaction

* fix: re-prompt instead of stripping DDL when new-migration modal is cancelled

* feat: refresh datatable schema after running a migration from the SQL REPL

* feat: record db manager DDL on data tables as migrations

* feat: make datatable migrations opt-in per data table

* fix: make migration view editor read-only so its code can scroll

* fix: don't re-prompt DDL guard when creating a migration without running

* feat: generate down migrations for db manager DDL (postgres)

* fix: correct down migration for db manager alters (no double-wrap, serial)

* feat: explain migrations purpose with a tooltip in the migrations modal

* compare paeg

* feat: add datatable_migration kind to workspace diff pipeline

* chore: point ee-repo-ref at datatable_migration git-sync companion

* fix: harden datatable migration version allocation and initial-migration bookkeeping, add tests

* feat: deploy and run datatable migrations on workspace merge

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Refactor + handle datatable setting delete/rename

* refactor: move datatable migration rename/delete cascade into module

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(windmill-utils-internal): bump to 1.7.1 for datatable migration deploy provider methods

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(db-manager): add Migrations button to top bar, make Refresh icon-only

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* BEGIN/END placeholder in down migration

* feat: autofocus migration name input and flag it red when empty

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(datatable-migrations): allow non-admins to create/run/revert migrations, gate only opt in/out

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* border nits

* refresh db manager schema on migrations

* BEGIN/END scaffold in CLI

* feat(cli): push local datatable migrations before running on migrate up

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: flag invalid migration name with red border, not just empty

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor: drop random slug from auto-generated migration names

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: offer revert-and-delete when deleting an installed migration

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: record fork merge as a migration when target datatable opts in

* nit

* clone migrations on fork

* windmill-utils-internal

* fix(datatable-migrations): serialize run/rollback with a per-db advisory lock

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(db-manager): fail closed when migrations-status check errors on DDL apply

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: fix generate_initial migration ordering comment to match code

* chore(datatable-migrations): remove unused update_datatable_migrations endpoint

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: run DDL migration guard on the script editor Test button

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* split

* ee-repo-ref

* chore(frontend): sync package-lock with package.json (@emnapi deps)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(datatable-migrations): never resolve instance credentials into migration job args

datatable_database_arg eagerly resolved instance data-table credentials
(including the shared instance-wide Postgres password) and passed them as the
migration job's plaintext `database` arg, landing in v2_job.args. Since the
run route has no admin gate, a non-admin could run a migration and read
args.database to recover the password, granting cross-workspace psql access to
all instance data-table DBs.

Pass a `datatable://<name>` reference for both resource-backed and instance
data tables instead; the pg executor already resolves it to real credentials
server-side at run time, so nothing sensitive is ever stored in the job args.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* nit

* fix: handle dollar-quoting and comments when splitting SQL statements

* feat: deploy datatable migrations on merge with explicit opt-in error

* fix(frontend): sync package-lock with npm 11 peer-dep resolution

npm ci failed with 'Missing: @emnapi/core@1.11.2 / @emnapi/runtime@1.11.2 from
lock file'. @napi-rs/wasm-runtime declares @emnapi/core|runtime ^1.7.1 as
peerDependencies while @rolldown/binding-wasm32-wasi pins them to exactly
1.10.0. Newer npm (bundled with node 24 in CI) installs the peer deps at the
highest match (1.11.2) alongside rolldown's nested 1.10.0, so the ideal tree
needs both versions; the committed lock only had 1.10.0.

Regenerate the lock with npm 11.18 so it carries both 1.11.2 (top-level, for
the peer deps) and 1.10.0 (nested, for rolldown's pin). Verified npm ci passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* nit npm publish

* fix: fail closed on migrations-status error in fork schema merge

* nit CI emnapi/core version

* prevent initial_datatable_migration if migrations already exist

* fix(datatable-migrations): validate persisted data table names as path segments

edit_datatable_config only validated rename segments, not the actual
settings.datatables keys, so a data table could be saved directly under a name
like '..' or one containing '/'. Since new tables default to
migrations_enabled = true, generate_initial_datatable_migration would then
insert a migration row and the sync export would build
migrations/datatable/<name>/... paths from that name, producing malformed or
directory-escaping export paths.

Validate every persisted data table name in edit_datatable_config (alongside
the existing rename checks) and add validate_datatable_path_segment to
generate_initial_datatable_migration for defense in depth.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: scope datatable _wm_migrations by data table and cascade renames/deletes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(system_prompts): resolve nested local command groups in CLI docs generator

The CLI docs generator anchored on the first `new Command()` in a file and
never resolved locally-defined command groups passed as
`.command("name", localCmd)`. For datatable this flattened the nested
`migrate` group: it emitted `datatable new/up/down` plus a bare
`datatable migrate`, and mislabeled the datatable command with the migrate
group's description. jobs was broken the same way (its description was pull's,
and pull/push rendered empty).

Anchor block extraction on the `export default`ed command, recurse into
locally-defined `const x = new Command()` groups mounted as subcommands, and
render nested sub-subcommands. Regenerated docs now show
`datatable migrate new/up/down` and `jobs pull/push` with their real
options.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor: drop unreleased _wm_migrations legacy-upgrade handling

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: return datatable migration SQL from getItemValue for the diff drawer

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(frontend): use windmill-utils-internal 1.8.2 for migration diff drawer

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* nit

* nit

* fix: handle datatable migration renames on push and dedupe timestamps

* fix: reject rewriting an already-applied datatable migration on upsert

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): add missing @emnapi/core and @emnapi/runtime lockfile entries

Resolves npm ci EUSAGE failure: the optional cpu:wasm32 @rolldown/binding-wasm32-wasi
declares deps on @emnapi/core@1.11.2 and @emnapi/runtime@1.11.2 that had no resolved
lockfile entries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cli): datatable migrate up/down default to main datatable, not all

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: fail closed when applied status unreadable on datatable migration rewrite

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: surface full error detail in Database Manager DDL/query errors

* "See migration" button in the toast

* feat: add Enter shortcut to Create-a-migration in the DDL guard

* fix(frontend): warn before running a newly-created datatable migration out of order

The row-level Run action warns when earlier migrations are still pending, but
the create-and-run paths ran a just-created migration with `only` directly,
applying it ahead of older pending migrations without that confirmation.

Reuse the same "Run migration out of order" confirmation across all
create-and-run paths via a shared helper (datatableMigrationUtils):
- NewDataTableMigrationModal "Create and run" (and the DDL guard path)
- DatatableSchemaDiff fork→parent merge
- dbOps schema ops (DB manager create/alter/drop) — the pure factory throws a
  MigrationRunCancelled sentinel on decline, which DBTableEditor treats as a
  silent cancel

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: keep renamed datatable migrations visible in compare view

* fix: record per-migration deployment on datatable migrations disable

* fix(cli): run deployed datatable migrations after workspace merge

The merge command upserted datatable_migration definitions into the target
workspace and reported the item as successfully deployed, but never ran the
migrations. For forked datatables backed by separate databases, this left the
target schema unchanged until someone manually ran `wmill datatable migrate up`,
while the CLI reported a successful merge.

Collect the datatable migrations deployed (not deleted) into the target and,
after the deploy loop, offer to run them via the existing offerToRunNewMigrations
helper — the same post-deploy run prompt the push/sync path uses (interactive
only; `--yes`/non-TTY skip the mutating run, matching push behavior). Export
parseDatatableMigrationDeployPath so the merge path can parse the deployed items.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(backend): serialize datatable migration edits/deletes with the run lock

A migration run snapshots a migration's code_up from datatable_migrations and
only records its version in the data table's _wm_migrations after the job
succeeds. upsert_datatable_migration checked _wm_migrations before allowing an
edit but took no lock, so a concurrent edit could read "not applied yet",
rewrite code_up/code_down, and then the in-flight run would record the version
for the old SQL — leaving _wm_migrations pointing at SQL that was never applied
(migrate up then skips it; rollback runs a down that doesn't match).

Serialize definition rewrites and deletes with the same per-database advisory
lock the run/rollback paths use:
- Factor the connect+advisory-lock into lock_datatable_migration_runs and the
  applied-versions read into read_applied_versions_on_client.
- run_datatable_migrations now snapshots the definitions AFTER taking the lock,
  so code_up can't change between snapshot and version-record.
- upsert (when changing an existing def) and delete take the lock across the
  applied-check and the write; delete now rejects deleting an already-applied
  migration (would orphan its _wm_migrations record), symmetric with upsert.
  Both fail closed if the data table database is unreachable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): stack the out-of-order migration confirm above the DB editor preview

Creating a table on a migrations-enabled data table opened the DB table editor's
"Confirm running the following" preview modal, whose confirm triggers applyDdl,
which then asks for out-of-order confirmation. Both are ConfirmationModals with a
hardcoded z-[9999]; the out-of-order one lives in DBManagerContent (mounted before
the editor), so it rendered behind the still-open preview modal.

Add an optional zIndexClass prop to ConfirmationModal (default z-[9999],
backward-compatible) and give the DB-manager out-of-order confirm z-[10000] so it
stacks on top.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(hub-projects): generate and apply datatable migrations for projects

Detect datatable assets in a project's scripts/flows/raw apps when
publishing to the Hub, generate a best-effort CREATE TABLE migration per
data table from the source workspace's live schema, and let the publisher
edit/toggle them in the bundle drawer. On import, offer to run the shipped
migrations: recorded (datatable_migrations + _wm_migrations) when the
target data table opted into migrations, otherwise as a one-off preview
job. Missing target data tables are surfaced and skipped.

- backend: POST /hub/migrations proxy forwarding to the Hub
- frontend publish: projectMigrations.ts detection + generation, new
  "Data table migrations" section in DeployToHub
- frontend import: run/skip modal + missing-datatable confirmation
- extract pure SQL-gen from DatatableSchemaDiff.svelte into
  datatableSchemaSql.ts so plain .ts modules can import it

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(hub-projects): close datatable migration table set over foreign keys

Pull a referenced table's FK targets into the generated migration
transitively, so it creates every table it references (ordered by FK
dependency), and drop any FK whose target still isn't in the set so the
generated SQL always runs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(hub-projects): show Data table dependencies in the publish view

Detect data table usage off the predeploy bundle preview and surface it as
a "Data table dependencies" summary right after "Resource dependencies",
mirroring how resource types and triggers are shown. The editable
migration itself stays in the bundle drawer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(hub-projects): explain un-generated migrations with SQL comments

When a table can't be found in the schema, a data table is referenced as a
whole, or the schema can't be loaded, write a `--` comment describing the
problem into the migration instead of leaving it blank. Partial migrations
keep the CREATE TABLEs that did generate and comment the rest; comment-only
migrations stay disabled. The bundle drawer now always shows the SQL box so
those comments are visible and editable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(hub-projects): review/edit migrations on import + rollback down migration

Replace the plain "run migrations?" confirmation with a review drawer that
previews each runnable migration, lets the user edit the SQL and toggle
which to run, before the import proceeds. When recording an imported
migration, also record a down migration (DROP TABLE of the created tables,
in reverse order) derived from the up SQL, so it can be rolled back; the
derived rollback is previewed in both the publish bundle drawer and the
import review drawer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* nit

* feat(hub-projects): editable Up/Down Monaco editor for migrations

Replace the plain textarea with a Monaco SQL editor split into Up/Down
tabs. The down migration is now generated once as best-effort (DROP TABLE
in reverse creation order) and is fully editable — no longer parsed back
out of the up SQL. The down is threaded through publish → Hub → import
(new project_migration.sql_down) and recorded as code_down when an imported
migration is applied.

- projectMigrations: GeneratedMigration.sql_down generated from the table set
- MigrationSqlEditor.svelte: shared Up/Down tabbed Monaco editor (re-keyed on
  regeneration since Monaco ignores external code changes)
- DeployToHub + install review drawer use it; sql_down pushed/applied
- backend: PublishMigrationBody carries sql_down

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(hub-projects): generate CREATE TABLE IF NOT EXISTS for project migrations

The FK closure pulls a referenced table's parents into the same transaction
(e.g. `orders` drags in `customers`); those shared parents often already
exist in the target, so a plain CREATE TABLE aborted the whole migration on
the first collision. Emit CREATE TABLE IF NOT EXISTS for project migrations
(via a new opt-in flag on generateMigrationSql, leaving the schema-diff
behavior unchanged) so a pre-existing parent is skipped instead of failing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(hub-projects): key FK ordering by schema-qualified table name

orderByFkDependency keyed its dependency graph by bare table name (and
resolved FK targets with .split('.').pop()), so two same-named tables in
different schemas collapsed and one was dropped from the ordered set and
never created. Key by schema.table like the rest of the pipeline, resolving
FK targets through resolveTable. Also let resolveTable fall back to the bare
table name when a schema-qualified ref's schema doesn't match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(hub-projects): comment out generated down-migration DROP statements

The generated down migration listed DROP TABLE for every table in the FK
closure, including shared parent tables that may have pre-existed in the
target — a rollback could drop a table the project never created (data
loss). Emit all DROP statements commented out with a note, so nothing is
dropped by default; the publisher uncomments the tables this migration
actually owns.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(hub-projects): disable Import button during migration review

planMigrations awaits the review / missing-datatable modals before setting
installing = true, so the Import button stayed enabled during review and a
second click launched a concurrent install() (second review drawer,
duplicated item creation). Track a planningMigrations flag, disable the
button on it, and early-return install() if already installing or planning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(hub-projects): toast when migration generation fails

regenerateMigrations cleared the drafts on error, showing "No data table
usage detected" — indistinguishable from a genuine schema-load failure. Add
a toast on the catch so the publisher can tell the two apart.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(hub-projects): honor cancel on the missing-data-table warning

planMigrations awaited missingDatatableModal.ask() but ignored its boolean,
so cancelling the "some data tables are missing" warning still proceeded
with the import — the cancel affordance did nothing. Show the warning first
and abort the whole import when the user cancels (planMigrations returns
null; install() early-returns), so they can create the data table(s) and
re-run. Confirming still imports without the missing migrations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(hub-projects): detect data tables from low-code app DB-table config

Low-code apps don't carry a persisted asset list, but the DB-table
component declares its data table and table explicitly: a `oneOf` `type`
config with `selected === 'datatable'` holding `datatable://<name>` and the
table. Walk the app value for those configs so an app that reads a data
table is picked up by the Data table dependencies detection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Revert "feat(hub-projects): detect data tables from low-code app DB-table config"

This reverts commit 9c43ebd512.

* fix(hub-projects): detect data tables from full-code apps' declaration

Full-code (raw) apps explicitly declare the data tables/tables they use in
value.data.tables (refs like main/customers or main/schema:table), which the
"Data table dependencies" detection missed — it only looked at inline-script
assets. Read the declaration via extractDataConfig/parseDataTableRef. The
bundler previously dropped value.data (kept only files + runnables); include
it so detection sees it and the imported app keeps its declaration, and pass
it through on import.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(hub-projects): recompute app policy on project import

Apps imported from a Hub project were created with an empty triggerables_v2
policy, so running any inline component script failed at runtime with
"Path rawscript/<sha> forbidden by policy". The policy is computed client-side
on deploy and stored verbatim by the backend, and import skipped that step;
retargeting also rewrites inline-script content (changing its sha), so a copied
policy would not match either.

Recompute the policy from the retargeted value at import, mirroring the deploy
path: updatePolicy for grid apps, updateRawAppPolicy for raw apps, defaulting
execution_mode to publisher.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* nit fix

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(hub-projects): retarget plain trigger resource paths on import

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(hub-projects): reset migration drafts on workspace/folder switch

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(hub-projects): bundle http auth resources, pin drafts during deploy

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(hub-projects): make generated data table migrations idempotent

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Reapply "feat(hub-projects): detect data tables from low-code app DB-table config"

This reverts commit 112844deea.

* fix(hub-projects): create all tables before FK constraints in migrations

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(hub-projects): reset install state when the hub slug or workspace changes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Revert "Reapply "feat(hub-projects): detect data tables from low-code app DB-table config""

This reverts commit 14abefb4f6.

* fix: dedupe args state duplicated by main merge in AssetGraphDetailsPane

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(deploy-to-hub): extract session class keyed by workspace+folder

All DeployToHub state and async operations move into DeployToHubSession
(deployToHubSession.svelte.ts), an immutable-(workspace, folder) state class.
A workspace/folder change replaces the instance and remounts the UI via
{#key} instead of manually resetting ~20 state vars, and in-flight async
work writes to the discarded object instead of racing the new scope. The
workspace-scoped seq counters (workspaceLoadSeq/triggerLoadSeq for
lifecycle, migrationsSeq) collapse into a dispose flag plus intra-session
tokens only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqqWYQR46tcunvPVRfidZS

* refactor(triggers): single shared module for all-kind workspace trigger listing

TRIGGER_KINDS (badge/route/note/resourceField/eeOnly + list call),
listAllWorkspaceTriggers, triggerResourcePath, stripTriggerConfig and
triggerDetails move to $lib/components/triggers/workspaceTriggersList.ts, so
EE-license gating per trigger kind is declared once instead of being re-decided
at each call site. DeployToHubSession consumes it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqqWYQR46tcunvPVRfidZS

* refactor(hub-publish): route every endpoint through one validation choke point

HubPublishCtx (a FromRequestParts extractor) is now the only way a handler
reaches the Hub: it performs the admin check, resolves and validates the
workspace:folder source key, and carries the forwarded token — a new endpoint
cannot skip any of it. Project slugs become a ProjectSlug newtype whose only
constructor is validating deserialization (body field or path segment), so
every slug that reaches a Hub URL or payload is valid by construction; the
previously unvalidated slugs in publish_draft/scripts/flows/apps/raw_apps/
embed/recording bodies are now checked too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6

* refactor(hub-projects): shared bundle format module + per-kind project installer

The Hub export format (types + retargetProjectExport/buildRetargetMap) moves
into projectBundle.ts so publish and install share one definition, with unit
tests for retargeting. projectInstall.ts owns the import: one importer per
item kind with per-item error capture, and trigger creation goes through
createWorkspaceTriggerDisabled in the shared trigger module, which encodes
the per-kind disable semantics (schedules use enabled:false, everything else
mode:'disabled') and EE gating once. The install page shrinks to
orchestration and UI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6

* fix(hub-projects): AMQP kind, trigger handler bundling, import containment

Review-round fixes: register the AMQP trigger kind (CE) in the shared
registry so it lists/bundles/imports like every other kind; stop stripping
error_handler_path/args from trigger configs and bundle + relocate handler
runnables (including schedules' script|flow-prefixed on_* refs) with the
project; resolve full schedule rows on listing (listSchedules is slim) and
spread the exported config on import so cron_version, retry, handlers and
no_flow_overlap survive; refuse per-item any export path that escapes the
selected f/<folder>/ target; and gate the install page's results/done
writes on the load sequence so a stale import can't mark a newly loaded
project as imported.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6

* fix(hub-projects): schedule config hygiene and complete handler bundling

Strip email/is_draft/paused_until from exported trigger configs (the full
schedule row carries owner and runtime state that must not reach the Hub);
bundle and relocate dynamic_skip handler scripts (schedule creation refuses a
missing one, so an unrelocated path breaks the import); exclude and report a
schedule whose detail fetch fails instead of silently exporting the slim row
with default behavior; and seed migration detection with the same
handler-augmented item set as deployment so data tables used only by bundled
handlers get their migrations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6

* fix(deploy-to-hub): single guarded publish path, gated on trigger load

publishBundle() owns draft creation + deployment under one synchronously-set
deploying flag, so a double-click can't start two interleaved publishes, and
it refuses to run while triggers are still loading — snapshotting an
incomplete relevantTriggers list would permanently omit triggers, their
handlers and handler-only migrations from the draft. The bundle CTAs disable
while trigger discovery is in flight.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6

* fix(hub-projects): block publish on failed trigger discovery, strict schema-qualified table resolution

listAllWorkspaceTriggers now distinguishes a feature-gated 404 (kind not
compiled into the instance — legitimately empty) from a real listing or
detail-fetch failure: failures are surfaced, recorded per kind, and the
session blocks publishing with a visible retry until discovery completes
cleanly, so an incomplete trigger snapshot can't be bundled silently.
resolveTable no longer falls back to a same-named table in another schema
when a qualified ref misses — that generated a migration for an unrelated
table; the miss now produces the existing commented warning instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6

* docs(openapi): document the 15 hub publish proxy routes

All /w/{workspace}/hub endpoints (draft/items/recordings/resource
types/resources/triggers/migrations/export/submit/by-source) enter the
public API contract with their body schemas derived from the serde structs,
a shared HubProjectSlug schema encoding the slug validation, and passthrough
text responses matching the proxy behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6

* fix(hub-projects): bundle $res refs nested in trigger configs, nullable trigger payload fields

Trigger dependency collection now scans the full stripped config for
$res:/res:// tokens (schedule args, on_*_extra_args, error_handler_args —
e.g. the built-in Slack handler's channel resource) in addition to the
kind-specific resource field, so those resources enter the bundle path map,
get relocated by rewriteTriggerConfig, export a typed stub, and show up in
the dependency pane. PublishTriggerBody's summary/description/
script_ask_id/flow_id become nullable in the OpenAPI contract, matching
what the publisher actually sends and the Rust Options accept.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6

* fix(hub-projects): flow preprocessor/env refs, no cloud provisioning on import, config containment

Flow extraction and rewriting now cover preprocessor_module (walked like any
other module) and flow_env $res: values, so those dependencies are bundled
and relocated instead of keeping source-workspace paths. GCP/Azure triggers
are refused at import with an actionable message — their create endpoints
manage cloud subscriptions before storing the trigger, even disabled, so
auto-creating them from an import could mutate external infrastructure. The
import containment guard now also validates everything a trigger config
binds to (kind resource field, handler runnables incl. hub/ refs, nested
$res: tokens), closing the path where a crafted export binds a trigger to
assets outside the chosen folder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6

* refactor(hub-projects): per-kind config allowlists from a full trigger-field audit

Every trigger kind's boundary-crossing config is now an explicit per-kind
allowlist (configFields in TRIGGER_KINDS), derived from a field-by-field
audit of every create type: portableTriggerConfig replaces the blocklist
and is applied on export AND import, so an upstream field addition is
dropped until consciously admitted (no more email-style leaks) and a
crafted export can't inject fields like permissioned_as into create calls.
The audit also surfaced unbundled websocket runnables — $script:/$flow:
URLs and initial-message runnable_result paths are now collected and
relocated — and drops GCP/Azure provisioned identities (subscription ids,
delivery_config with the source instance's endpoint) from exports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6

* fix(hub-projects): bundle $res refs nested in JSON flow_env values

The worker resolves $res: references inside nested JSON flow_env values
(transform_json walks the full value), so extraction and rewriting now scan
the env's full serialization instead of only top-level strings. Also: the
install-page Enterprise note includes GCP/Azure, the trigger-discovery Retry
button binds to the loading state so clicks can't stack requests, and
extractTriggerConfigResourceRefs no longer splits rewriteTriggerConfig from
its doc comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6

* fix(hub-projects): scope $script:/$flow: relocation to the websocket url field

The runnable-url form is only meaningful in that one field; remapping it on
every nested config string could corrupt a literal payload that happens to
look like one (e.g. a websocket initial raw_message).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6

* fix(hub-projects): nested static-transform refs, shared flow walk for migrations, top-level-only url remap

Static input transforms accept arbitrary JSON and the worker resolves $res:
refs nested inside them — extraction and rewriting now scan the full
serialization, preserving the value's type. projectMigrations reuses
projectBundle's allFlowModules instead of carrying its own module walk, so
the preprocessor module (and any future module class) can't diverge between
bundling and migration detection. The websocket $script:/$flow: url remap
applies only at the config's top level, leaving nested url keys in args or
handler payloads untouched. Schedule tag stays excluded by design (a
source instance's worker-group name; a foreign tag queues jobs forever) —
now documented in the allowlist contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6

* fix(hub-projects): remap prefixed runnable refs only in their known config fields

script/<path> and flow/<path> forms are now rewritten only in the top-level
schedule handler fields (on_failure/on_recovery/on_success), joining the url
field treatment — shape-based remapping on arbitrary strings could rewrite a
literal payload that merely looked like a handler ref. Bare-path exact
matches and $res: tokens remain position-independent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6

* fix(hub-projects): abort stale-session imports after review, walk failure-module descendants

Confirming a migration review whose project/workspace was switched away from
now aborts with a toast before any write — previously the writes went to the
old workspace with all feedback suppressed by the session guard. And
allFlowModules puts the failure module in the root list so its nested
children (loops/branches inside a failure handler) are expanded like every
other module, for both bundling and migration detection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6

* fix(hub-projects): preserve app share state on Hub draft rehydration

`rehydrateFromHub()` rebuilt `draftItems` from the Hub project payload, which
carries only draft membership, so it dropped each app's `published`/`publicUrl`
and app-table origin. Outside `predeploy` the UI reads `draftItems` exclusively,
so reopening a draft showed a still-public app as unshared and removed its
Unpublish control. Merge the live workspace-item state onto matching drafts after
both `#loadWorkspace` and `rehydrateFromHub` (they race).

Also gate the Share-as-iframe action on `canShareAsIframe`: legacy raw apps live
only in the `raw_app` table, but that flow drives `AppService` (the `app` table)
and fails with "App not found" for them, so the action is now hidden for legacy
entries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(hub-projects): stale-identity import guard, live publish state on drafts, iframe action gating

The install session check now also compares the live slug and workspace to
the captured ones — loadSeq only advances when a new load starts, so
navigating away (workspace or ?hub becoming empty) previously left the
stale migration review able to import into the captured workspace. Draft
items are decorated with the live workspace item's shared-iframe fields
(published/publicUrl/appTable) so a public app still shows as public after
reopening a draft, settling reactively regardless of load order. The
share-as-iframe action is offered only for apps and app-table raw apps —
legacy raw_app entries have no AppService representation and the action
could only fail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6

* refactor(deploy-to-hub): drive share-state merge from one reactive derived

The rebase left two parallel fixes for the same rehydration gap: an
imperative mergeShareState call after each racing load, and a read-time
derived. Keep the pure, tested mergeShareState as the single implementation
and invoke it from the derived — no load-completion call sites to maintain,
and the merge settles whichever load finishes last.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6

* fix(hub-projects): block publish on unresolved refs; contain imported item refs

Address two Codex findings:

- Publish continued after `buildProjectBundle` reported unresolved references
  (a selected root or transitive runnable that failed to fetch, or a resource
  with no resolvable type), shipping a project whose items silently vanished or
  still pointed at the publisher's private source-workspace path. `#deployAll`
  now aborts before any Hub write when the bundle doesn't close, and the bundle
  drawer surfaces the unresolved list and disables "Create bundle".

- `installProject` validated only each item's own path, so a crafted or
  incomplete export could place a script/flow/app inside the target folder while
  its `$res:`/script/flow reference stayed bound to an existing `u/...` or other
  `f/...` asset. Extract each item's live references and reject any that escape
  `f/<folder>/` (hub/ script refs allowed), mirroring the existing trigger-config
  containment check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(hub-projects): contain imported $var refs; dedupe unresolved list

Follow-up to the publish-blocker and import-containment fixes:

- `$var:` references (flow static inputs, flow_env, app values, and trigger
  config fields such as SQS queue_url) were not caught by the containment check,
  which only recognized `$res:`/runnable refs. Retargeting leaves them unchanged,
  so an export with `$var:u/admin/token` imported an item that resolves a
  variable outside the target folder under the runnable's permissions. Scan each
  imported flow/app/trigger for `$var:` tokens and reject out-of-folder ones.
  Scripts are skipped: `$var:` is resolved in job args, not script source.

- `buildProjectBundle` stored bare paths in `unresolved` while keying missing
  items by kind:path, so a script and flow sharing a missing path produced a
  duplicate string. The new keyed unresolved list in the bundle drawer then hit
  Svelte's duplicate-key runtime error instead of rendering the publish blocker.
  Dedupe `unresolved` at the source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(hub-projects): contain $var/$jsonvar imports; retryable partial publish; iframe rollback

Address four Codex findings:

- `$jsonvar:` (secret JSON args) was not contained on import, and scanning the
  serialized flow/app for `$var:` tokens falsely rejected inline-code literals.
  The worker only substitutes a variable when an argument value *is* the
  reference (whole value, walking nested JSON), never a token embedded in code.
  Replace the token scan with a structural whole-value walk (`$var:`/`$jsonvar:`)
  and reject out-of-folder refs in flows, apps, and trigger config. Scripts carry
  no variable args, so they are skipped.

- A partial publish (failed item/trigger/migration write) still transitioned to
  the submit-ready `draft` phase. Stay in the retryable `predeploy` state on any
  failure, keeping the failed items visible, so nothing incomplete can be
  submitted and re-publishing retries every idempotent write.

- `#setAppShared` flipped a raw app public before checking its Hub item id or
  syncing the embed, so a missing id or a failed embed sync left the app publicly
  accessible while reporting failure. Validate the Hub target up front and roll
  the policy back if the embed sync fails.

- `buildProjectBundle` could emit duplicate unresolved paths (a script and flow
  sharing a missing path), breaking the keyed publish-blocker render. Deduped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(hub-projects): full-set trigger sync; count iframe re-sync + URL failures

Address three Codex findings, two of them refinements of the incomplete-publish
gate and iframe-rollback fixes:

- `#pushTriggers` returned early on an empty set, so re-deploying a project after
  removing all its triggers left the previous Hub triggers intact. Always post the
  trigger list (an empty one clears them), mirroring the migrations full-set sync.

- A raw app's post-deploy iframe re-sync failure only toasted; it now increments
  `failures`, so a public app left with a stale embed keeps the draft out of the
  submit-ready phase.

- `#setAppShared` skipped the embed and still returned success when the public URL
  couldn't be resolved, leaving the app anonymous with no usable link. It now rolls
  the policy back and throws when a share has no resolvable URL, alongside the
  existing embed-failure rollback (factored into one helper).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(hub-projects): count URL-less iframe re-sync; evict failed preview caches

Two Codex findings, both refinements of earlier fixes:

- The post-deploy iframe re-sync skipped a published raw app whose public URL was
  missing (URL resolution had failed) without counting it, so the re-bundle left
  the app public with a cleared Hub embed yet the draft still became submit-ready.
  Treat a published raw app with no resolvable URL as an incomplete publish and
  count it like a push failure.

- The bundle-preview dependency caches memoized promises that resolve to undefined
  after transient item/resource fetch failures, so fixing or retrying a dependency
  could never clear `bundlePreview.unresolved` and the Create bundle button stayed
  disabled until the session was recreated. Evict a cache entry once it resolves to
  undefined so a later rebuild re-fetches.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(hub-projects): keep Unpublish for a public app whose URL didn't resolve

The iframe controls required both `published` and `publicUrl`, so an anonymous app
whose public-URL lookup failed rendered as unshared with only a Share action and no
way to unpublish. Branch the Public badge and Unpublish on `published` alone, gate
the URL-dependent Open/Copy-iframe actions on `publicUrl`, and offer a Retry link
that re-resolves the URL when it is missing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(hub-projects): make $var/$jsonvar dependencies portable on import

Variable references were neither retargeted nor materialized, so a published
project that used a variable broke on import: a renamed-folder import rejected the
containing item (the `$var:` kept the old folder prefix), and a same-folder import
left the reference dangling (the target variable never existed).

Treat variables like resource stubs, fully on the import side (their `$var:`/
`$jsonvar:` refs already travel inside the exported item values):

- `buildRetargetMap` now also relocates the internal variable paths embedded in
  the export's flows/apps/triggers, and `rewriteContent` rewrites `$var:`/
  `$jsonvar:` tokens (kind preserved) for any path in the map — so the publish map,
  which omits variables, is unaffected.
- `installProject` creates an empty secret placeholder for each in-folder variable
  ref, conflict-safe via `existsVariable`, for the importer to fill. Values are
  never shipped. External refs stay rejected by containment.

Custom resource-type definitions (the sibling finding) are intentionally left to
the standardized official Hub resource types, so no schema import is needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(hub-projects): relocate $var refs structurally, never in inline code

Routing variable retargeting through `rewriteContent` also rewrote `$var:`/
`$jsonvar:` tokens embedded in script source, inline rawscript, and serialized app
strings, so an inert literal sharing a real variable's path was silently altered on
a renamed-folder import — contradicting the whole-string runtime-reference rule.

Relocate variables with a structural walk (`rewriteVarRefsInValue`) that rewrites
only whole-string `$var:`/`$jsonvar:` values (the sole form the worker resolves),
applied to flow/app/trigger values in `retargetProjectExport`; `rewriteContent` is
back to `$res:`-only. Inline code literals are left untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(hub-projects): relocate $var refs into the slug at publish

Import-side retargeting assumed exported `$var:`/`$jsonvar:` refs already began with
the project slug, but `buildProjectBundle` never relocated them from the source
folder. Publishing `f/source_folder/...` as slug `my-toolkit` therefore exported
`$var:f/source_folder/key`; import (fromSlug=my-toolkit) left it unchanged and
containment rejected the item.

Collect each item's runtime variable refs, feed them through the same path map that
relocates items/resources into `f/<slug>/`, and structurally rewrite the whole-value
refs — symmetric with the import retarget. The export is now slug-relative whatever
the source folder, and inline-code literals stay untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(hub-projects): relocate trigger config $var refs at publish

Item variable refs were relocated into the slug, but triggers publish through a
separate path (`#pushTriggers` → `rewriteTriggerConfig`), which doesn't touch
`$var:`/`$jsonvar:`. Publishing `f/source/...` under a different Hub slug left
schedule args and other config refs pointing at `f/source/...`, and import
containment then rejected the trigger.

Collect each trigger config's whole-string variable refs (`#triggerVarPaths`), feed
them through the bundle path map via a new `extraVarPaths` arg to
`buildProjectBundle`, and structurally rewrite the config on publish. Symmetric with
the item and import-side handling; the import retarget already relocated trigger vars.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(hub-projects): correct varContainmentViolation retargeting contract

The comment claimed retargeting doesn't rewrite variable refs; it now relocates a
project's own refs into the target folder, and containment rejects only those left
outside it. Describe the current behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: hugocasa <hugo@casademont.ch>
Co-authored-by: centdix <40307056+centdix@users.noreply.github.com>
Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
Co-authored-by: Aldrin Jenson <aldrinjenson@gmail.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: centdix <centdix@users.noreply.github.com>
Co-authored-by: Alexander Petric <alpetric@users.noreply.github.com>
Co-authored-by: Diego Imbert <70353967+diegoimbert@users.noreply.github.com>
Co-authored-by: Guilhem <guilhemlemouel@gmail.com>
Co-authored-by: Diego Imbert <diego@windmill.dev>
2026-07-23 13:47:28 +02:00
Ruben FiszelandClaude Fable 5 7b04820f8e feat(frontend): group compare & deploy items by folder (#9880)
* feat(frontend): group compare & deploy items by folder

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(frontend): group folder items with their folder, fix disabled hint

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 11:13:29 +02:00
GuilhemandClaude Opus 4.8 e09cd5862c feat: per-user draft review & deploy page (gating, badges, rename, raw-app deploy fixes) (#9625)
* feat: per-user draft gating, badges and rename display on deploy page

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): don't strike the path when a draft adds a summary to a summary-less item

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): don't strike draft-only items' auto-generated path against the pretty path

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): deploy raw-app drafts from top-level files so the bundle isn't dropped

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(frontend): share raw-app source→draft-value projection across chat and deploy page

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): deploy renamed/new flow, app and raw-app drafts at draft_path, not the temp storage path

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(frontend): add a design-system Checkbox and use it for deploy-page row/select-all checkboxes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: "Show all drafts" toggle on the deploy-drafts page

Replace the deploy-drafts page's legacy-hiding "Only my drafts" toggle with
a "Show all drafts" toggle that switches the listing scope between the
current user's own drafts (+ legacy no-owner rows) and every user's drafts
in the workspace.

Backend (`drafts.rs`, `openapi.yaml`):
- `/drafts/list` gains an `all_users` query param that drops the owner
  filter, and a per-row `mine` flag (own draft or legacy no-owner row).
  `DISTINCT ON` now prefers the user's own row, then the legacy row, then
  another user's, so `mine`/`legacy_draft` describe the kept row.

Frontend (`CompareDrafts.svelte`, `workspaceDrafts.svelte.ts`):
- "Show all drafts" toggle (default off). The all-users superset is fetched
  lazily via the shared resource only while the toggle is on, so the page's
  fork draft-count (own drafts) is unaffected.
- Other users' drafts are view-only: disabled checkbox + Discard with a
  "belongs to another user" tooltip; Show diff stays enabled. Selection,
  select-all and the deploy count only ever include the user's own drafts.
  The multi-user warning triangle shows on owned rows only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(backend): gate all_users draft listing by read permission

Addresses the PR review on the per-user deploy-drafts page:

- `/drafts/list?all_users=true` previously had only `WHERE workspace_id = $1`
  with no read-permission check, so any non-operator could enumerate every
  draft's path, summary and authors — including items they can't read. Now
  rows the caller doesn't own (`mine = false`) are gated through
  `require_can_read_path` (the same gate `/drafts/get` uses) and dropped when
  unreadable; both its `NotFound` and `NotAuthorized` denials are treated as
  "not visible".
- Skip the per-row `require_can_write_path` probe on those non-owned rows
  (they're never selectable — `isSelectable` requires `mine`): set
  `can_write = false` directly, removing a redundant N RLS write-probes when
  `all_users` is on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): only confirm destructive draft discards on the deploy page

Discarding a draft is non-destructive in every case except removing the last
draft of a never-deployed item (`draft_only` with no other user's draft),
which permanently deletes it. Confirm only that case; reverting a draft over a
deployed item, or discarding your copy while another user still holds a draft,
now runs immediately (the ⚠️ already signals the multi-user case). Drops the
redundant "other users still have a draft" / "deployed version unaffected"
confirmation branches.

Harden the destructive check: it keyed off `otherDraftUsers()`, which subtracts
`currentUsername`; while `$userStore.username` is unhydrated, your own draft
looked like another user's, flipping a draft-only item to "non-destructive" and
deleting it with no confirmation. Now: deployed counterpart → never destructive;
`draft_only` with unknown `currentUsername` → treated as destructive (confirm).
The delete modal also shows the friendly `draft_path` instead of the raw
`draft_{uuid}` storage path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): deploy low-code app drafts (value + summary persistence)

A visual (low-code) app draft is autosaved as the *bare* App value
(grid/theme/... plus a draft-only `draft_path`), not wrapped in
{ value, summary, policy } like script/flow drafts. The Review & Deploy page
read `requestBody.value = d.value` — undefined for that shape — so deploying any
low-code app draft (created or edited) sent no value and failed. Read the value
from the draft object itself, strip the draft-only `draft_path` from it, and use
that as the deploy path.

Also persist the app summary, which was dropped entirely: the autosave stores
the bare App value (the summary normally lives only in the `app` table column,
set on deploy), so a draft never carried it — reopening a draft or deploying it
lost the summary. Mirror the summary onto the autosaved App (like `draft_path`),
read it back when loading a draft, and on deploy send it as the summary column
while stripping it (and `draft_path`) from the deployed value so the value stays
clean.

Verified end-to-end: a new low-code app with a summary deploys at its pretty
path with the summary set, content intact, and no draft_path/summary leaked into
the deployed value; the draft is cleaned up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:49:36 +00:00
GuilhemandClaude Opus 4.8 b0b330c786 feat: deployed↔draft compare + AI-session draft bar (#9435)
* feat: deployed↔draft compare for current workspace + session draft bar

Add a "Deployed ↔ draft" comparison alongside the existing fork-vs-parent
compare flow, and surface drafts in the AI session UI.

- Merge the fork-direction toggle (Deploy to parent / Update current) and
  the new deployed↔draft mode into one 3-way CompareModeToggle, rendered
  inside the comparison card. Hidden in non-fork workspaces (draft only).
- CompareDrafts: list/deploy/discard server drafts (scripts, flows, apps
  incl. raw apps) via shared WorkspaceDeployLayout.
- Session draft bar (SessionDraftBar) mirrors the fork bar, only visible
  when drafts exist; its diff button opens the shared read-only diff
  drawer extracted as WorkspaceDiffDrawer (ForkDiffDrawer + DraftDiffDrawer
  are thin wrappers over it).
- WorkspaceDraftsBanner: home banner linking to draft review.
- Backend: GET /drafts/count endpoint for the draft-count badge.
- Raw app draft deploy (rawAppDeploy.ts) + vite /ui_builder proxy headers
  so the bundler iframe loads cross-origin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: refine draft/fork compare toggle UX

Follow-up polish on the merged deploy/draft compare control:

- Relabel the draft toggle to "Deploy draft (N)" and show per-direction
  counts on all three toggle buttons (deployable / updateable / drafts),
  suppressed when zero. Counts are computed page-side so they persist in
  draft mode too.
- Warn before deploying to the parent when the fork has undeployed drafts
  ("Only deployed versions in this fork can be sent to {parent} …") with a
  one-click link to the draft view; milder note in the update direction.
- Show an empty-state message per direction ("Nothing to update — this
  fork is up to date with {parent}") instead of a table of greyed,
  non-actionable rows; hide the deploy/update button in that case.
- Drop the standalone "Pending drafts" info alert from the draft list.
- Align the fork "Show diff" button to the non-deprecated Button API
  (unifiedSize, onClick, startIcon) so it matches the draft one; mark
  "Discard draft" destructive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: link compare row titles to the item editor

- Render each compare row title (fork and draft) as a link that opens the
  item in a new tab, scoped to the current workspace (raw apps route to
  /apps_raw/edit), matching the AI-session diff drawer: target=_blank, hover
  underline + ExternalLink icon, click stops row-selection propagation.
  Kinds without an editor stay plain; the fork rename markup is preserved.
- Drop the "Kind → name" prefix from draft rows — that arrow reads as the
  rename visual and the kind is already shown by the row icon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: clickable rows + multi-select in deploy layout

- Make deploy-layout row cards selectable on click via an opt-in
  `selectOnRowClick` prop on the shared Row (default off, other tables
  unaffected); clicks on the checkbox, title link and action buttons are
  ignored. Adds role/tabindex + Enter/Space keyboard support.
- Support multi-select with modifier keys like classic list pickers:
  Shift+click selects the contiguous range from the anchor row; Cmd/Ctrl
  (and plain) click toggles a single row. select-none avoids text
  highlighting on shift-click.
- Turn the "Select all" text into a <label> associated with its checkbox
  so clicking the text toggles it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: select all drafts by default in draft compare

Drafts now load pre-selected (deploy-all is the common intent); guarded so
a reload after a deploy doesn't re-select the items left behind. Mirrors
CompareWorkspaces' default auto-selection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: show diff for draft-only items stored without a draft row

A draft_only flow/script/app whose content lives in the entity row itself
(created via create*(draft_only: true), no separate draft-table row — like
u/admin/new) returns draft == null from get*ByPathWithDraft. getDraftDiffValues
passed that null through, so the diff "after" side was empty and nothing
rendered. Fall back to the row's own value as the draft content when draft is
null (deployDraft already did this), fixing both the compare-page DiffDrawer
and the session bars' WorkspaceDiffDrawer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: shared diff button across session bars + bar spacing

- Extract SessionDiffButton (variant=default, ± DiffIcon, count, "Open diff"
  title) and use it for the diff-drawer trigger in both the fork bar and the
  draft bar, so they're identical. Drop the icons from both "Review" buttons.
- Add gap-1 (4px) between the fork bar and draft bar when both are visible
  (flex wrapper; single in-flow root per bar, drawer is portalled — no stray
  gap when only one shows).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: deploying a new (draft-only) flow or app

A draft_only flow/app already has an entity row (created via
create*(draft_only: true)), so deployDraft's createFlow/createApp rejected it
with 400 "already exists". Use updateFlow/updateApp instead — a listed draft
always has a row, and update promotes a draft_only entity to a real deployed
version (clearing the flag), like the editor does. Scripts were unaffected
(createScript + parent_hash makes a new version).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: refresh fork comparison after deploying/discarding a draft

Deploying a draft promotes it to the workspace's deployed version, changing
the fork comparison (ahead/behind vs parent) — but the compare page only
re-fetched it on workspace change, so the deploy/update toggle counts and the
CompareWorkspaces tab went stale. CompareDrafts now fires onChanged after a
successful deploy/discard; the page rewires it to refresh the comparison and
draft count.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: stop draft-count effect from freezing the AI session page

ensureDraftCount cleared its dedupe key on error; since the caller is a reactive $effect (SessionDraftBar), a persistently-failing countDrafts spun the effect into an infinite retry loop that flooded the console and froze the tab. Claim the key before awaiting and keep it set on failure; refresh*() still forces a re-fetch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: correct draft count and refresh compare counts after actions

count_drafts now counts deployable drafts (draft_only OR has-a-draft-row across script/flow/app), matching the CompareDrafts list, instead of raw draft-table rows which miss new draft-only items. CompareWorkspaces and CompareDrafts fire onChanged so the compare page re-fetches the comparison and draft count after deploy/update/discard, keeping the toggle badges in sync.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: session draft bar shows a fresh count on every (re)open

The runtime persists across client-side navigation, so the deduped
ensureDraftCount() kept a stale count (e.g. a 0 cached before a draft was
created) when a session was re-opened — the bar stayed hidden even though
the server count was >0.

Force one fresh fetch per mount from a non-reactive onMount via
refreshDraftCount(workspace) (which now takes the workspace so it works
before the dedupe key is set). The reactive $effect keeps using
ensureDraftCount: refreshDraftCount reads loadingDraftCount ($state), so
calling it from an effect would track-and-mutate that state into an
infinite fetch loop — ensureDraftCount's plain-key early-return avoids it.
ensureDraftCount also now releases its key after a 5s backoff on failure so
a transient countDrafts error retries instead of leaving the bar stuck.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor: make the draft count a single deep Workspace Drafts module

The Draft Count was computed four ways (backend count_drafts SQL, the
CompareDrafts list filter, a bespoke sessionRuntime cache, and the compare
page state) that drifted — the root cause of the unreliable count, the
stale-on-reopen bug, and the effect-loop freeze.

Introduce one module (workspaceDrafts.svelte.ts):
- getDraftItems(ws) lists the deployable Draft Items once; count ≡ list length,
  never a separate query.
- useWorkspaceDrafts(() => ws) is a component-scoped runed resource (fetches on
  mount + ws change, no persistent cache → fresh on every (re)open).
- invalidateWorkspaceDrafts(ws) refreshes mounted consumers; deployDraft/
  discardDraft self-invalidate, so callers never reason about staleness.

Rewire every reader to it (SessionDraftBar, CompareDrafts, DraftDiffDrawer,
WorkspaceDraftsBanner, compare page) and delete the sessionRuntime draftCount
apparatus (key + loading flag + 4 methods + effects + backoff). With no caller
left, remove the count_drafts endpoint (handler, route, openapi, sqlx cache,
generated client) — drafts.rs/openapi return to their main state. Record the
draft vocabulary in CONTEXT.md.

A single GET /w/{ws}/drafts/items endpoint can later replace getDraftItems'
three list calls behind the unchanged seam.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: warn before deploying a draft based on an outdated version

When a newer version is deployed while a draft exists (git-sync/CLI deploys preserve drafts via skip_draft_deletion), the compare/deploy-drafts page now flags the draft as Outdated and gates deploy behind an override confirmation with a diff — instead of silently clobbering the newer version. Staleness is read from the draft's base version: scripts already store parent_hash; flows/apps now record a draft_base_version sidecar on save.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: drop redundant /ui_builder proxyRes hack (superseded by #9433)

main's global configure-response-headers plugin now runs with enforce:'pre'
and sets COOP/COEP/CORP on dev responses (#9433), so the per-proxy proxyRes
override is no longer needed. Revert the /ui_builder block to main's headers
form — vite.config.js now matches main with no branch-specific change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(sessions): keep draft count reactive to preview/chat deploys

Invalidate the Workspace Drafts resource at every frontend deploy seam
(ScriptEditorView / FlowEditorView / RawAppEditorView onDeploy + onSaveDraft)
so user-driven deploys from the Preview panel update the count immediately,
and refresh SessionDraftBar on the same coarse signals SessionForkBar uses
(AI turn-end + tab refocus) to cover chat-driven deploys that happen
server-side and never surface as frontend calls.

Also: always show the draft toggle count including (0) on the compare page,
drop the header/content separator line in both compare cards, and derive the
"Deploy N drafts" footer count so it stays reactive after discard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor: address branch review findings

- WorkspaceDraftsBanner: use the modern Button API (variant/unifiedSize/onclick)
  instead of the deprecated size/color/on:click triad; drop "pending" from the
  banner copy to match CONTEXT.md vocabulary.
- WorkspaceDeployLayout: make Cmd/Ctrl-click distinct from a plain click.
  Plain row click now selects only that row (classic file-picker), Cmd/Ctrl
  toggles, Shift extends the range, and the checkbox still plain-toggles.
  Adds an onSelectOnly callback wired in CompareDrafts/CompareWorkspaces.
- WorkspaceDiffDrawer: document why the file filter is a raw input (bespoke
  keyboard-nav integration the design-system inputs can't express).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* revert: drop draft version-gating (stale-draft warning)

Remove the "deploying an outdated draft would override a newer version"
guard. It's a rare edge case and will be handled properly by conflict
resolution in a follow-up PR.

- CompareDrafts: drop staleMap/computeStaleness, the TOCTOU pre-deploy
  re-check, the "Outdated" badge, the override-in-diff button, and the
  "Newer version deployed" confirmation modal; deploySelected is now the
  plain deploy.
- utils_draft_deploy: remove getDraftStaleness/DraftStaleness and the
  draft_base_version strip.
- FlowBuilder / AppEditorHeader / RawAppEditorHeader / AppJsonEditor: stop
  injecting draft_base_version into draft saves — these editor paths are
  back to matching main, shrinking the PR's blast radius.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* style: unify home banner CTAs on the modern Button API

Both the Workspace Drafts banner and the sibling Fork banner now use
variant="default" unifiedSize="sm" onclick, so the two CTAs on the home
page render identically and neither uses the deprecated size/color/on:click
props.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(compare): show draft deploy direction badge inside forks

Mirror the fork compare header's "from → into" badges on the Deploy-draft
tab: "deploy: draft → into: <fork>". Makes it explicit that deploying a
draft promotes it within the fork (deployed↔draft), not up to the parent.
Only rendered inside forks, where the parent could otherwise be confused
with the deploy target.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(compare): address PR review — dedup drafts resource + shared link

- De-dupe the compare page Workspace Drafts fetch: the page owns the single
  resource and passes draftItems/draftsLoading into CompareDrafts (was mounting
  a second resource → 6 list calls; now 3).
- Prune transient deploymentStatus for items dropped from the list (no unbounded
  growth, no stale 'deployed' suppressing a re-drafted row).
- Type getDraftItems' list fields via a narrow DraftListEntry (drop Array<any>).
- Clear comparison catch-up timers on unmount (onDestroy).
- Extract shared ExternalEditLink.svelte; use it in CompareDrafts,
  CompareWorkspaces, WorkspaceDiffDrawer (was a near-verbatim <a> block x3).
- Note conflicts intentionally count in both toggle directions; drop a stray
  blank line in sessionRuntime.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(compare): show draft summary renames via shared item-summary component

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(compare): address round-2 PR review

- Point the fork-compare edit link at the workspace the item actually lives
  in: a parent-only row (absent in the fork) would 404 if linked into the
  fork, so link it into the parent instead.
- Replace the bespoke raw <button class="underline">Deploy drafts</button> in
  the undeployed-drafts alert with a design-system Button (variant=subtle).
- Drop the stray Prettier reflow in sessionRuntime (restore to match main).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(compare): warn on fork items with a pending draft

In the fork compare list, items that are deployed *and* have a pending
draft (has_draft) now:
- show a yellow "+Draft" badge (AlertTriangle), rendered before the
  New/status badges, with a per-direction tooltip explaining that
  deploying/updating moves the deployed version, not the draft;
- are excluded from the default selection (still manually selectable);
- trigger a confirmation modal if explicitly selected and deployed/updated,
  listing the affected paths.

The signal comes from the page's existing fork drafts resource (a
kind:path Set passed down) — no new fetch, no backend change. Also rename
the undeployed-drafts alert CTA from "Deploy drafts" to "See drafts".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(compare): rename page to "Compare & Deploy"

Update both the page heading (PageHeader) and the browser-tab title.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(compare): multi-select rows by default (no modifier)

In the shared WorkspaceDeployLayout (fork + draft lists), a plain row
click now toggles the item in/out of the selection instead of replacing
the whole selection with it. Removed the modifier-based selection
entirely: the now-dead onSelectOnly path and its two call sites, plus
shift+click range selection (and its anchor/isPickable helpers).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(table): don't toggle row selection on keyboard child activation

Row's onkeydown selection handler lacked the interactive-child guard that
handleRowClick already had, so pressing Enter/Space on a checkbox, action
button, or title link both activated the child and toggled the row's
selection. Extract a shared fromInteractiveChild() guard and apply it in
handleRowKeydown, mirroring the click path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(fork-banner): show draft CTA when fork is up to date

When a fork has no changes vs its parent ("Everything is up to date") but
has pending drafts, the banner now mirrors the non-fork drafts banner:
the status text becomes "This workspace has N draft(s)" and the button
becomes "Review & deploy drafts", linking to the compare page in draft
mode. When the fork has real ahead/behind diffs, the existing status and
buttons are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(compare): honor renamed draft paths + raw-app draft fixes

Address the Codex review:
- Draft deploy now uses the draft payload's path for scripts, flows and raw
  apps (keeping the URL path as the existing item key), so a rename in a
  draft deploys to the new path instead of silently staying at the old one.
- DraftDiffDrawer maps raw apps to the `raw_app` kind so their row edit
  links open the raw-app editor, not the legacy app editor.
- ScriptEditorView.restoreDeployed invalidates the workspace drafts after
  deleting the draft, so the session draft-bar count drops immediately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(compare): guard showDiff race, tree label, mode fallback

Address the cubic review:
- CompareDrafts.showDiff uses a monotonic request token so two quick "Show
  diff" clicks can't let a slow earlier fetch overwrite a faster later one.
- WorkspaceDiffDrawer.buildTree labels a 2-segment path with its leaf name
  (parts[1]) instead of the full scope key.
- The compare page only resolves ?mode=draft immediately; ?mode=fork (and
  an absent mode) defer to the isFork-aware effect, which falls back to
  draft for non-fork workspaces instead of stranding them on the fork UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: remove CONTEXT.md from the PR

Drop the root CONTEXT.md domain glossary and the lone comment pointer to
it in workspaceDrafts.svelte.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(compare): send custom_path on raw-app draft deploy

A raw-app draft that changes or clears its custom route was silently
dropped on deploy from the compare page: updateAppRaw omitted custom_path,
so the backend preserved the old route. Send the draft's custom_path on
update — matching the fork deploy path (which spreads the full app,
custom_path included) and the createAppRaw branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(sessions): refresh draft count on raw-app session save-draft

The script/flow session editors invalidate the workspace drafts on
save-draft, but the raw-app editor only did so on deploy. Thread an
onSaveDraft callback through RawAppEditor → RawAppEditorHeader and call
invalidateWorkspaceDrafts from RawAppEditorView, so saving a raw-app draft
in an AI session updates the SessionDraftBar count immediately (and the bar
appears when the count was zero).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(compare): honor renamed paths, draft triggers & paginate inventory

Address the Codex review:
- Draft deploy honors the draft's renamed path for scripts/flows/raw apps
  (keeping the URL path as the existing item key).
- Script/flow draft deploy now deploys draft_triggers via the shared
  deployTriggers, instead of silently dropping them with the draft.
- rawAppDeploy sends custom_path admin-gated on update (admin: value/'' to
  clear; non-admin: undefined) so non-admins don't hit RequireAdmin.
- getDraftItems pages through listScripts/listFlows/listApps so drafts past
  the first page are included in the count, banners, drawer and deploy list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(compare): admin-gate custom_path on visual-app draft deploy

The visual-app branch of deployDraft sent custom_path unconditionally on
updateApp, so a non-admin deploying an app draft for an app with a custom
route hit RequireAdmin. Mirror AppEditorHeader and the raw-app path: admins
send the draft's custom_path ('' clears), non-admins send undefined so the
backend preserves the existing route.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(raw-app): save initial draft directly when path is known

In the AI-session preview, a never-deployed raw app has newApp=true but a
known path, so saveDraft opened the "Initial draft save" path-picker drawer
— which is gated on `appPath == ''` and therefore never rendered, making
Save draft silently do nothing. Branch the new-app case on appPath: pick a
path via the drawer only when none is chosen yet; otherwise call
saveInitialDraft() directly. saveInitialDraft now also toasts and fires
onSaveDraft so the session draft-bar count refreshes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(compare): preserve deployed custom_path on visual-app draft deploy

The visual-app draft value usually omits custom_path, so the admin branch's
`d.custom_path ?? ''` sent an empty string, which the backend treats as
"clear the route" — an admin deploying a content-only draft would wipe the
app's existing custom route. Fall back to the deployed route
(`r.custom_path`) when the draft omits it; an explicit '' still clears.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 18:24:23 +00:00
c57c769dea feat: add CI test scripts with auto-trigger on deploy (#8736)
* feat: add CI test scripts with auto-trigger on deploy

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: fix annotation parser early return and handle renames correctly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: move CI test results to top of script/flow detail pages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: improve CI test results spacing, icon, and remove pass label

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: support one-line annotation and use script/path format

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: move CI test trigger logic to EE

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: move CI badge next to New badge and add deduplicated CI summary

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add CI test e2e tests and fix nullable column annotations

Add integration tests for CI test annotation parsing (creates/removes
ci_test_reference rows) and the CI test results API (single + batch
endpoints). Add backend test for auto-trigger on deploy (private+python).

Fix sqlx LEFT JOIN LATERAL nullable column annotations in
get_ci_test_results and get_ci_test_results_batch queries — sqlx
cannot infer nullability from LATERAL subqueries, causing runtime
decode errors when no matching job exists.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix build/sqlx

* fix

* feat: CI test improvements and templates

- Fix windmill-dep-map/private feature propagation in worker, api-scripts,
  and api-flows Cargo.toml so CI test triggers actually fire in EE mode
- Clone ci_test_reference rows during workspace fork
- Add polling to CiTestResults component (refetch every 3s while running)
- Add running state and auto-refresh to ForkWorkspaceBanner CI summary
- Add yellow "CI test" badge on script list rows and detail page
- Fix Library badge border color (remove indigo border override)
- Add CI Test TypeScript and CI Test Python templates in ScriptBuilder
- Update sqlx offline cache
- Add debug tracing for CI test trigger in worker_lockfiles

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add missing children prop to WorkspaceDeployLayout

Fixes svelte-fast-check type error when passing named snippets as
children content inside the component tag.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review feedback

- Remove empty wrapper divs around CiTestResults, move mb-4 into component
- Add batch endpoint size cap (max 200 items)
- Add ON DELETE CASCADE to ci_test_reference workspace FK (new migration)
- Downgrade CI test trigger logs from info to debug
- Fix false-positive polling: only treat status='running' as running,
  not null status (CiTestResults, CompareWorkspaces, ForkWorkspaceBanner)
- Fix test numbering in integration tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to latest EE commit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to d9d68c2406df0b59f413ea0b2cb24780a9817d04

This commit updates the EE repository reference after PR #516 was merged in windmill-ee-private.

Previous ee-repo-ref: d7ccd9b86da99ec056a0e8708e3637d64290387a

New ee-repo-ref: d9d68c2406df0b59f413ea0b2cb24780a9817d04

Automated by sync-ee-ref workflow.

* fix: treat queued jobs (job_id set, null status) as running

Jobs that have been pushed but not yet picked up by a worker have a
job_id but null status. Treat these as 'running' to avoid showing
misleading 'pass' badges or '0 passing'. Tests that were never
triggered (no job_id, null status) remain neutral/hidden.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: hugocasa <hugo@casademont.ch>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-04-09 17:21:36 +00:00
3d4f4c6c38 feat: Fork datatables (#8339)
* export_datatable_schema

* Propose to fork the datatable on ws fork

* dump datatable

* Dockerfile

* Fix import_datatable_dump

* datatable schema fork works!

* Option to copy both schema and data

* Datatable fork behavior

* nit ui

* use psql instead

* remove fork_datatable route

* feat: add fork_pg_database and export_pg_schema routes with DB Manager UI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: pluralize "schema" to "schemas" in DB Manager export/import UI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add import mode select (schema only vs schema + data) to DB Manager import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Select schema or schema+data when important database

* fix: prepend $res: prefix to resource paths in DB Manager import/export

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: dynamic import button label based on selected mode

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* nits

* feat: add warning alert when schema+data import mode is selected

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* nit hide on cloud hosted

* refactor: remove fork_behavior from datatable settings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: split CreateWorkspace into layout wrapper and CreateWorkspaceInner

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: instantiate CreateWorkspaceInner in globalForkModal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* nit icons

* Data table fork UI

* feat: pass per-datatable fork behaviors from UI to backend during workspace fork

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix fork overwriting all datatables

* UI nits

* custom instance db refactor

* custom instance db wizard btn for all in dropdown

* nit

* Delete custom instance database button

* Disable forking for resource datatables

* Big import buttons when db empty

* Revert "Disable forking for resource datatables"

This reverts commit 9561cc8fd4.

* feat: add non_diffable flag to resource table

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add resource-type datatable fork with CREATE DATABASE

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: tag forked datatables with nonDiffable and forkedFrom

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: diff datatable and ducklake settings individually on workspace merge

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: skip non_diffable resources and datatables in workspace diff

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: default datatable fork behavior to keep_original

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: make grant permissions non-fatal in instance datatable fork

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: make datatable and ducklake diffs visible in workspace comparison

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: remove datatable fork logic from workspace fork route

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: correct ahead/behind logic for datatable and ducklake diffs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Revert "fix: correct ahead/behind logic for datatable and ducklake diffs"

This reverts commit 6b50884dc6.

* revert: remove datatable and ducklake settings diffing logic

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add datatable clone UI with step-by-step confirmation modal

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: extract datatable fork UI into ForkDatatableSection component

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* nit

* fix: run datatable cloning before workspace fork creation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* nit disable fork admins

* nit fix switching workspace prematurely

* fix: use source workspace for forkPgDatabase calls during fork

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: update forked workspace datatable settings after fork creation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add forked_from field to DataTable and set it for instance forks

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* nit onFinish

* fix: add forked_from to DataTableSettings OpenAPI schema

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: track datatable table DDL changes in workspace_diff

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Revert "feat: track datatable table DDL changes in workspace_diff"

This reverts commit 7526dd68b9.

* feat: add get_datatable_full_schema endpoint and snapshot schema on fork

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix duplicate migration key

* fix: set forked_from on datatable config for both instance and resource types

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* nits

* feat: drop forked databases on workspace deletion with confirmation UI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: extract drop_forked_datatable_databases from delete_workspace

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: cast pg char columns to text in FK schema query

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: show dbname instead of resource type in fork deletion modal

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* ui nit

* refactor: extract drop_custom_instance_database into windmill-common

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add datatable schema diff section to merge UI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* UI

* feat: add review drawer with YAML diff and SQL migration runner

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: use Monaco DiffEditor for YAML diff in review drawer

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* nit

* Revert "refactor: use Monaco DiffEditor for YAML diff in review drawer"

This reverts commit a86008ba4c.

* Revert "feat: add review drawer with YAML diff and SQL migration runner"

This reverts commit 0a0deb5ddb.

* feat: add review drawer with DiffEditor and SQL migration runner

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* ui nits

* fix: show diff between forked_from schema and changed side

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: re-fetch target live schema after migration for correct baseline

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* revert

* nit auto next

* feat: add confirmation modal before deploying migration to parent

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: handle missing columns/foreignKeys in schema conversion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* nits

* refactor: use temp file on disk for pg_dump instead of in-memory string

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Don't replace postgres dbname

* fix: add validation to drop_custom_instance_database and use source db for CREATE/DROP

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: type DataTable.forked_from as DataTableForkedFrom struct

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: simplify fork_pg_database to take source + target_dbname

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* dead code

* feat: enforce schema_and_data admin-only and extract create_custom_instance_database

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: rename fork_pg_database to import_pg_database with source/target/override params

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* nit

* refactor: remove original_dbname/original_resource from forked_from, resolve from parent

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* nit

* fix: resolve forked dbname from fork workspace when dropping resource databases

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* nits

* fix: always clean up global_settings even if database doesn't exist

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: check datatable resource_type from config instead of URL prefix

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: wrap PG default value expressions in braces to prevent CAST quoting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Revert "fix: wrap PG default value expressions in braces to prevent CAST quoting"

This reverts commit 77f5a2c4e8.

* refactor: reuse columnDefToTableEditorValuesColumn for default value handling

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: store raw API schema in forked_from to avoid double transformation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Revert "fix: store raw API schema in forked_from to avoid double transformation"

This reverts commit e326197a20.

* Revert "refactor: reuse columnDefToTableEditorValuesColumn for default value handling"

This reverts commit bd8f071d9f.

* fix: validate dbname with strict regex to prevent SQL injection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix default value

* always validate dbname

* refactor: move get_datatable_full_schema structs and logic to query_builders.rs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: split import_pg_database into create_pg_database + import_pg_database

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: extract drop_forked_datatable_databases into its own route

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: require admin when using $res: resource paths in import_pg_database

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: use UserDB for $res: resource access and restrict dbname creation

- resolve_pg_source_checked uses UserDB (row-level security) for $res: paths
- transform_json_unchecked is now pub(crate) to prevent misuse
- Non-superadmins can only create databases with wm_fork_ prefix
- datatable:// remains accessible to everyone

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: refuse to drop forked databases unless name starts with wm_fork_

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: remove resolve_pg_source, use resolve_pg_source_checked everywhere

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix permissions

* sqlx prepare

* compilation nits

* sqlx prepare

* sqlx prepare

* wrong route syntax

* fix: allow workspace owner to edit datatable config for fork setup

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Revert "fix: allow workspace owner to edit datatable config for fork setup"

This reverts commit ab683e637b.

* refactor: move datatable fork setup into create_workspace_fork backend

Instead of updating datatable settings from the frontend after fork
creation (which required admin/owner access), pass forked_datatables
info to create_workspace_fork and handle it atomically in the same
transaction. Removes applyPostForkDatatableUpdates from frontend.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: snapshot schema in backend during fork instead of frontend

The schema snapshot is now taken by the backend in apply_forked_datatable
via snapshot_datatable_schema, which connects to the parent workspace's
datatable and runs pg_get_full_schema. This removes the need for the
frontend to call getDatatableFullSchema and pass the schema through.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: use get_resource_value_interpolated_internal for $res: to resolve $var: references

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* nit

* sqlx prepare

* fix: add permission check to drop_forked_datatable_databases, validate dbnames, restrict temp file perms

- drop_forked_datatable_databases: same permission as delete_workspace
  (fork owner or super admin)
- validate_dbname on target_dbname_override and ForkedDatatableInfo.new_dbname
- Enforce wm_fork_ prefix on forked datatable new_dbname
- DumpFile: set /tmp/windmill/ to 0700 and create files with 0600

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* nit CLI

* Rename to ws_specific

* sqlx prepare

* nit always validate dbname

* fix: include foreign keys in CREATE TABLE migration for added tables

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: detect nextval defaults and use SERIAL/BIGSERIAL types in CREATE TABLE

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Update frontend/src/lib/components/DBManagerDrawer.svelte

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* Update backend/windmill-common/src/lib.rs

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* Update backend/windmill-common/src/lib.rs

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* fix: sort foreign keys by constraint name for deterministic schema output

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* sqlx prepare

* rename migration to update timestamp

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-04-07 21:03:06 +00:00
wendrul 7ac93f6ee3 feat: option to preserve on_behalf_of and edited_by for admins and users in the new wm_deployers group (#8079) 2026-02-25 12:05:22 +00:00