From 29133398f99cd2dd5b33057ee9df4492d82e067a Mon Sep 17 00:00:00 2001 From: Guilhem Date: Thu, 27 Aug 2026 10:39:36 +0200 Subject: [PATCH] feat: a wizard for importing a hub project, and finishing what the import cannot (#10729) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(frontend): guided setup wizard for data tables On Cloud a data table cannot use the Windmill instance database, so a new workspace hit a dead end: an alert telling the user to go find a PostgreSQL resource somewhere else. Setting one up meant three disconnected places, and the connection could only be tested after the config had already been saved. Adds a three-step wizard (choose a database -> set it up -> name it) reached from the data tables settings page: - Supabase: signs in via the existing supabase_wizard OAuth client and creates the project from inside Windmill. Because db_pass is an input to project creation, Windmill sets the password and the user never visits a dashboard. - Your own database: picks an existing postgresql resource, or adds one with a connection string through the form that already supports it. - Windmill database: hands back to the inline row editor, since instance databases are provisioned by a superadmin. Verifying access is no longer a step the user takes: Continue runs the check and passing it is what advances the wizard, so a database that cannot create tables never reaches the workspace config. Co-Authored-By: Claude Opus 5 (1M context) * chore: pin ee-repo-ref to the Supabase provisioning endpoints Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): do not claim the database is ready when its check failed Co-Authored-By: Claude Opus 5 (1M context) * fix: address review findings on the data table wizard - The Supabase create branch advanced on `provisioning === 4` without consulting the check it had just run, so a role that cannot create tables could reach Finish. It now blocks and offers Try again. - Retrying no longer mints a fresh secret variable + resource each time: the credentials are only re-created when the password actually changed. - The generated password is captured before the create call rather than after, since a throw there can still leave a project behind. - On a failed provision the project list is refreshed, so the just-created project can be picked up from the other tab instead of provisioning a second. - Finish refuses a name that already belongs to another data table, which previously repointed it at the new database. - Secrets go to the acting user's namespace instead of a literal `u/admin/`. - The progress list no longer ticks "Created on Supabase" before the request is sent, and does not claim the database is ready when its check failed. - The wizard's resume state is cleared when it closes, so reopening after an abandoned OAuth round trip is not stuck on step 2. - The OAuth callback shares the session-storage key rather than repeating it. - SupabaseConnect uses the shared provisioning helpers instead of a fork. - Restores the doc comment displaced onto TestDataTableResourceQuery. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): simplify Alert layout and balance its vertical padding The body was rendered by two near-duplicate branches, each wrapping the text in an extra div only to hang a margin on it, and the margins disagreed: the collapsible branch spaced above with mt-2, the static one below with mb-2. Since isCollapsed defaults to true, every non-collapsible alert took the static branch, so titled alerts read as 24px of space below the text against 16px above -- visibly off-centre -- with the title and body flush against each other. Collapse both branches into one and drop the margins; the container's own padding now sets top and bottom equally, with a small gap under the title row. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): only offer Supabase when its OAuth client is configured The wizard offered the Supabase card unconditionally, so on an instance whose superadmin never configured a supabase_wizard client -- or whose backend is built without the oauth2 feature, which compiles the whole /api/oauth router out -- the card dead-ended at a 404. Gate it on listOauthConnects, the same check ApiConnectForm already makes, fetched on open so configuring the client mid-session does not require a reload. Also drop the Supabase project ref from the existing-project cards: it is an opaque identifier that means nothing outside Supabase's own dashboard URLs. Show the region instead, plus a status word when the project is not healthy, since a paused project is the one case where the connection check fails for a reason unrelated to the password. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): run the Supabase OAuth leg in a popup A full-page redirect unmounts the wizard, so anything the user does on Supabase's side -- signing in, confirming an email, browsing their dashboard -- leaves them with nothing pointing back at Windmill, and the wizard had to park its state in sessionStorage to survive the trip. Open the connect endpoint in a popup instead. The modal stays on screen throughout and the callback hands the token back through postMessage rather than navigating. The parked-state path stays as the fallback for browsers that block the popup. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): scope the connection check to the choice that produced it A failed check stayed on screen when the user switched Supabase mode or picked a different provider, so a fresh tab opened showing an error about a database it had nothing to do with. Clear the report and the error on both switches; re-clicking the tab already selected leaves an error the user is reading in place. Also polish the Supabase step: project cards get the provider-card treatment (icon, p-3, flex column) instead of a hand-rolled variant whose block layout left more padding above the name than below; form labels settle on text-emphasis; and the signup link sits under the primary button for anyone who does not have an account yet. Drop the "free" badge and the "Free on Supabase" line -- every option in the wizard is free, so neither told the user anything -- and say what the Supabase card actually does now that connecting an existing project is the default. Co-Authored-By: Claude Opus 5 (1M context) * refactor(frontend): one setup checklist and one Supabase step for every host The data table wizard, the instance database modal and the resource drawer each had their own version of the same two interactions, and they had already begun to drift: the wizard's Supabase resource shape was rebuilt by hand in the drawer, and the instance checks rendered with no notion of a step being in flight. SetupChecklist replaces LoggedWizardResult, whose only consumer was the instance modal. It adds the running state that component lacked, so a list driven by an endpoint that reports nothing until it returns still shows where it is. Both the instance checks and the Supabase provisioning stages render through it. SupabaseProjectStep owns picking or creating a project, and useSupabaseOauth owns the popup leg. Each host keeps only what is genuinely its own: the wizard saves a variable and resource then verifies the connection, the resource drawer fills in its own form. Both trigger authorization themselves, so a host can offer it a screen earlier than the step does. The lists load behind a spinner because which mode to open on depends on whether the account has projects; deciding that after rendering flipped the toggle under the user. Adds a kitchen_sink playground for the checklist so the animation and every failure position can be exercised without a backend, a superadmin, or a Supabase account. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): tidy the resource drawer around the Supabase entry point Connect Supabase was a hand-styled anchor carrying Supabase's brand hex values rather than a Button, and it sat in a row whose other controls had settled on unifiedSize md. Making it a Button meant SupabaseIcon had to satisfy IconType, so it now takes `size` (deriving height/width from it) alongside the string props its other callers pass. The manual resource form spaced every field 32px apart and WhitelistIp added another 16px of its own, which read as a gap rather than a rhythm. One gap of 16px, with the form itself given a little more separation from the description above it. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): stop Supabase resources coming up modified when first opened Resource forms fill in every unset property from the schema as soon as they render, so a postgresql resource saved without region, root_certificate_pem and use_iam_auth was dirty -- and had saved a draft -- the first time anyone looked at it. Write them with the rest of the value. SupabaseConnect also rebuilt the resource shape by hand instead of using the shared helper, which is how the pooler host format ended up in two places. Co-Authored-By: Claude Opus 5 (1M context) * feat(backend): record where a data table came from and whether setup finished edit_datatable_config replaces the whole datatables map and DataTable does not deny unknown fields, so anything the request omits is dropped without a word. origin and setup_incomplete would have been erased by any unrelated save; preserve_unmanaged_datatable_fields carries them -- and migrations_enabled, which had the same problem inline -- forward for entries that already exist, following renames. setup_incomplete is what lets a row be recorded before the resource it points at exists, so the wizard can write nothing until the user finishes. There is deliberately no intermediate state: the setup runs entirely in the browser, so nothing server-side could advance one. datatable_health probes every data table at once for the settings page and skips the incomplete ones, whose resource_path resolves to nothing yet. set_datatable_setup patches a single entry instead of resending the map. test_datatable_connection_value checks a connection the caller has not saved anywhere, which the wizard needs before it has written a resource. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): make destructive default and subtle buttons read red Both variants were neutral until the pointer arrived, then filled solid red: nothing marked the button as destructive until you were already on it. They now carry red text at rest, with a faded red border on default and a light red wash on hover, which is what the legacy red border style in the same file had always done. Three call sites passed color="red" alongside a design-system variant. getStyleClass returns before colour is read for accent, accent-secondary, default and subtle, so the delete-migration control, its modal confirm and the import-database button had all been rendering neutral. They pass destructive now. The dropdown variant strips the button's own border, and matched border-border-light literally -- a class the destructive style no longer contains. Co-Authored-By: Claude Opus 5 (1M context) * feat(frontend): rebuild data table setup around a read-only row The wizard gathers intent over two steps, reviews it on a third and writes nothing until Finish, so a billable Supabase project is created only once the user has seen what will happen. runSetup is also the retry: every step probes for its own result before doing anything, so running it again on a half-finished data table resumes instead of duplicating. Its steps are keyed rather than dispatched on their titles, where rewording one changed what it did. The settings row stops being an editable form with a dirty/save cycle. It carries the name, where the database came from, a health dot and two actions; everything rare moved into the gear panel, which also offers Finish setup for a data table whose wizard never completed. Manage is ExploreAssetButton, the control the ducklake list already uses, and the row and panel both link out to the underlying resource. supabaseResourceValue no longer assembles the pooler host from the region. aws-0-.pooler.supabase.com is wrong for any project Supabase allocated elsewhere, so the host, user and port come from the pooler config endpoint. Two data tables sharing one database also share _wm_migrations, which is probed unqualified, so the review step warns when the database being connected is already behind another data table. SupabaseConnect is deleted. The resource drawer uses the shared project step restricted to existing projects: creating one is a billed action and belongs in the wizard, which has somewhere to report what it did. The kitchen_sink checklist playground goes with it. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): fall back to a direct Supabase connection when the pooler cannot be read Reading a project's Supavisor config needs the database_pooling_config_read scope, which an instance's Supabase OAuth app may never have been granted. No retry recovers from that, and the wizard treated it as fatal: the user was left with an error and no way to finish connecting a project that was otherwise fine. resolveSupabaseConnection replaces the bare pooler read everywhere it happened. Asking for session pooling and failing now yields a direct connection plus the reason, which supabaseResourceValue already knew how to write. Nothing about the fallback is silent -- direct is IPv6-only, which is the whole reason session pooling is the default -- so the wizard warns on its review step and the resource drawer says so in its toast. The row is recorded before credentials are saved, so an origin claiming session pooling has to be corrected once a direct host is what gets written; the run patches it through set_datatable_setup rather than leaving the panel to report a mode nothing uses. Co-Authored-By: Claude Opus 5 (1M context) * feat(frontend): open the database behind a data table, and say when it cannot write Every database in the list now opens the surface that owns its credentials. A postgres one opens its resource in the editor drawer; a Windmill instance one opens the instance modal, which is where its setup checks, password rotation and drop already lived. Both are reachable from the row and from the panel's provenance list, and the provider icon moved inside the button so the whole thing is one target. CustomInstanceDbWizardModal targeted #content unconditionally, which put it underneath the panel drawer that now opens it. It takes a target, and the panel portals it to the body. The status column gains a third state. The probe reports privileges but nothing gated the dot on them, so a data table whose role cannot create tables showed as Connected and only failed when someone ran a migration. It reads "Limited permissions" instead, and opens the panel on the report carrying the GRANTs that fix it -- the settings page has already probed, so the panel takes that report rather than asking the user to run Test connection over work already done. fullyPrivileged is exported from the report component so the dot and the report cannot disagree about what counts as healthy. Co-Authored-By: Claude Opus 5 (1M context) * revert(frontend): keep the data tables settings table as it was The settings table and the setup wizard are two changes that only shared a file. Splitting them makes each reviewable: this branch keeps the wizard, and the read-only row, gear panel, health probe and clickable databases move to their own branch. The rows go back to the editable form with its pickers and save footer, still opening the wizard from Add a database. DataTableSettingsPanel, dataTableHealth and dataTableOrigin had no other consumers and go with them; the connection report stays, because the wizard shows it too. DataTableSettingsType keeps `origin`: the wizard writes it, and the review step reads it back to warn when two data tables would share one database and therefore one _wm_migrations table. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): confirm before dismissing the data table wizard mid-setup Closing was guarded while a run was in flight and unguarded before one, which is backwards: a run leaves a row to resume from, whereas a backdrop click on the review step threw away the project, the pasted password and the folder with nothing to recover them from. Backdrop, Escape and the close button now go through one path that asks first. It only asks when there is something to lose -- no provider chosen yet, or a run that already produced a result, closes immediately -- so the dialog does not become something to click through. Continue in the background still leaves in one click; that exit was always the deliberate one. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): stop the wizard claiming the resource folder controls who can use a data table "Who can use this database" was wrong. Every path that resolves a datatable:// reference -- both executors and the agent-worker endpoint -- reads the resource unchecked, by workspace and name. A resource in u/admin is usable by everyone's scripts. The folder governs who can see and edit the connection, and who can reference the resource directly in a SQL step; neither is who can use the data table. The wizard was contradicting the tab's own description two screens later. The folder select and name field become one Path picker, the same one the resource, variable and script forms use, so the review step reads as a resource path rather than a permission choice. Its initialPath is snapshotted when the step opens: Path seeds itself from it, and a live value fights the typing. Finish now also gates on Path's error, so a taken or malformed path stops the run before it writes anything. The button that opens all this says "Add a data table" -- the data table is what you get; the database is a detail chosen along the way. Co-Authored-By: Claude Opus 5 (1M context) * revert(frontend): move the destructive button restyle out of the wizard PR This reverts 3881e4d8ea. Making default and subtle destructive buttons red at rest changes every existing caller of the prop -- the workspace integrations, AI skills, workspace creation and the instance database drop -- so it is a design-system change, and the call sites it fixed are the migrations list and the database manager. None of that is the setup wizard. Nothing on this branch passes destructive any more, so it leaves with no loose ends. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): make the wizard stepper navigate the steps it already offers Stepper dispatches a click and paints cursor-pointer on every reached step, but the wizard never listened, so the breadcrumbs invited a click and did nothing. They now reach any step already passed, in either direction: going back to check something should not cost the progress, which means tracking the furthest step reached rather than the current one. Forward movement still only happens through the primary action, so a step is never reachable without having been validated -- and changing the intent revokes the steps ahead of it, or Finish could run against a review built from something the user has since edited. The five places that cleared the probe on an edit now do both through one call. During a run nothing is reachable, and the stepper says so rather than showing a pointer over steps that will not respond. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): restore the data tables description lost in the branch split The rewritten description went into DataTableSettings.svelte shortly before that file was restored wholesale to its pre-rebuild state, so it left with the row rework it had nothing to do with. The tab went back to describing the plumbing -- a fully managed PostgreSQL database, reachable from the SDK -- which never answered the question a new user actually has: why this rather than a Postgres resource. It leads with what a data table is, then the two things a resource cannot do -- nobody needs the credentials to query it, and the name can be pointed at another database without editing anything that uses it -- and closes with what Windmill runs on top. Both middle claims are the ones every resolution path backs up: datatable:// resolves by workspace and name, unchecked. Co-Authored-By: Claude Opus 5 (1M context) * fix(backend): say what is missing when a $res: or $var: reference does not resolve Both interpolations fetched with fetch_one and mapped the error through to_anyhow, so a reference to something deleted surfaced as "no rows returned by a query that expected to return at least one row @workspaces.rs:2169". It names neither the kind of thing that was missing nor its path, and it is what a data table pointing at a deleted resource reports. They now fetch_optional and return NotFound naming the path, and datatable resolution adds the data table on the way out: the caller asked for one by name, and a bare "resource f/x/y does not exist" leaves them to work out which of them points at it. The health probe is new, so this string had only just become something users read. Co-Authored-By: Claude Opus 5 (1M context) * feat(frontend): gate the data table wizard behind a dev flag The wizard only appears with `dataTableWizard` set in localStorage; without it the settings page keeps the inline-row flow it had before this branch, down to the empty-state copy and the "New Data Table" button, and the wizard component is not mounted at all. The existing e2e suite drives that button, so the default-off flag is also what keeps it green. Step 2 of "your own database" becomes one list rather than a segmented control: the workspace's Postgres resources, then a New resource card that expands in place. A connection string is not an alternative to a resource, it is how one is written, and the old layout taught otherwise. The card holds the same connection as a string or as fields and carries values across when you switch, so `parse` and `compose` have to be inverses -- hence the percent-encoding on both sides, which also fixes a password containing `@` silently corrupting in the resource form. The Supabase step now uses the same shape. Names and paths are checked as they are typed rather than at the end of a run that may have created a billed project first: the data table name against the charset `edit_datatable_config` enforces, the instance database name against what `setup_custom_instance_db` will accept, and the resource path against both the resource and variable namespaces, since the run writes to both and both writes upsert. `test_datatable_connection_value` refuses `$var:`/`$res:` in its body. It feeds `transform_json_value_unchecked`, which resolves references with no permission check of its own, so an admin could otherwise have had the API server decrypt any workspace secret and hand it to a host the same request chose -- without the audit trail a variable read leaves. Callers testing something unsaved hold the literal value already. Alert, SetupChecklist and postgresConnectionString change for everyone, not just behind the flag: body-only alerts no longer reserve an empty title row, the checklist can nest the checks a step is made of, and the connection-string parser is shared with the resource form. Co-Authored-By: Claude Opus 5 (1M context) * chore: pin ee-repo-ref to the EE branch merged with EE main The Supabase proxies the wizard calls are still unmerged, so the ref cannot be an EE main commit yet; it now names that branch merged with EE main rather than the branch alone, which was nine commits behind and would have been built against a CE main it never saw. Co-Authored-By: Claude Opus 5 (1M context) * feat(frontend): gate the supabase resource path behind the dev flag * test(frontend): pin connection string parsing to libpq behaviour * fix(frontend): keep the supabase resource link off the popup callback path * refactor(frontend): load the supabase resource dialog only behind the flag * fix(frontend): refuse a resource path the wizard run does not own * fix(frontend): let a failed data table setup be corrected without losing what it made * fix(frontend): let a failed setup reuse the resource path it claimed Co-Authored-By: Claude Opus 5 (1M context) * fix(backend): record the two data table connection tests in the audit log Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): use Section for the data table wizard advanced group Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): read connection strings the way libpq does Co-Authored-By: Claude Opus 5 (1M context) * fix(backend): pin the ee ref back to a commit this branch can build Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): keep a failed setup's claims across the redirect and rollback Co-Authored-By: Claude Opus 5 (1M context) * fix(backend): probe a data table with the auth mode the worker will use Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): keep every part of a connection string through the round trip Co-Authored-By: Claude Opus 5 (1M context) * refactor(frontend): give a setup run one record of what it created Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): mark a resource claim by edited_at, not its creator Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): mark every claim by revision, and keep an unconfirmed project's secret Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): refuse to test or save behind a connection string that will not parse Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): refuse a connection string carrying options the resource cannot hold Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): allowlist the connection-string parameters a resource can honour Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): guard every created Supabase project, not just the last one Co-Authored-By: Claude Opus 5 (1M context) * feat: three-step wizard for importing a hub project Importing used to be a single page that inherited whatever workspace happened to be active, with no way to say where the project should go — the hub cannot know, since it only ever links to *an* instance. `/projects/import` now asks: which kind of destination, which workspace, then imports. Nothing is created, switched or written until the last step runs. The wizard's state is a plain value in the URL (`importWizard/plan.ts`), so the back button, the stepper and the Back control are the same operation, and none of them can strand a half-created workspace — there is no state anywhere else to unwind. `importWizard/execution.svelte.ts` is the only code that acts on a plan: it runs create → fetch → import as an observable task list, reuses what already succeeded when retried, and offers to delete the workspace it created if the run stops early. Its UI needs — the data table migration review — are injected, so it holds no components. The old `/projects/install` becomes a redirect: hubs upgrade on their own schedule and a self-hosted one may keep pointing at it for a long time. Co-Authored-By: Claude Opus 5 (1M context) * fix: let the import wizard survive sign-in and a missing workspace Signing in with `rd=/projects/import?hub=...` dropped the destination: the login redirect only honours `rd` verbatim for `/user/workspaces`, so anyone with more than one workspace landed on the workspace picker instead — the page the wizard exists to replace, asking the question it was about to ask. Both copies of that logic now allow the wizard through. The root layout's "no workspace selected" redirect skips the wizard too. It picks the destination itself and may end in a workspace that does not exist yet, so bouncing it to the picker forces the very choice it is there to make. Co-Authored-By: Claude Opus 5 (1M context) * chore: bench page for the import project card /kitchen_sink/import_project_card renders the card against fixtures — a real project, an oversized one, a minimal one — so its layout can be judged without a hub running or an import in flight. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): do not warn about renaming an item that does not exist yet Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): make the review step read as one list of what will exist Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): keep the picked Supabase project across the redirect, reject connect_timeout Co-Authored-By: Claude Opus 5 (1M context) * fix: check the data table connection from a worker, not the API server The wizard's connection check ran on the API server through two endpoints added for it. That server is a different machine with a different identity, so the answer was about the API server rather than about the worker that will run the queries: a host reachable from one is not necessarily reachable from the other, and IAM RDS and Azure workload identity authenticate as whichever process opens the connection. Run the privilege query as a preview job instead. A job goes through the worker's Postgres executor, which is where `PgAuthMode::of` already picks the authentication mode, and it takes either a resource value or a `$res:` path exactly as a Postgres step does. Postgres composes the suggested GRANT statements through `format('%I')`, so identifier quoting stays where it is already implemented. Removes `test_datatable_resource_connection` and `test_datatable_connection_value`, and `connect_as_the_worker_would` with them. Co-Authored-By: Claude Opus 5 (1M context) * refactor: fold check_datatable_connection back into its only caller The helper was split out so the two connection-test endpoints could share a body. Those endpoints are gone, leaving one caller. Co-Authored-By: Claude Opus 5 (1M context) * revert: keep the data table connection check schema inline It was lifted into components so three endpoints could share it. Two of those are gone, so it is back to one user and the extraction changes nothing. Co-Authored-By: Claude Opus 5 (1M context) * fix: restore openapi.yaml to the branch point The previous commit restored main's tip rather than the merge base, which carried three unrelated main-only changes into this branch: the resource mcp_tools truncation fields, the execution_mode description, and a version bump. Co-Authored-By: Claude Opus 5 (1M context) * refactor(frontend): drop four effects from the data table wizard Each was doing work a derived, a load callback or a real entry point does better. - The name conflict is kept with the name it was raised for and derived from it. As an effect it was correct only because it never read what it wrote: the pre-flight sets the message and the effect does not re-trigger, so adding a read would have cleared it the instant it appeared. The message now also comes back if the taken name is retyped, which is what the server will say. - The default resource selection is seeded inside the fetcher that loads the list, where "has the fetch settled" cannot be asked wrong. - Reset-on-open becomes an exported open(), called by the settings page, so a fresh run is set up by the act of opening rather than by a flag emulating mount. - The OAuth connects and the folder list become resources; supabaseAvailable and folders are derived from them. defaultFolder takes the list rather than reading it, so the fetch can seed off its own result. Leaves the debounced path check, which is async with an out-of-order guard. Co-Authored-By: Claude Opus 5 (1M context) * refactor(frontend): drop three effects from the Supabase branch - useSupabaseOauth reports success as onAuthed, alongside the failures it already reported. SupabaseResourceConnect was watching `authed` to find out; it takes the callback instead, keeping the guard that stops an authorization started elsewhere on the page from opening its dialog. - SupabaseProjectStep loads its orgs and projects through a resource keyed on the token, so the `loaded` latch goes and re-authorizing reloads rather than keeping the lists from the expired session. - SetupChecklist records what the user toggled and derives the open state from it, a failed step defaulting to open. Recording the open state instead needed an effect to force it, and that effect re-ran on every progress update, so a description closed while anything was still ticking reopened. A close now holds for the life of the checklist, including across Try again. Leaves the message listener, which subscribes to another window. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): confine the modal restyle to the wizard, and trim the comments The wider side padding and lighter dialog heading were changing all 17 Modal2 dialogs to suit this one flow. They move behind an opt-in `formStyling`, taken by the three dialogs this branch owns; every other Modal2 renders as it did. Also drops two comments that cited a design approval rather than a constraint, and shortens the blocks that had grown past the four lines AGENTS.md asks for. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): use the accent token for the wizard's links `text-blue-500` is the marketing blue `#3B82F6`, which brand-guidelines.md rules out in the app interface. Co-Authored-By: Claude Opus 5 (1M context) * chore: point ee-repo-ref at the EE branch head Picks up EE main, which the branch now needs, and the Supabase proxy auth fix. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): read sslmode by name, and stop decrypting a secret to date it - `sslmode` was found by searching the query text, so it also matched inside another parameter's value: `?application_name=sslmode=disable` passed the allowlist on the parameter name and then parsed as a request to turn TLS off, which both the wizard and the resource form saved and probed. Parsed with `URLSearchParams` by exact name, with a test. - `secretMark` read the variable with `decryptSecret` defaulted to true, so every write decrypted a secret nothing reads and recorded the decryption -- including someone else's on the retry about to refuse it. It wants only `edited_at`, which is returned either way. - The probe gave up at 15s while the worker allows its Postgres connect 20s, so a host that accepts the connection and never answers was cancelled and reported as a missing worker rather than a failed connection. - The create-mode region and project name did not report an intent change, so renaming a project after a name collision left the failure naming the old one. - Two comments described the code as it was before the claim mark became a revision, and a doc comment outlived the field it documented. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): read connection parameters the way libpq does One reader for both the parser and the allowlist, since they disagreed about what a string says in two ways that both ended in a weaker connection than was pasted: - `URLSearchParams.get` takes the first of a repeated parameter and libpq takes the last, so `?sslmode=disable&sslmode=require` was read as `disable`. - The allowlist folded the parameter name and the parser did not, so `?SslMode=verify-full` was refused by neither and honoured by neither, and saved as the `require` default. The parked Supabase run is now handed to `open()` rather than read back off the `resume` prop it was just assigned to, so restoring it does not depend on when that prop reaches the component. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): keep connection parameter names case-sensitive libpq does not fold them: `?SslMode=disable` is rejected as an invalid URI query parameter rather than read as `sslmode`, which a local server confirms. Folding made Windmill accept and honour a string Postgres itself refuses; naming the parameter instead tells the user why it cannot be stored. The last-value-wins rule for a repeated parameter is unchanged, and matches what the same server does. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): seed the Supabase organization from the project it selects The loader took `orgs[0]` independently of the project it seeded, so an account whose first project sits outside its first organization had the review step name an organization the database does not belong to. Picking a project by hand already derives it; the seeding now does the same. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): let the probe report an empty search_path instead of failing on it `format('%I', NULL)` raises rather than returning NULL, so a role whose search_path names no valid schema failed the whole privilege query and was reported as an unreachable database. That is the one case `fix_search_path` exists to name, and it never reached the user. Verified against a local server with `SET search_path = ''`. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): say which of the two refusals a connection string hit Making parameter names case-sensitive gave `unsupportedConnectionParam` two reasons to refuse, and the single message explained only one. `?SslMode=` was answered with "Windmill cannot store SslMode on a Postgres resource", which is false twice over: sslmode is exactly what the resource stores, and the string asks for nothing because Postgres rejects the URI. It now names the spelling when the parameter is one we keep, and the storage limit otherwise. The folder-list guard also still read the `resume` prop that `open(parked)` was changed to stop trusting, so the resumed path now comes from whatever `reset` was handed. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): leave the Supabase organization unset when the lookup misses Falling back to the first organization named one the seeded project is not in, since `supabaseSummary` prefers `intent.org` over the project's own. Unset, it falls through to the project's organization identifier — the right one, spelled as a slug rather than a name. Co-Authored-By: Claude Opus 5 (1M context) * fix: harden the import wizard and put it on the design system Review fixes, then the parts of the wizard that were hand-built where the design system already had an answer. Correctness: - Hub SVGs are sanitised with DOMPurify before `{@html}`. The earlier comment claimed the markup came from the hub's own icon package rather than user input, which the custom-URL feature makes false: the hub is whatever address the user typed. - The run owns navigation while it is in flight. The stepper refuses to move, `beforeNavigate` cancels browser back/forward, and unmounting resolves a pending migration review so the executor cannot hang waiting on a component that is gone. - The folder edited on the last step reaches the executor, so a retry after changing it imports where the field now says. - `validateWorkspaceId` and the workspace-entry pair (`listUserWorkspaces` then `switchWorkspace`) are extracted, so the wizard and the real create form cannot drift on what an id is or on what entering a workspace means. Design system: - The destination tiles are `RadioCard`, which gains `showRadio` and a snippet `description`; the wizard turns the glyph off because the border and tint already say which one is picked. `RadioCard` now also carries `role="radio"` and `aria-checked`, which it had neither of, and marks its selection with `surface-accent-selected` — the token `FileExplorer`, `TriggersTable` and `RunnableRow` all use for the chosen row. - Form labels follow `brand-guidelines.md` — sentence case, real `