mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-10 08:07:03 +00:00
a6abf2c8a744e9ee6acf4830cdfbb84f2f95cb36
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fd35b47658 |
feat: create the cloud workspace in onboarding, and teach the empty home (#10959)
* [ee] feat: create a personal workspace on cloud signup instead of the demo invite Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * feat(frontend): land cloud users in their workspace after onboarding Cloud signup creates exactly one workspace for the new user, so the picker that followed onboarding was a page with a single choice on it. Switch to that workspace and go to the home page instead, falling back to the picker whenever there is a real choice: an invite to accept, several workspaces, or none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * chore(frontend): remove the tutorial system Deletes the guided-tour feature: the tutorials directory, the per-editor wrappers, the home banner and button, the /tutorials route, and the driver.js dependency they were built on. Also removes what only existed to serve them — the `tutorialsToDo` / `skippedAll` / `isCurrentlyInTutorial` stores, the `disableTutorials` prop chain through the flow editor, the `?tutorial=` deep links, PopupV2's clickOutside exemption for the driver popover, and the selector-anchor class on the flow editor tabs. The backend `tutorial_progress` endpoints and table stay: nothing calls them now, and removing them is a public-API break plus a migration that would drop existing progress. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * feat(frontend): suggest Hub projects on an empty workspace home A workspace with nothing in it showed only "Welcome to Windmill". Replace that with a grid of ready-made Hub projects to import, and hide the search box, kind toggles and the sort/filter row while the workspace is empty — they would act on an empty list. A search that matches nothing still keeps its controls and shows the no-match message. The project list is seeded locally for now; the Hub endpoint that ranks them is not there yet, and Import is still a placeholder. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * feat(frontend): make a new cloud workspace the thing onboarding produces Signup already makes a personal workspace; nothing let its owner name it, and a user who ended up without one landed on a workspace picker whose only action was a button. Onboarding now ends on the workspace itself, and the empty home that follows says what a workspace is for rather than "Welcome to Windmill". Onboarding gains a third step that names the workspace signup created, prefilled from the login provider's name or the email local part — `ruben@…` gives "Ruben's workspace". Skipping the survey reaches it too: the questions are ours, the workspace is theirs. Advanced settings swaps in the real creation form for someone setting up for a team. The workspace picker stands down when it has nothing to offer: no workspace to enter and no invite to accept leaves one action on the page, so the page is that action — one field, prefilled, "Create workspace". Both hand-overs hold a loading state for 900ms and the app fades in behind them, so creating a workspace reads as something that happened. The empty home draws three static placeholder rows in the shape of real ones, under a caption offering a template or the New menu. "Start from a template" opens a popover listing the hub's projects, most-starred first, preloaded when the empty state renders and paged as you scroll. Picking one opens the import wizard in a dialog: its two destination steps are already answered by being in a workspace, so it starts at the import itself and pages to the credentials step with the animation the paged-modal pattern provides. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix(frontend): gate the empty state and repair the derived workspace id Review findings from the first round. The empty state offers a template import and the New menu, and neither checks a permission — so an operator, or a workspace whose direct-deploy protection cleared `showEditButtons`, was offered both. It now sits behind the same gate as the create menu thirty lines above it. `validateWorkspaceId` answers with the *reason* an id is unusable, so `if (validateWorkspaceId(next)) break` stopped on the first invalid candidate and returned it: someone named Global got the reserved `global`, and a 50-char seed got a taken one. Invalid candidates are skipped instead, and when none works the caller opens advanced settings rather than posting a name the server refuses. Also: `rd` may be absolute (the CLI login sends one) and `goto` refuses those, which would strand the caller on the "Creating …" screen with the workspace already made; the hub host is parsed defensively, since the instance setting is whatever an admin typed and `new URL` was throwing in render; `insert_workspace` says which authorization its callers still own; and the two arrival animations' comments now describe when they actually play. Tests for the two pure helpers the review named: `defaultWorkspaceName` and `hubProjectDescription`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix(frontend): use unifiedSize on the new onboarding step's buttons `size` and `color` are deprecated on Button; the new step copied them from the survey steps above it. AGENTS.md: deprecated props survive at old call sites, copying one forward is still a bug. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix(frontend): let Skip wait for the workspace onboarding names `loadWorkspaceStep()` was fired and dropped, while Skip and the use-case Continue branch on `ownWorkspace` in their `finally`. Skip awaits one POST that starts after those two GETs and can finish before them, so a first-frame Skip fell through to `leaveOnboarding()` and landed in the workspace with the backend's name — the step this flow exists for, silently gone. Both exits await the load; `isSubmitting` already covers the wait. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * refactor: create the workspace in onboarding rather than at signup Signup no longer makes a personal workspace, so the last onboarding step creates one instead of renaming it — the same one-field `SimpleCreateWorkspace` the workspace picker falls back to, so a user who leaves onboarding early meets the form again rather than something new. The id now comes from the name they type rather than from their email, and there is one creation path instead of two. `insert_workspace` goes back to being private: the extraction existed only so the EE signup path could call it, and nothing outside `create_workspace` does now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix(frontend): count a pending invite as somewhere to go Onboarding read `listUserWorkspaces`, which returns membership. An invite is a `workspace_invite` row until `accept_invite` runs, so an invited teammate reached the last step owning nothing and was walked into creating a personal workspace, with the invite nowhere on the page. Invites are fetched alongside the workspaces, the way the picker already gates the same decision. A failed load now reads as placed rather than not: the picker can work the decision out, while the create step's only way forward is creating. The create form reports when it is handing over, so the Previous button beside it stands down for the ~900ms rather than offering a way back out of a workspace that now exists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix(frontend): open the template picker downward and size it from the popover The popover's default positioning caps its height to the viewport, and the list inside it carried a fixed one, so a capped box overflowed its own frame — visible with the AI composer hidden, where the caption sits high and `placement: top` left almost no room above it. It opens downward now, with flip fallbacks, at a definite `min(72vh, 520px)`; the list fills what the header leaves, which is still the definite height it needs to page. `creating` on the create form becomes `onCreatingChange`: `$bindable(default)` on an optional prop is banned, and this is something the form reports rather than state it shares. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * refactor(frontend): drop the InfiniteList containerClass prop Added for the template picker, which turns out not to need it: DataTable's own container is already `h-full`, so `containerClass="h-full"` merged to nothing and the height the list pages against comes from the flex chain above it. A prop with no effect at its only call site is public surface for free. InfiniteList is back to what it was. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix(frontend): stop a dismissed project import from running on Modal reports dismissal only through its bindable `open` — the X, Escape and the backdrop dispatch neither `confirmed` nor `canceled`. Bind it, so clearing `pick` follows the dialog closing: re-picking the same project opens it again, and a run still in flight is abandoned with a toast instead of writing to the workspace with no UI in front of it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix(frontend): stop a dismissed project import from running on Modal reports dismissal only through its bindable `open` — the X, Escape and the backdrop dispatch neither `confirmed` nor `canceled`. Bind it, so clearing `pick` follows the dialog closing: re-picking the same project opens it again, and a run still in flight is abandoned with a toast instead of writing to the workspace with no UI in front of it. Also mark the inline-link buttons as sanctioned rather than oversights, and give Log out `text-accent` instead of `text-blue-500`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix(frontend): keep the stopped-import toast off the Finish path `done` survives a retry, so Finish is clickable while the run is going again, and its own closing reaches the same falling edge the X does. Abandon the run either way; say it was stopped only when that is what the click asked for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * feat(frontend): reach the hub importer from the create menu, and count the funnel The picker only existed inside the empty state, which disappears as soon as a workspace holds one item — nothing else in the product linked to `/projects/import`. New → Import now offers a hub project, opening the catalogue in a dialog: a popover anchored to an item inside an open dropdown leaves two melt layers arguing over focus. The list and the import dialog move up to ItemsList, so one dialog serves both doors. `template_setup` records how the credentials step ended — `filled` only when nothing was outstanding, `skipped` carrying how many rows were left — and `template_abandon` records where a dismissed import was given up. `template_picker_open` gains a key naming the entry point. Also on the workspace picker: logging out is a text link on the line that says who you are and an item in the settings menu, rather than the page's accent action, and onboarding's Previous joins the row it belongs to instead of hanging under the button that finishes the form. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix(frontend): make the import counters answer what they claim to `template_abandon` folded a landed import closed with the X into `idle`, the bucket read as "opened this and bounced" — three outcomes in one number. It gets its own `done` stage. `template_setup` counted skipped rows through `value`, which is an increment: `skipped` accumulated rows while `filled` and `none` counted imports, two units in one counter with no way to recover one from the other. The row count becomes a bucket in the key, so every event is one import and the buckets compare. The import dialog also asked for the hub URL settings at init, and the home list now mounts it for everyone on every arrival — two GETs for a string only the project card renders. Deferred to the first pick. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix: sanitize the inferred username, and stop counting an unread setup step as clean `loadUsernamePolicy` derived a username by stripping dots, so `O'Connor` and `alice+demo` both produced values the `proper_username` constraint refuses — posted invisibly by the simple form, which then failed with nothing on screen explaining why. `usernameFromName` keeps only `[\w-]` and answers undefined when nothing usable is left, which is already the form's cue to open the full one. The credentials step offers Finish when the export could not be read, since it cannot tell what is outstanding — and that landed in `template_setup` as `filled`, the bucket meaning the step came out clean. It reports whether it checked anything, and an unread step counts as `unchecked`. Also drops an orphaned `.sqlx` entry left by the create-at-signup query this branch abandoned, and rewrites the stepper's first-frame comment, which argued from a meaning of `resourceCount` that main has narrowed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix(frontend): cap the inferred username at what the column holds `usr.username` is VARCHAR(50) while the provider name and email it is derived from run to 255, and `create_workspace` inserts the value untruncated — so a long first name failed the same way the invalid characters did: posted invisibly, refused on insert, with nothing on screen naming the field. Undefined instead, which the form already routes to the full one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix(frontend): show the import note where it applies, and drop onCreated's unused id The note is about landing on top of what a workspace already holds, so it belongs wherever the destination is an existing workspace. The route already read it that way; the dialog, which always imports into the current workspace, was hiding it. It costs one collapsed row. `onCreated` was typed as taking the new workspace id, and the advanced branch passed `''` because `CreateWorkspaceInner` does not report one. No caller reads it — the form has already switched to the workspace by then — so the argument goes rather than the lie staying. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix(frontend): keep the empty-state toolbar reachable, and name the view counters "Empty" here means the default listing found nothing, and a workspace whose items are all archived looks exactly the same. The searchbar carries "Only archived", so taking it off the pointer left those items unreachable without hand-writing a query URL. Dimmed still, never `inert`. The disclosure named the counters that fire on a creation or an import and not the three that fire on merely seeing the empty home or opening either picker. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix(frontend): respect disable_hub in both hub-project entry points An instance with the hub turned off still got the catalogue preloaded on every empty home and an "Import a hub project" entry in the create menu — an outbound request the operator has said not to make, and a door to somewhere unreachable. Both now observe `disableHubStore`, the store the script and flow hub pickers already read. With the hub off the caption reads "Create a new one." rather than continuing a sentence whose first half is gone. The telemetry disclosure also scoped the create menu and picker counters to the empty home, when both fire from the toolbar in a populated one, and said a creation was recorded when what is recorded is the menu opening. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * refactor(frontend): drop the catalogue preload rather than gate it twice Warming the hub catalogue when the empty state rendered bought the time between the caption appearing and someone clicking it, and cost two defects: the request fired on instances with the hub turned off, and the gate added for that raced `disable_hub`'s own load, which starts false and stays false if the settings request fails. The picker fetches on open instead. Measured: nothing before the click, one request after it, 411ms to a filled list. `disableHubStore` still hides the link and the menu entry, which cost no request and correct themselves if the setting lands late. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * docs: count the home actions, and state the lazy-fetch constraint without its history The telemetry doc's tally is maintained by hand and main had just moved it; this PR adds a feature, so it reads 48 across eighteen with `home` in the list — verified against the pinned EE ref rather than counted by eye. The empty state's comment narrated a preload that no longer exists and the defects it caused. What a future reader needs is the constraint: `disable_hub` loads asynchronously, so a fetch from here goes out before the setting forbidding it is known. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix(frontend): stop the prefill overwriting a typed name, and gate submit on the policy `load()` assigned the suggested name unconditionally, so a name typed while its two requests were in flight was replaced a moment later. It now yields to anything already typed. Nothing may be submitted before the username policy lands either: `automateUsername` starts at the common case, and posting that guess to an instance that derives no usernames sends none where one is required. `policyLoaded` gates both the button and `create()`, and is set in a `finally` so a failed load leaves the form usable rather than wedged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix(frontend): settle the username policy on failure instead of guessing it `policyLoaded` was set in a `finally`, so a failed policy load unblocked the form with `automateUsername` still at its default — the exact submit the flag exists to prevent. The failure now hands over to the full form, which asks for a username outright rather than inferring one, so the flag is never true while the answer is still a guess. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix(frontend): answer the username policy instead of rejecting it Three rounds of this bug moved between call sites because the shared loader rejects when it cannot read `automate_username_creation`, leaving each caller to guess — and both guessed "automated", which hides the username field and posts none to an instance that derives none. `loadUsernamePolicy` now answers "ask for one" in that case, so `SimpleCreateWorkspace` and `CreateWorkspaceInner` both render a field someone can type into rather than submitting a guess. An instance that does automate ignores a username it was sent, so asking is safe either way. The prefill and the policy are settled apart now too: a failed `globalWhoami` costs the suggested name and nothing else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix(frontend): block creation when the username policy is unknown There is no safe default. `create_workspace` refuses a username on an instance that automates them and requires one on an instance that does not (`workspaces.rs:5820`), so a client that cannot read the setting has two request shapes available and the server rejects both. Last round's "ask for one" was as wrong as the "automated" guess it replaced. So the loader reports the failure instead of inventing an answer, and the form says so: Create stays disabled, with a line explaining why and a link to try again. Verified in the browser both ways — unreadable policy disables Create and shows the message, a healthy load prefills the name and enables it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix(frontend): close the advanced-settings bypass while the policy is unknown Create was gated on knowing whether the instance derives usernames, and the link beside it went to a form with no such gate — so the way around the block sat next to it. It is disabled until the policy is known, with a title saying why. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix(frontend): reload the list on dismissal, and never re-offer a workspace that exists Closing a landed import with the X left the home list stale: only `finish()` reloaded it, so a workspace that now holds a project kept showing its placeholder rows. A run that started wrote items whether it finished, was abandoned or failed partway, so any dismissal after one reloads. Creation reported failure for a failed *list refresh* too, and handed the form back — where a retry picks the next free id and creates a second workspace. Once `createWorkspace` returns, nothing may report failure: the refresh is logged if it fails, and the hand-over proceeds, since the workspace is real either way. The disabled-link tooltip also claimed the settings could not be read during the ordinary load, before anything had failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix(frontend): reload after the abandoned run stops, not when it is asked to `abandon()` stops the run at the next phase boundary; the request already sent still lands. Reloading the list at that moment could read it before that write committed, leaving the caller stale again — the thing the reload was added to fix. It now waits for `running` to clear, which is immediate for the common case of dismissing a finished import, with a cap so a run that never settles still ends in a reload. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix(frontend): give the list reload one owner, taken by both exits Finish reloaded immediately while dismissal waited for the run to stop, so Finish pressed during a retry — `done` survives one, which is what makes the button clickable then — read the list mid-write, and its `finishing` flag stopped the deferred reload from correcting it. Both exits now go through the same wait. One reload per closing, always after the writing stops, whichever way the dialog was left. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * feat(frontend): give ImportExecution a whenIdle(), and await it instead of polling The reload waited on a 250ms poll of `running` with a 15s cap, because the modal receives the execution after `run()` was already called and so holds no promise to await. The cap was its own hole: a write slower than 15s reloaded early, and nothing followed. `run()` now keeps the in-flight promise and `whenIdle()` hands it out — resolved when nothing is being written, immediate when no run is in flight. The modal awaits that: no poll, no cap, no window. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix(frontend): back the settlement reload with a bound, and keep reporting run failures `installProject` writes serially and takes no signal, so a request left pending after earlier items committed leaves those invisible until the next page load — `whenIdle()` alone never resolves for it. A bound now reloads once in that case, *without* replacing the settlement reload: replacing it was the flaw in the timeout this grew out of, so a hung run reloads on the bound and again if it ever finishes. `whenIdle()`'s rejection handler also swallowed the only report an unexpected throw had — `#runInternal` has no catch of its own, and a throw outside its inner ones leaves a stalled run with nothing on screen. It logs now instead of discarding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * feat(frontend): say when a workspace holds only archived items A workspace whose items are all archived read as empty, because the placeholder is decided by the default listing. Reaching those items then depended on the toolbar, which is why it had been left interactive while dimmed — and that let a kind toggle replace the invitation with "no items found" on a workspace that really was empty. The state is named instead. When the default listing comes back empty, one request asks whether anything archived exists, and the placeholder says which of the two it is: "Everything in this workspace is archived" with a link to show them, or the ordinary invitation. Held until that answer lands rather than drawn and swapped, since the wrong one claims the workspace is empty when it is not. The toolbar is dimmed and `inert` again, its original design: the archived case now carries its own way in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix(frontend): make the archived route independent of write permissions Three ways the archived-only placeholder failed to deliver what it promised: Reading archived items is not a write, but the notice offering them sat behind the create-permission gate — so an operator, or a workspace whose direct-deploy protection cleared `showEditButtons`, got "no items found" over items it could see and a toolbar now inert. The gate governs the create actions alone; the notice is shown to whoever the probe found something for. The probe answered once per workspace and was never invalidated, so archiving the last item left a cached "nothing archived" claiming the workspace was empty until a page load. `reloadItemsAndCounts` clears it. And it omitted `includeWithoutMain`, which the backend reads as excluding library scripts — a workspace holding only archived ones answered "empty". Always true here: hiding library scripts puts a filter in `activeFilters`, which `workspaceEmpty` requires to be empty. `whenIdle()` gains the two tests its contract deserves, since the reload correctness three rounds argued over rests on it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix: harden the hub proxy, the workspace picker's gating and the rd hand-off Findings from four local review passes over the branch: - `list_projects` refuses when `disable_hub` is set, and `is_public_hub` now compares the parsed host, so no spelling of the public hub (mixed-case scheme or host, port, trailing dot, userinfo) forwards a member's bearer token there. Covered by a unit test table. - The workspace picker waits on `usersWorkspaceStore` as well as `workspaces`, which derives to `[]` while the store is unloaded; with the create-form latch, one such frame swapped a member's picker for the create form until reload. - `refreshSuperadmin` takes `force`, and the picker uses it: a `false` left over from a logged-out load decides whether the page is a picker or a create form. A cancelled call no longer publishes `false` over the live request's answer, and only its own request's handle is cleared. - `rd` is sanitized once where it is derived rather than at each of the four hand-offs, so an absolute target keeps the OAuth callback's allowance and `https://evil.example/` is dropped. - The archived-items probe answers "unknown" on failure, which keeps the ordinary caption and leaves the toolbar reachable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * fix: answer the round-33 nits on the hub route and the empty state - `empty_state_view` is no longer logged for an archived-only workspace, which is not the state the counter measures. - `detail`'s fetcher keeps its last answer in a local instead of reading `detail.current`, a self-reference that typed the resource `any`. - `list_projects`' comment, including its authorization contract, is back on the handler rather than on the predicate inserted above it. - `listHubProjects` documents the 400 an instance with the hub disabled returns. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 * feat(frontend): keep the operator onboarding tour Operators cannot create anything, so the home page is the whole product to them and its three tabs are worth naming. The tour that did that is the one piece of the removed tutorial system that still has an audience. Restored trimmed: driver.js, the driver wrapper and its controls, the `.driver-popover` styling, and a module for the progress bit. The catalogue machinery it used to sit in — the config, the role gating, the router, the banner and the tutorials page — stays deleted, so the five steps are reached directly instead of through a registry of one. It runs on an operator's first home page visit and is recorded as seen however it ends, including navigating away; afterwards it is in the sidebar menu under Take the tour, which is where the last step points. Progress uses the surviving `tutorial_progress` route, slot 6, read-modify-written so the slots of the removed tutorials keep their state. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JirHCYVR6qg7Xqe4PcZ1KG * fix(frontend): dim the home toolbar only where the placeholder replaces it Standing the toolbar down depends on something else offering a way onwards. An operator in a workspace that is simply empty gets no placeholder — they cannot create, and there is nothing archived to reach — so the search and the kind toggles were the only controls on the page, dimmed to 40% and `inert`. They now follow the placeholder rather than emptiness, which also stops the operator tour spending three of its five steps highlighting controls this page had greyed out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JirHCYVR6qg7Xqe4PcZ1KG * docs(frontend): put the inline-link rationale on the first link, not the third The caption's three links share one reason for being bare `<button>`s, and it was written on the last of them. A reader — or a reviewer — meets the archived one first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JirHCYVR6qg7Xqe4PcZ1KG * fix(frontend): keep private hub project names out of telemetry An instance pointed at its own hub imports its own projects, and the slug naming one is the customer's content — `template_import` was recording it verbatim, which the disclosure ("the name of any public hub project") does not cover and `hub_script` already avoids by collapsing a private script to `private`. `hubProjectUsageKey` gives projects the same treatment, deciding by the configured hub's host so a port, a scheme's case or a trailing slash cannot turn a private hub into a public one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JirHCYVR6qg7Xqe4PcZ1KG * fix(frontend): treat an unread hub setting as private, not as the public hub `hubBaseUrlStore` is seeded with the public hub and written in one place, by a loader with no catch and no retry. A settings read that threw therefore left the store naming hub.windmill.dev for the rest of the session, and the import counter read that as permission to report a private instance's project slug — the leak the previous commit closed, narrowed to "after one failed read". The fact has three states and the store held two, so `hubBaseUrlKnown` carries the third: the loader sets it only once the value is the instance's own, and the telemetry key requires it. Links keep rendering the default meanwhile, which is what they always did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JirHCYVR6qg7Xqe4PcZ1KG * fix(frontend): tag each hub detail with the slug that asked for it `resource()` assigns whatever its fetcher returns, with no guard for a run that has been superseded, so handing back the previously fetched project on a stale response published that project. With two slow requests in flight — pick A, leave B loading, pick C — B's answer put A's name, author and counts on the card while the plan underneath still said C, and Import wrote C. Each answer now carries its own slug and is read only while that slug is the chosen one, which also drops the local the previous shape needed to keep the resource's type from going circular. The hub-telemetry tests reset their shared fixture per case; the private-hub one had been passing on what the case above it left behind. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JirHCYVR6qg7Xqe4PcZ1KG * fix(frontend): stop the template picker spinning on a hub with no projects Opening the picker against a reachable hub that has published nothing pinned the renderer at full CPU and froze the tab. The effect arming the list called `setLoader` and `loadData`, which read `InfiniteList`'s reactive state as well as writing it, so the effect depended on what its own load changed and re-ran itself; a list that stays empty never settles that cycle. It now arms the loader once per workspace, untracked, and leaves the load to `setLoader`. The same empty list also claimed the hub was unreachable, since one `empty` snippet serves both. The loader records which happened, so a hub with nothing on it says so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JirHCYVR6qg7Xqe4PcZ1KG * fix(frontend): keep a superseded hub answer from erasing the chosen project `resource()` publishes whatever its fetcher returns, superseded or not, and `fetchHubProject` takes no abort signal — so an answer for a project the user had moved on from replaced the published value, the slug guard rejected it, and the chosen project's item counts went off the card for good with nothing left to ask for them again. The fetch now records its own answer, tagged with its slug and only while that slug is still the chosen one, and the card reads that. Nothing reads the resource, so it is a `watch` — the same machinery without the value that was the problem. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JirHCYVR6qg7Xqe4PcZ1KG * chore: update ee-repo-ref to 81edd1382d951265ab3e9b67fc7ca7967676fd56 This commit updates the EE repository reference after PR #775 was merged in windmill-ee-private. Previous ee-repo-ref: 21ace847ec1c1406bafc50153004e1874642bf6c New ee-repo-ref: 81edd1382d951265ab3e9b67fc7ca7967676fd56 Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com> |
||
|
|
130a2f7408 |
feat: instrument sandbox isolation, data tables and in-flow script edits (#10981)
* feat: instrument sandbox isolation, data tables and in-flow script edits Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GLnp4v49BozkDd3KeWn5Q3 * fix: address review findings on the new telemetry counters Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GLnp4v49BozkDd3KeWn5Q3 * refactor: inline single-site telemetry helpers and trim what is collected Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GLnp4v49BozkDd3KeWn5Q3 * docs: tighten the telemetry disclosure copy Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GLnp4v49BozkDd3KeWn5Q3 * chore: update ee-repo-ref to 5921c03c8e28642efd1c390f590c0dab9834fa99 This commit updates the EE repository reference after PR #780 was merged in windmill-ee-private. Previous ee-repo-ref: 548b5e0421a04a2d9a76cce6efc6c91b1d8560ee New ee-repo-ref: 5921c03c8e28642efd1c390f590c0dab9834fa99 Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com> Co-authored-by: Ruben Fiszel <ruben@windmill.dev> |
||
|
|
79426a1a68 |
feat: reconcile IdP instance groups from the SSO groups claim (#10957)
* feat: add sso_groups_claim setting for login-time instance group sync Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YESxWqzt959S6TY6vbc4eG * chore: bump ee-repo-ref for the SSO groups claim reconcile Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YESxWqzt959S6TY6vbc4eG * chore: update ee-repo-ref to 3b89bfc11314a326a191101cfe3ef65f6f7f82a8 This commit updates the EE repository reference after PR #774 was merged in windmill-ee-private. Previous ee-repo-ref: e388527f9adbbe466fe050ca8d1d236ce3342bc3 New ee-repo-ref: 3b89bfc11314a326a191101cfe3ef65f6f7f82a8 Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com> |
||
|
|
0bbd559ac8 |
feat: instrument AI fill/fix, evals, agents and the debugger (#10853)
* feat: track AI fill, AI fix, evals, reusable agents and debugger usage Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore: pin ee ref to the feature_usage registry commit Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore: update ee-repo-ref to c3b6f62ea579a3583d4b474e9885c77104cfc87e This commit updates the EE repository reference after PR #745 was merged in windmill-ee-private. Previous ee-repo-ref: 77992910929188a854eadc06ee45971877b6f954 New ee-repo-ref: c3b6f62ea579a3583d4b474e9885c77104cfc87e Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com> Co-authored-by: Ruben Fiszel <ruben@windmill.dev> |
||
|
|
e866b68cdf |
feat: surface execution usage in the sidebar and explain what an execution is (#10760)
* feat: surface execution usage in the sidebar and explain what an execution is Users read "executions" as a job count and are surprised by the real number, which meters a second of compute. Every place the UI prints an execution count now says so, and the sidebar carries a usage meter for the quota that will bind first. Adds SidebarUsage at the bottom of both sidebar surfaces: a ring in the collapsed rail, a labelled bar when expanded, and a modal breaking down every quota. On the free tier it meters the per-user and per-workspace 1000-execution caps; on a paid plan it meters workspace usage against the executions the workspace's seats already include. Item.tooltip was inert on disabled dropdown rows: DropdownSubmenuItem rendered the info icon inside the disabled button, which swallows hover, and the row's own title attribute shadowed any wrapper title. Both renderers now fall back to a wrapper title the way DropdownV2Inner already intended. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep the usage meter tied to the workspace it describes isPremiumStore held the previous workspace's tier across a switch, which no consumer noticed while it only gated affordances — the usage meter is the first surface to render a number from it, and would have shown a paid seat quota for a free workspace. It is now undefined until the active workspace's tier is known, and a superseded response no longer writes. The seat fetch had the same shape: a slow response for the workspace we left overwrote the current count and stayed wrong until the next switch. The usage wrapper also carried the padding the brand-mark row used to own, which shifted the sidebar bottom by 4px on every instance where the meter renders nothing. The component owns its own padding instead. Names the collapsed ring for assistive tech, which otherwise saw an unlabelled button whose only signal was the arc's color. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: scope the usage meter to the billing workspace and a known tier A fork's usage, tier and bill all resolve to its billing root, but its member list is deliberately a subset of the root's, so counting fork members metered root usage against a fork-sized cap and invented billed-seat overages. Seats now come from the billing root, and the paid meter stays hidden when that root is not visible from the fork. The tier was cleared only after the user-store round-trip, so the meter rendered the previous workspace's tier for the length of it — a free→paid switch showed the 1000-execution hard cap on a paid workspace, not a race but every time. The clear now happens before the first await. Workspace usage had neither guard: a superseded response overwrote the store permanently, and the meter is the first surface to print that number as its headline rather than bury it in a dropdown. The free-tier counters keyed off `!$isPremiumStore`, which reads an unknown tier as free and flashed the free-tier blocks during a paid-to-paid switch. They wait for a known tier instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: never render an unresolved execution count as zero The workspace-usage clear wrote 0, which is a real usage value: an in-flight or failed fetch rendered as a green "0/1,000" bar, and a rejection left it there for the session because loadUsage had no failure path. Usage is now undefined until it resolves, each endpoint is assigned on its own so one failing leaves the other's number intact, and a quota is listed only once its own usage, tier and cap are known. The legacy counters show an em dash rather than a fabricated 0. The fork gates read an unknown tier as not-premium, so clearing the tier on switch made the fork entry point disappear for the length of the fetch on a paid-to-paid switch. They hold while the tier is unknown. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: read the usage endpoints as numbers, and fall back to the free tier Both usage endpoints serve text/plain, so the client hands back a string despite the generated `number` type. Interpolation and arithmetic coerced it, which is why nothing noticed before, but `toLocaleString` on a string returns it unchanged — a five-figure count rendered without its thousands separator against a formatted cap. A failed tier fetch left the tier unknown for the session, and consumers hold premium-only affordances through the unknown window so a free workspace kept offering them. It falls back to the free tier instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep an unknown tier unknown, and refresh the seat cap on demand Falling back to the free tier on a failed tier fetch fixed the affordance gates by lying to the meter: a paid workspace's real five-figure usage rendered against the 1000 hard cap, red, under "jobs stop running for the rest of the month". The tier stays unknown instead, and the two consumers get what each needs — the meter hides, while affordances read `maybePremium`, which holds through the pending window but fails closed once the fetch has failed. Membership changes elsewhere don't reach this component, so the seat cap could show an overage against a cap that had since grown. It re-resolves when the modal opens, which is when the number is read rather than glanced at. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: let anything showing executions re-read them The counters were written in one place, the root layout, on a workspace change only — so a tab left open all day showed the count from whenever the workspace was opened, and the modal-open refresh could only reach the seat cap, leaving a freshly computed denominator over a stale numerator. Moves the fetch to lib/usage.ts, next to the stores it writes, so the meter can refresh both numbers when its modal opens. Seats follow a membership signal that WorkspaceUserSettings bumps where it already refetches after every mutation, so the cap stops lagging a role change without either side owning the other. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: order concurrent usage and seat refreshes The workspace id doesn't order two requests for the same workspace, and both refreshes can now have two in flight: usage through A→B→A or a modal-open refresh landing on one already running, seats through a membership bump arriving mid-request. An older response could win and restore the count it replaced. Each refresh takes a generation and only writes if it is still the newest. The membership signal also fired on a plain read, so opening the users tab made every consumer re-fetch a list identical to the one it held. It bumps on an observed change to the member set instead, never on the first read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: count only billable seats, and order the tier requests The cap counted every member row, while the backend bills `NOT disabled AND NOT is_service_account` — a workspace with service accounts got an inflated included quota, which hides a real overage rather than inventing one. The seat basis matches `count_paid_seats` now, and the membership signature carries both fields so enabling or disabling a member re-resolves the cap. The tier fetch was the one refresh still ordered by workspace id alone, so a late failure for a workspace could raise the failure flag over a tier a newer request had already resolved. It takes a generation like the other two. The membership signature is keyed by workspace: this page survives a workspace switch, and comparing one workspace's members against another's reported a membership change where only the workspace had changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: compare the member set only against the same workspace's Qualifying the signature with the workspace put the workspace inside the value being compared, so a switch made every comparison unequal and bumped the version unconditionally — the opposite of the intent, and worse than before the key. The workspace is the key now, not part of the payload: a different one has nothing to compare against and re-baselines silently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: hold the usage and tier fetches in resources Every one of these values belongs to a workspace but lived in a bare store, so each writer and reader re-derived "does this still describe what I'm rendering?" by hand. Nine sites did, and the ones that forgot were most of this branch's review findings: three stale-workspace overwrites, three A→B→A races, and two placeholders (`0` executions, `false` tier) that read as real data because an in-band value was standing in for "not known". `resource` from runed — which frontend/AGENTS.md prescribes for async data, and which ~80 files here already use — supplies all three properties as behaviour rather than convention: a superseded fetch is discarded, the value resets when its key changes, and loading and error are states instead of magic values. The seat count keys on the billing root and the membership version, so both a workspace switch and an added member re-resolve it. That removes three generation counters, two workspace trackers, and the manual clear-and-compare around each fetch. What remains is one publish site that asserts the value still carries the active workspace before it reaches a store. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: order the resource responses that runed does not The refactor claimed `resource` discards a superseded fetch. It does not: its only ordering is an AbortController whose signal the generated client cannot consume, and `current = result` runs unconditionally once a fetch resolves. So a late answer for a workspace we had left still landed in `current`, and the publish site — which trusted `current` — cleared the value on screen for the workspace we were on. That reinstated the races the generation counters had covered. `loading` was standing in for the missing ordering, and it cannot: it is also true during a `refetch()`, when `current` is still the right value. Gating on it meant every re-read blanked the meter, and clicking it unmounted the modal that same click had opened, since both sit behind the quota it had just cleared. Values now carry the scope they describe and `scopedValue` keeps the newest one matching the active scope, so a superseded answer neither publishes nor erases, and a re-read leaves the display alone. The account-wide user counter keys on the account, so a workspace switch no longer clears it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: order responses within a scope, not just across scopes The tag carried what a value described but not when it was asked for, so two fetches for one scope — a refetch landing on an in-flight load, or a second membership invalidation — were indistinguishable and the older won if it landed last. That left the seat cap reading the pre-change number until the next bump or switch, which is the stale cap the generation counters had covered. Widening the tag to the resource key would have fixed it by blanking the bar on every membership change, so the issue order travels alongside the scope instead: `tagged` stamps each request as it is issued, and only a strictly newer answer for the current scope replaces the held one. The unit tests now cover the same-key case they missed; both new ones fail against the key-only guard. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: ignore a user list a newer read has overtaken `lastSeen` was written unconditionally after the await, so a response for a workspace already left overwrote the baseline for the workspace on screen. The next real membership change there then compared against a baseline that was never taken for it, re-baselined silently, and never bumped `workspaceMembershipVersion` — leaving the sidebar on the old seat cap. The `users` assignment had the same hole: an overtaken list could paint over a newer one. Both now go through a single check: a read whose issue order is behind the last applied one is dropped before it touches either. Also trims the two `scopedValue` docstrings and the membership rationale to the four lines AGENTS.md allows, and records there that a failed refresh keeps the last successful value rather than blanking. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: do not claim a plan before the tier resolves Widening `isPremiumStore` to `boolean | undefined` left `UserMenu`'s `{:else}` catching the unresolved state: with the tier still in flight, or after the request failed, a free workspace was told it was on the "Premium plan". Both branches under that block assert a plan, so the block now renders only once the tier is known — which also keeps the bordered divider from appearing empty while it resolves. Verified against the running instance with the tier stubbed slow: unresolved shows neither branch, `false` shows the free counters, `true` shows "Premium plan". Reverting the guard reproduces the wrong label at 300ms. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test: pin how a late answer orders against the read that replaced it Returning to a scope whose earlier read is still in flight is the one case the guard resolves by scope rather than by sequence, and the suite only covered it with nothing outstanding. It now covers the late answer itself: it stands while it is the only value describing the scope, the read issued on returning supersedes it, and it cannot come back afterwards. Also gives the meter the explicit `type="button"` the sibling sidebar rows use. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: size the modal's plan button with unifiedSize `size` is deprecated on `Button`. `unifiedSize="sm"` renders the plan button at the same height and weight as the modal's own Close button. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: match the plan button to the modal's own action button `unifiedSize="sm"` is `h-7`, and the Cancel button `Modal` renders beside it is `px-3 py-[7px]`, i.e. 32px — so the two sat 4px apart. `md` is the unified size that lands on 32px, which pairs them without putting a deprecated prop back. Measured both boxes rather than the new one alone: 32px and 32px, same top. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: instrument the execution meter, and bill-align PremiumInfo's seats The meter's only interaction is opening the modal, so that is what it counts: `usage_meter/opened`, keyed by the plan tier and the quota that was tightest — `free:user`, `free:workspace`, `paid:workspace`. The full set is a type next to the call site so the vocabulary stays readable in one place. The pair is registered in `FEATURE_USAGE_KINDS` (windmill-ee-private), without which the post is dropped with a 204 and records nothing. Verified both halves: the browser posts `{"feature":"usage_meter","kind":"opened","key":"free:user","value":1}`, the running EE image drops it because its registry predates the entry, and `is_recordable_event` accepts it once the entry is there. `PremiumInfo` computed its seats from an unfiltered user list, so the billing page counted disabled members and service accounts that `count_paid_seats` does not bill. Same filter as the sidebar's cap now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: point ee-repo-ref at the usage_meter registration Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: read the member list before the seat rows that depend on it `loadPremiumInfo` reads `users` after its own await and nothing recomputes the seat rows when the list lands, so whenever `premium_info` won the race the page rendered zero developers, zero operators and zero seats and kept them. The list is now fetched first, and a failure to read it no longer costs the rest of the page. Also refreshes the registered-action inventory in `docs/feature-telemetry.md`, which the new pair makes 21 across nine features. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: scope the seat comment to the counter it matches The comment claimed parity with the seats actually charged, which nothing in this repo computes: `count_paid_seats` documents itself as counting provisioned members rather than billing's active-user population, and the Stripe quantity is not derived here. What the filter buys is agreement with that counter. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref to c6902ec2c51dc0ce30962afbfab3e456c5d9b831 This commit updates the EE repository reference after PR #735 was merged in windmill-ee-private. Previous ee-repo-ref: bbc48fae6b73b6d72fe2e125e6003794a4ece167 New ee-repo-ref: c6902ec2c51dc0ce30962afbfab3e456c5d9b831 Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com> Co-authored-by: Ruben Fiszel <ruben@windmill.dev> |
||
|
|
53eb94659b |
feat(telemetry): extend feature-usage tracking beyond AI features (#10681)
* feat(telemetry): extend feature-usage tracking to long-tail features Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: describe telemetry as product feature usage rather than AI usage Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(telemetry): trim disclosure copy and drop unused pick origin Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(telemetry): count trigger fires per run and key hub picks from hub data Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(telemetry): slugify hub keys and order both writers' upserts Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(telemetry): key native trigger adoption by service so it matches fires Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref for native trigger adoption fix Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(telemetry): move feature-usage collection into the ee crate * docs: point feature-telemetry at the moved registry and rust writer * docs: correct the trigger-fire gate comment to match measured step counts * docs: put the private-build caveat on the verification step * chore: update ee-repo-ref to f079db9e7962a413b349c4ff8036080894f30771 This commit updates the EE repository reference after PR #725 was merged in windmill-ee-private. Previous ee-repo-ref: 055adb80416f9339c9a28ae7fbaeadad30d74959 New ee-repo-ref: f079db9e7962a413b349c4ff8036080894f30771 Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 5 (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> |
||
|
|
c3b2275864 |
docs(agents): rework agent context, fix dev-env docs, vendor skills (#10667)
* docs(agents): scope agent guidance to where it loads AGENTS.md loads in every session. Three of its sections only ever applied to one directory, and docs/autonomous-mode.md was unreferenced by anything in the repo, so none of its content was in effect. - Move "Verifying Backend Changes" to backend/CLAUDE.md, "Verifying Frontend Changes" and "Banned Patterns" to frontend/CLAUDE.md. They now load when working under those directories, which is when they apply. - Update the two cross-references that pointed at the moved sections (pr and svelte-frontend skills). - Delete docs/autonomous-mode.md. Its "don't stop early" half is already in .webmux.yaml's oneshot system prompt, which actually loads; its trigger was bypassPermissions, which does not imply an absent user; and it restated AGENTS.md and the pr skill with copies that had drifted (hardcoded ports, relative screenshot paths). Salvaged the UI traps it uniquely documented into frontend/CLAUDE.md and dropped the three stale profile references. AGENTS.md drops ~3.6k characters with no guidance lost. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(agents): guidance for building a feature — reuse, telemetry, live verification Three recurring gaps, all cases where a pointer existed but nothing triggered on it. Component reuse. The svelte-frontend skill documented three components with props, which reads as the whole catalog; the barrel exports 23 and common/ has 34 subdirectories against those 23. So "never use raw HTML elements" was an instruction agents could not follow. Added a mandatory discovery step: read the barrel, grep the tree, and treat the documented three as examples. Brand guidelines. frontend/brand-guidelines.md is 34k characters referenced by bare path, which nothing opens speculatively. Added a table mapping what you are building to the section that governs it, entered with grep rather than a full read. Product telemetry. feature_usage has 14 registered actions across three features, and an unregistered (feature, kind) pair is dropped by valid_feature_usage_event with a bare continue — no error, still a 204 — so frontend-only instrumentation silently records nothing. New docs/feature-telemetry.md carries the criteria for when to instrument, the four-step recipe including the allowlist and the InstanceSettings disclosure, and the privacy rules. Raised in the plan for user-facing work, not as a separate question, and not at all for bugfixes or refactors. Also: validation now ends at exercising the change on the running instance, with standing permission to spin up whatever that takes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(dev): correct the worktree dev-environment guidance Several things agents were told to do did not match what the machine does. - Env discovery pointed at .env / .env.local / backend/.env. In a webmux worktree the real values are in $(git rev-parse --git-dir)/webmux/runtime.env (BACKEND_PORT, FRONTEND_PORT, DATABASE_URL, CARGO_FEATURES, WM_DB_NAME), sourced by every pane and undocumented. Reading it is also not blocked by the Read(**/.env) deny rules, which the old instruction walked straight into. - The database name rule said branch-with-underscores. worktree-common.sh uses the worktree directory basename, and Postgres truncates at 63 characters, so branch hugo/win-2340-… resolves to windmill_win_2340_…_and_eval with no hugo_ prefix and the tail chopped. A wrong DATABASE_URL guts the sqlx cache. - The restart procedure said "tmux pane 1" and sent keys to an undefined <pane1>. Pane 1 is the backend under the full profile and the frontend under frontendOnly. Replaced with finding the pane by pane_current_command, recovering the live feature set from the running process (CARGO_FEATURES in runtime.env only records what the pane started with), and restarting in place. - Added recovery for an orphaned backend holding the port: it reparents to systemd when its shell dies, so it survives anything that looks like cleanup. Three checks before killing a single pid, because pkill -f windmill takes out every sibling worktree. - Agents spawned their own servers because AGENTS.md opened by telling them to. Now it checks for the existing panes first; the spawn commands are scoped to a plain checkout. - New EE worktrees branched from the EE repo's local main, which nothing fast-forwards, so they started behind the commit pinned in backend/ee-repo-ref.txt — the one CI builds against. They now base on the pin, falling back to main only when it is unreadable. - Enabled webmux autoPull so local main stays current; new worktrees are branched from it. Documented what WM_CLONE_DB does, including that it terminates every connection to the base windmill database. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(skills): vendor grilling/architecture skills; tighten PR ready and review rounds Vendors five skills from https://github.com/mattpocock/skills (MIT, pinned at 84fdeffd12f2ee307994d1eb6feb48173b6e0502). They are one dependency closure: grill-me is a stub that runs grilling, and improve-codebase-architecture draws its vocabulary from codebase-design and its CONTEXT.md upkeep from domain-modeling. .agents/skills/UPSTREAM.md records the license, the pin, and the four local deltas so a refresh stays a diff: - flattened the upstream engineering/ and productivity/ split - rewrote bundled-file links to repo-root paths, since relative links break when read through the .claude/skills symlink - dropped the upstream agents/openai.yaml packaging metadata - removed every ADR path. This repo has not adopted ADRs, and a skill that offers to create them is how the practice arrives by side effect rather than by decision. PR workflow changes, all in the pr skill: - A round that never starts is usually a conflict with main, not a CI outage. Resolve by merging, not rebasing — a rebase rewrites the head SHA that round verdicts and the clean-round marker are keyed to. If the merge advances backend/ee-repo-ref.txt, the EE worktree has to follow or cargo check --features private compiles a tree neither the author nor CI intends. - A clean round no longer means an automatic flip to ready. Wide blast radius (*_ee.rs, migrations, OpenAPI or the generated client, auth paths, shared worker infrastructure, a new public surface) asks first; self-contained changes flip. Unattended, the judgement holds and the action degrades: flip the small ones, leave the rest at a clean draft with the reason in the PR body. - Rounds that never converge are usually structural. After three without convergence, stop, name the module the findings cluster around, and suggest improve-codebase-architecture rather than burning more CI. AGENTS.local.md (gitignored, with CLAUDE.local.md importing it) holds the ready/ask calibration, recorded as dated observations rather than a rule. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(dev): state that each worktree gets its own fresh database The per-worktree section warned which DATABASE_URL to use but never said where the database comes from: the post-create hook creates and migrates a new one per worktree, so it starts with none of the main instance's workspaces, scripts or flows. WM_CLONE_DB was documented only as a comment in .webmux.yaml, which reads as how things work rather than as a per-project opt-in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(sqlx): script the cache backup/restore instead of documenting it The update-sqlx skill spelled out a cp/comm/rm dance around `cargo sqlx prepare`, which empties backend/.sqlx before regenerating — a failed run leaves the cache gutted (observed: 2350 -> 142 entries), and a --all-targets run in a CE checkout fails that way every time. Three problems with documenting it: - The backup path was the literal /tmp/sqlx_backup, shared by every worktree. Two concurrent runs overwrite each other's backup, which is the only thing standing between a failed prepare and a gutted cache. - The restore was a copy-pasted `rm -rf .sqlx && cp -r ... && cp ...` chain. - Skipping the backup is what turns a routine failure into a lost cache, and a convention is easier to skip than a command. sqlx-cache.sh has backup / newq / restore, keeps state in a per-worktree directory, and leaves the judgement call where it belongs: `newq` prints each added entry's query field for review, and only `restore` writes them in. Also adds the general rule that scratch files belong outside the checkout — anything written into the tree has to be deleted again, and rm prompts each time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(agents): state why a routine cleanup prompts, and where scratch goes The guard hook already auto-allows a plain rm whose operands are under /tmp or inside a git checkout in $HOME, so deleting a temp dir or a stale .sqlx entry costs nothing. What prompts is the command shape: the hook's tokenizer defers on &&, ;, redirects, quotes and $VAR, so a chained cleanup falls through to the Bash(rm:*) ask rule. That was recorded only inside a paragraph about screenshot file paths in frontend/CLAUDE.md, where nobody looking for it would find it. Stated in Core Principles instead, alongside the rule that scratch belongs outside the tree — for the reason that actually applies, which is not committing junk rather than avoiding prompts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(security): deny agent edits to the permission hooks and project settings .claude/hooks/guard-rm-outside-tmp.sh and guard-main-branch.sh are the enforcement points for everything the permission rules are meant to catch, and nothing stopped an agent editing them. One sed -i disables the guard for every later command, silently, and the deny list in .claude/settings.json has the same exposure. Defence in depth rather than a boundary: an agent with arbitrary bash can still delete, and this may only close the Edit-tool path if Bash writes are not covered by Edit deny rules. It costs nothing and removes the cheapest way to turn the guards off. Changing them now means editing the files by hand, which is the intent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: address review round findings on head |