chore(deps): Bump next from 15.5.10 to 15.5.18 in /web/server#8
Open
dependabot[bot] wants to merge 1 commit into
Open
chore(deps): Bump next from 15.5.10 to 15.5.18 in /web/server#8dependabot[bot] wants to merge 1 commit into
dependabot[bot] wants to merge 1 commit into
Conversation
Bumps [next](https://github.com/vercel/next.js) from 15.5.10 to 15.5.18. - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](vercel/next.js@v15.5.10...v15.5.18) --- updated-dependencies: - dependency-name: next dependency-version: 15.5.18 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com>
public-repo-publish Bot
pushed a commit
that referenced
this pull request
Jun 12, 2026
…s (feedback #8) (#490) * backend: expose fieldFilters on /api/runs/list Add an optional `fieldFilters` query param (JSON-encoded array of structured terms, same shape as the tRPC runs.list input) to the OpenAPI runs-list endpoint so REST/SDK/MCP callers can filter by arbitrary config.* / systemMetadata.* fields server-side. Reuses the existing indexed run_field_values filter infrastructure rather than inventing a query language: a new exported helper `queryFieldFilteredRunIds` runs a raw-SQL `Runs.id` subquery built from the same `buildFieldFilterConditions` the frontend table filters use, and the handler feeds the matching ids into its existing Prisma query via `id: { in: ... }` (intersecting with search ids when both are present). Operator semantics are therefore identical to the table UI — no Prisma re-implementation. Tests (smoke Suite 38): contains (text), is (text), numeric >, multi-term AND, and 400s on malformed/invalid fieldFilters. Adds a deterministic `config-filter-target` seed run with nested config.checkpoint.r2_prefix, config.model.name and config.trainer.lr. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * mcp: add config_filter to list_runs tool Add a `config_filter` parameter to the MCP `list_runs` tool (server.py) and the `PlutoClient.list_runs` method (api_client.py), backed by a new `config_filter.py` parser. Accepts a friendly "<key> <operator> <value>" string using the SAME operator vocabulary the backend understands, e.g.: "checkpoint.r2_prefix contains 37a" "trainer.lr > 0.001" "model.name is dit" "systemMetadata.hostname is gpu-01" - source defaults to config; a `systemMetadata.` key prefix overrides it. - dataType inferred: number if the value parses numeric, else text. - operator words/symbols map to backend strings (contains, does not contain, is, is not, starts with, ends with, regex, >, <, >=, <=). - Also accepts a single structured term dict, or a list of strings/dicts (AND-ed) so power users can reach operators like "is between"/"is any of". Terms are JSON-serialized into the backend `fieldFilters` query param. Tests (tests/test_config_filter.py): parser unit tests plus a parametrized equivalence check that the friendly-string and structured forms produce identical terms, and wiring tests that config_filter lands in the fieldFilters query param. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ci: run MCP server tests in Python smoke step The Python Service Smoke Tests step only ran py/ tests; the MCP config_filter parser (mcp/tests/test_config_filter.py) and the rest of the MCP suite were never executed by CI. Add an install + pytest block for mcp/ in the same step (same python:3.11-slim image, repo already mounted at /app), so the config_filter parser is covered on every build. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Address Gemini review on PR #490 - config_filter: reject symbolic-operator split when key before the symbol is multi-token (dot-paths cannot contain spaces), so values like "checkpoint.r2_prefix contains >=abc" are not mis-split. - config_filter: coerce structured-term values and infer dataType for numeric-only operators / numeric-looking strings, mirroring parse_config_filter_term; respect an explicit dataType. - runs-openapi: drop redundant bigint->string conversion in the id Set intersection (both id arrays are bigint[]). - Add unit tests for both config_filter cases. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(mcp): keep text-only config_filter operators as dataType=text Cursor Bugbot (high): contains/starts with/ends with/regex/does not contain with a numeric-looking value were inferred as dataType=number. The backend's buildValueCondition has no numeric branch for those operators, so buildFieldFilterConditions silently dropped the EXISTS clause and list_runs returned unfiltered runs. Force text-only operators to dataType=text in both the friendly-string parser and the structured-term normalizer (overriding even an explicit dataType=number, which is never valid for these operators). Numeric fields still carry a textValue, so e.g. "batch_size contains 6" now correctly matches "16"/"64". Verified against the local stack: "batch_size contains 6" returns 80 runs (SQL truth) instead of 161 (clause-dropped). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(mcp): align structured config_filter with friendly-string parsing _normalize_structured_term diverged from parse_config_filter_term in two ways that could make list_runs silently return unfiltered runs: 1. Numeric validation: numeric-only operators (>, <, >=, <=) now require every value to be numeric (int/float, or a string passing _looks_numeric), raising ValueError otherwise — regardless of an explicit dataType. Without this the backend's Number()/isNaN check drops the clause. 2. Key prefix stripping: a leading "config." / "systemMetadata." prefix on the structured key is now stripped (and "systemMetadata." sets source), matching the friendly-string path so both forms hit the same indexed key. The prefix takes precedence over a default/explicit source. Adds TDD tests covering both fixes plus string-vs-prefixed-structured convergence. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(api): server-side ordering & pagination on GET /api/runs/list Adds wandb-style `order`/pagination to the REST list endpoint so SDK callers can do `api.runs(order="-created_at", per_page=...)` and order by config/metric fields server-side. - `sort` param: built-in columns (createdAt/updatedAt/name/status, snake_case aliases, `-` prefix = desc, `id` tiebreaker; default unchanged = createdAt desc). - `sort=±config.<key>.value` / `±systemMetadata.<key>.value`: ordered via a new exported `queryFieldSortedRunIds`. `jsonFieldSortQuery` (tRPC) is refactored to call the same helper so REST and the frontend share one implementation. - `sort=±summary_metrics.<name>`: reuses `queryMetricSortedRunIds` (LAST agg) over the pre-aggregated mlop_metric_summaries. - `offset` pagination (Prisma skip) on the default path. - Sane limits: offset clamped to MAX_JSON_SORT_OFFSET (100k); candidate set capped at MAX_CUSTOM_SORT_CANDIDATES (10k); metric sort requires projectName and stays on pre-aggregated summaries. Custom sorts still respect search/tags/fieldFilters/ konduktor; response shape {runs, total} unchanged. Invalid sort field -> 400. Tests: smoke Suite 12b (14 cases) — built-in asc/desc, snake_case alias, offset paging, config asc/desc, metric asc/desc, invalid-key 400s. Verified end-to-end against a live 161-run project. check-types clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(api): add heartbeat_at ordering to GET /api/runs/list (on-demand CH) `sort=±heartbeat_at` orders runs by their last logged data point — wandb's heartbeat_at semantics. Computed on-demand from ClickHouse `MAX(time)` over mlop_metrics (the same signal the Python stale-run monitor uses), so no DB migration / materialized column is needed for now. - New `queryHeartbeatSortedRunIds(clickhouse, …)` mirrors queryMetricSortedRunIds but aggregates MAX(time); bounded by the candidate set (cap 10k), scoped by tenantId (+ projectName when present). Runs that never logged are excluded from the ordering (same as metric sort); `total` still reflects the full set. - REST sort grammar: `heartbeat_at`/`heartbeatAt`/`heartbeat`, `-`=desc; OpenAPI description + invalid-field error updated. - smoke Suite 12b.15: asc vs desc disagree at the top, total stable. Verified against the side-car: asc → oldest real runs, desc → newest, orphan CH rows (no Postgres run) correctly excluded. check-types clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Ubuntu <azureuser@andrewDev.iqigxx5hkb2uxpbdcmttsvsjcc.cx.internal.cloudapp.net> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
public-repo-publish Bot
pushed a commit
that referenced
this pull request
Jul 8, 2026
…th clean DOS/PSTT/filter/sorting logic (#524)
* [backend] register distinctGroups tRPC proc
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [backend] include group in run listing select blocks
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [backend] add Test Suite 36 (Groups Management) to smoke tests
Mirrors Test Suite 7 (Tags) for the new group field + /group/update
HTTP route. Suite number bumped to 36 (max+1) because "12" already
collides with Test Suite 12: Server-Side Search.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [backend] add groups input type to runs.list
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [backend] add buildGroupFilter helper to list-runs Prisma path
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [backend] add raw-SQL groups filter to list-runs builders
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [backend] add groups filter to runs.count proc
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [ralph] correct P3.7 to reference Test Suite 36 (Groups), not 12
The loop renamed the suite during P2.8 (Suite 12 was already Server-Side
Search). P3.7's literal reference would confuse a fresh Claude session
that has no memory of the rename.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [backend] document missing cache invalidation API in update-group
No per-key invalidation helper exists in lib/cache.ts — only clearL1Cache.
Per specs/groups/03-list-filter.md §4 fallback, rely on the 30s
GROUPS_CACHE_TTL in distinct-groups.ts and document the limitation at
the top of update-group.ts. Followup logged in fix_plan.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [backend] document missing cache invalidation API in HTTP /group/update
Mirror the rationale comment from the tRPC update-group proc to the HTTP
handler so both surfaces document the same 30s-TTL fallback. No per-key
invalidation API exists in web/server/lib/cache.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [backend] add read-side smoke tests 36.6-36.9 for runs.group filter
Seeds runs via the HTTP create endpoint and reads them back with a direct
Prisma query mirroring buildGroupFilter (list-runs.ts) and the distinct-
groups.ts raw SQL. The smoke harness has no session-cookie auth so the
protectedOrgProcedure tRPC procs can't be hit directly; per spec §5
fallback we use the "HTTP create + Prisma read" path. Follow-up tracked
in fix_plan.md to upgrade these to true tRPC coverage when the harness
gains session auth.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [ralph] scaffold Phase 4 (frontend, Scope A) for Run Groups
Adds specs/groups/04-frontend.md, appends P4.1-P4.9 to fix_plan.md,
wires the new spec into PROMPT.md stack allocation, and adds the
phase-4 file allowlist to AGENT.md. Old Phase 3 follow-ups are
marked [DEFERRED] so the loop skips them (they require infra outside
any phase allowlist).
Scope A: group column in runs table + edit popover, Group card on run
overview page, "Group by group" toolbar toggle with TanStack grouping.
No generic group-by-any-field dropdown — that's Scope B (separate phase).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [ralph] verify P4.1 — FE Run/get-run types already include group
Inferred end-to-end via tRPC (list-runs proc has group:true in selects;
get-run uses default Prisma select). @mlop/app check-types passes — no
explicit FE type declarations needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [frontend] P4.2 — add useDistinctGroups FE query
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [frontend] P4.3 — add useUpdateGroup FE mutation hooks
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [frontend] P4.4 — add GroupEditorPopover component
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [frontend] P4.5 — add GroupCell table renderer
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [frontend] P4.6 — wire GroupCell column + plumb getAllGroups/onGroupUpdate
Adds a `col.source === "system" && col.id === "group"` block to columns.tsx
adjacent to the tags block, rendering GroupCell with sortable single-string
semantics (mirrors the notes block pattern). Plumbs `allGroups` +
`onGroupUpdate` from index.tsx through data-table.tsx using the
useDistinctGroups query and useUpdateGroup mutation. Group column will
not yet appear in the column picker — registering it in
use-column-config.ts is tracked as a follow-up (file outside Phase 4
allowlist).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [frontend] P4.7 — add Group card to run summary page
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [frontend] P4.8 — add Group-by toolbar toggle + localStorage state
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [frontend] P4.9 — wire TanStack grouping for Group-by toggle
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [ralph] scaffold Phase 5 (W&B-parity UX) for Run Groups
Phase 5 reshapes the interaction model to match W&B exactly:
- Per-row "⋯" menu (View run logs / Move to another group / Delete run)
- Single-select GroupPickerPopover modeled on PR #477's tag picker
- Group column registered as default-but-hidden (EXTRA_SYSTEM_COLUMNS,
no STORAGE_VERSION bump)
- Strip phase-4 inline-pencil edits from group cell + run summary card
- Single-run delete reuses bulk useDeleteRuns([runId]) with mirrored
type-to-confirm Dialog from delete-runs-button.tsx
- Old GroupEditorPopover deleted
Defaults locked: Group card on summary page = plain read-only (not link);
"⋯" placement = hover-revealed icon at row end. No backend changes.
The Phase 4 follow-up about registering the column (was [BLOCKED]) is
superseded by P5.1 in this phase's widened allowlist.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [frontend] P5.1 — register Group in EXTRA_SYSTEM_COLUMNS (opt-in)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [frontend] P5.2 — strip inline edit from group-cell, drop popover
Pencil-on-hover and the GroupEditorPopover mount are gone. The cell is
now pure display: a Badge for a set group or a muted "—" when null.
GroupCellProps shrinks to { group: string | null }; columns.tsx stops
passing allGroups/onGroupUpdate at the call site (excess-property check
would otherwise fail). The surrounding columns plumbing keeps
getAllGroups and onGroupUpdate intact for the row-actions menu coming
in P5.6–P5.8.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [frontend] P5.3 — strip edit-on-hover from run-summary Group card
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [frontend] P5.4 — delete obsolete group-editor-popover
After P5.2 (group-cell stripped) and P5.3 (summary card stripped), no
application code imports GroupEditorPopover. Removing the file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [frontend] P5.5 — add group-picker-popover (single-select)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [frontend] P5.6 — add run-row-actions-menu (⋯ menu)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [frontend] P5.7 — append row-actions pseudo-column to runs-table
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [frontend] P5.8 — verify row-actions wiring satisfies spec
All callback plumbing for RunRowActionsMenu was already completed by
upstream phases (P4.6, P5.6, P5.7). This commit marks P5.8 done after
confirming no orphan props remain and check-types passes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [frontend] relocate row ⋯ menu into Name cell + restore pencil in Group cell
User feedback: the ⋯ row-actions menu belongs inside the Name column
(hover-revealed next to the run name), not in a separate end-of-row column
that pushes the actions off-screen. The Group column should mirror Tags —
a pencil-on-hover scoped to group editing only — not carry the broader
row actions.
- GroupCell: read-only badge + pencil-on-hover that opens
GroupPickerPopover (single-select).
- Name cell: RunRowActionsMenu now mounted as a sibling of the run-name
Link, hover-revealed via group-hover/row, stopPropagation on click so
it doesn't trigger the link.
- Drop the end-of-row row-actions pseudo-column entirely.
- Wire onRunDeleted from index.tsx → DataTable (deselect the run on
successful delete; the useDeleteRuns mutation's onMutate handles cache
removal optimistically).
check-types clean both workspaces.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [frontend] fix group pencil styling + dropdown→popover open race
Two user-reported issues:
1. Clicking "Move to another group" in the ⋯ menu opened the picker
for a single frame, then it closed. Cause: Radix DropdownMenu's
close-then-focus-restore fires in the same tick as our synchronous
setPickerOpen(true), and the popover's outside-click detection
reads the focus restoration as a click outside. Fix: setTimeout(0)
defers the open to the next tick so the dropdown is fully unmounted
first. Same fix applied to the Delete confirm dialog for the same
reason.
2. Group cell pencil was hover-revealed and a different size from the
Tags/Notes pencils. Rewritten to match those cells verbatim:
variant=ghost size=icon h-6 w-6 shrink-0, Pencil h-3 w-3,
title="Edit group", always visible. Pencil is now pushed to the far
right via flex-1 spacer (same pattern as notes-cell).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [frontend] prevent dropdown focus-restoration from closing the picker
The setTimeout(0) defer from the previous commit was not enough — the
GroupPickerPopover still closed within ~400ms of opening. Verified the
race with Playwright: picker opens at T=100ms, gone by T=500ms.
Root cause: when DropdownMenu closes, Radix restores focus to the
trigger button. This focus restoration dispatches a synthetic pointer
event that the just-opened Popover treats as an interaction outside,
firing its onOpenChange(false).
Fix: onCloseAutoFocus={(e) => e.preventDefault()} on DropdownMenuContent
suppresses Radix's focus restoration. Verified end-to-end with Playwright
that the picker now stays open and renders "Search or add group...",
"No groups found.", "Current group: (none)" stably.
The Delete confirm Dialog uses a portaled overlay (not anchored), so it
was unaffected by this same race — the existing setTimeout(0) is
sufficient for that path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [frontend][backend] group picker UX polish + cache freshness
Verified end-to-end via Playwright. Four fixes addressing user feedback:
1. useUpdateGroup now invalidates the distinctGroups React Query in
onSettled (was only invalidating runs queries). Without this, the
`allGroups` list staying stale meant a freshly-created group showed
"No groups found." on the next picker open.
2. Dropped the 30s server-side L1/L2 cache from distinct-groups.ts.
Even with FE invalidation the server would have served the stale
list for up to 30s. The underlying query is a single COUNT GROUP BY
on an indexed column, so caching adds little. Caching can come back
when web/server/lib/cache.ts grows a per-key invalidator (tracked
already as a Phase 3 follow-up).
3. group-picker-popover hides the "Current group:" footer entirely
when currentGroup is null (matches the tag picker empty state). No
more "Current group: (none)" line for runs that have no group.
4. group-cell renders the badge OR nothing — dropped the "—"
placeholder. Matches the Tags column convention (blank when empty).
Playwright run confirmed: 'a' appears in row-2 picker after creation
on row-1, no footer when empty, no "—" in empty cells.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [frontend] group cell pencil layout + collapse/expand fix
Two user-reported bugs, verified with Playwright.
1) Pencil clipped on long group names. The badge was max-w-[180px] with
the parent flex having overflow-hidden; when the column was just wide
enough for the badge to hit its max, the pencil was pushed past the
overflow boundary and clipped (z-index was not the issue — overflow
was). Fix: badge is now min-w-0 flex-1 (shrinks to fit available
space, truncates internally), dropped the spacer, dropped the
overflow-hidden from the parent. Pencil sibling stays shrink-0 and
is always visible.
2) Collapse/expand chevron did nothing. The expanded state was a
hardcoded `const expanded: ExpandedState = true` — TanStack's
getToggleExpandedHandler called the internal setExpanded but the
table had no onExpandedChange so the toggle silently no-op'd.
Switched to uncontrolled expanded state via `initialState.expanded:
true` (TanStack v8 quirk: state.expanded === true is honored only
after the first user toggle, so passing it as controlled state
renders the table collapsed on mount — not what we want). Now all
groups default-expanded and the chevron toggles each group's state
via TanStack's internal store.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [frontend] fix client-side sort for system columns (createdAt etc.)
User reported Created desc sort showing rows in ascending order. Root
cause was a pre-existing tableId-mismatch bug in sortRunsByColumn.
columns.tsx builds dynamic column ids as `custom-${source}-${id}`:
source=system → custom-system-createdAt
source=systemMetadata → custom-systemMetadata-hostname
source=config → custom-config-lr
But sortRunsByColumn's lookup was:
source=system → custom-systemMetadata-${id} ← WRONG
source=systemMetadata, config → custom-${source}-${id} (correct)
So for any sort on a registry system column (createdAt, updatedAt,
statusUpdated, notes), the function couldn't resolve the column and
returned the input unsorted. That's invisible when the input is
already server-sorted, but breaks visibly when pinned-selected runs
are mixed in: their out-of-page tail arrives in insertion order and
the no-op client sort lets it through to the user.
Fix: replicate columns.tsx's tableId formula exactly. Also updated the
two sort-pinned-runs tests that were validating the buggy format
(`custom-systemMetadata-createdAt` for a source=system column) to use
the correct format. All 850 unit tests pass; Playwright run with
group-by ON + selection ON confirms desc order is now correct.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [frontend] fix sortRunsByColumn for Date-typed createdAt values
Page 1 → page 4 → page 1 navigation surfaced May 18 (Mon) runs above
May 14 (Thu) runs despite an ASC sort. Root cause was String() on Date
objects in this branch's commit ad62d2d53: superjson deserializes
createdAt/updatedAt to Date instances in some React Query cache states,
and String(Date) is "Thu May 14 2026 ..." which lex-sorts by day-of-week
letter — 'M'on < 'T'hu, so May 18 ends up before May 14.
Before that commit sortRunsByColumn was a no-op for system columns (a
separate tableId-mismatch bug), so the buggy comparison never ran and
the server-side ORDER BY was passed through unchanged. Once the no-op
was removed, the date-stringification surfaced as wrong ordering after
pages got refetched.
Fix: normalize Date instances to ISO strings (which ARE lex-sortable in
chronological order, and match the server-side string format) before
comparing. Existing 28 sort unit tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [frontend][backend] sort by Group + save groupBy/expanded in preset
- backend: add `group` to SYSTEM_SORT_FIELDS so runs.list accepts
sortField="group" (NULLS LAST, double-quoted reserved word).
- columns-utils: client-side getCustomColumnValue handles "group" so
in-memory sort of selected/pinned runs matches server order.
- index.tsx: controlled ExpandedState persisted to localStorage; pass
expanded + setExpanded down to DataTable and into the RunTableView
config. handleLoadView applies config.groupBy and config.expanded
(defaults preserve old views without these fields).
- data-table + use-data-table-state: thread expanded/onExpandedChange
through, replacing the prior `initialState: { expanded: true }`
uncontrolled approach.
- run-table-view-selector: getCurrentConfig now includes groupBy and
expanded; props accept currentGroupBy / currentExpanded.
* [backend][frontend] persist groupBy/expanded in RunTableView config
- backend: add groupBy and expanded to RunTableViewConfigSchema so Zod
stops stripping them on create/update. groupBy is `"group" | null`;
expanded matches TanStack's ExpandedState (`true | Record<...>`).
- frontend: hasUnsavedChanges normalization now backfills missing
groupBy (null) and expanded (true) on legacy snapshots, matching
how pageSize was already handled — otherwise the "unsaved" dot
appeared on every page load against pre-existing views.
* [testing] unit + smoke + e2e coverage for run-groups feature
Covers the runs-table grouping work shipped in this branch:
- per-row Group cell + GroupPickerPopover
- ⋯ menu → "Move to another group"
- sort-by-group on the runs.list endpoint
- toolbar Group-by toggle + per-group collapse/expand
- groupBy + expanded fields round-tripping through RunTableView config
Source touches are selector-only — three new data-testids on the
existing UI (`group-by-toggle`, `group-row-chevron`,
`distinct-group-option`) plus the data-group-value/data-group-expanded
attributes on the grouped <tr>. No behavior changes.
Seed: new step 5h in setup.ts seeds a `run-groups-test` project with 5
runs across 3 named groups + 1 null (alpha ×2, beta, gamma, plus
rg-solo). Lives in its own project so mutating e2e tests can't pollute
smoke-test-project.
Smoke (server/tests/smoke.test.ts):
- 20.10 sort by `group` ASC → NULLS LAST
- 20.11 sort by `group` DESC → NULLS LAST
- 19.13 round-trip groupBy + expanded through runTableViews.create/get
- 19.14 update view to groupBy: null / expanded: true sentinels
- 19.15 legacy view config without the new fields still loads
Unit:
- server/tests/run-table-view-types.test.ts — 6 tests over
RunTableViewConfigSchema (legacy, new, bogus shapes)
- app/.../runs-table/__tests__/columns-utils.test.ts — append
`getCustomColumnValue` for the system `group` column (returns
run.group ?? null, NOT "-")
E2E (web/e2e/specs/runs/run-groups.spec.ts, 8 tests, serial):
#1 Group cells render seeded values, dropdown lists distinct groups
#2 Pencil: assign existing → create new → clear (state restored)
#3 ⋯ → "Move to another group" doesn't flash-close the picker
#4 Sort by Group ASC + DESC honors NULLS LAST in both directions
#5 Group-by toggle pins Group leftmost + renders grouped rows
#6 Collapsing a group persists across reload (localStorage path)
#7 Saved view restores groupBy AND collapsed group state
#8 Removing the Group column while in group view exits group view
Each mutating e2e test restores run.group via tRPC in a finally block
so subsequent tests see the seeded shape.
* [frontend][bugfix] preserve Group column on post-refresh group-view exit + stop picker click bubbling
Two fixes flagged by bot review on PR #483:
1. exitGroupView lost the Group column entirely when no snapshot was
available — the case after a page refresh where groupBy="group" is
restored from localStorage but enterGroupView never fired this
session. The old `else { updateColumns(rest) }` branch stripped the
column, forcing users to re-add it from the picker. Now we leave the
Group column at its current index with `isPinned: false`, so toggling
group view off just unpins it.
2. GroupPickerPopover's PopoverContent now stops click event propagation.
Mirrors what notes-cell.tsx already does on its popover. The picker
renders inside table cells (GroupCell + RunRowActionsMenu) and React
synthetic events bubble up through the React tree even though Radix
portals the DOM — without this, a click target listening on an
ancestor row would fire on every option select.
* [testing] run-groups e2e: drop serial mode, idempotent column add, broader localStorage reset
Three fixes to the run-groups spec:
1. Remove `test.describe.configure({ mode: "serial" })`. Serial mode
skipped tests #3–#8 when #2 failed, hiding what would have been
independent failures. Each test is already self-contained — restores
its own mutations in finally, beforeEach wipes view + localStorage
state — so they can run in any order without cascading.
2. Replace `toggleColumn(page, "Group")` with `ensureColumnVisible`,
an idempotent helper that only adds the column if not present in
`<thead>`. `toggleColumn` is XOR: after a reload inside a test,
localStorage still has the column visible, so re-toggling REMOVED
it (then assertions on the cell failed). #2 was exactly this.
3. beforeEach now clears `mlop:columns:*`, `mlop:col-base-overrides:*`,
and `run-table-*` localStorage keys in addition to the group-by /
expanded keys, and reloads after cleanup so the frontend renders
the clean state. Prevents column-config leak across tests now that
they aren't serial.
* [testing] run-groups e2e: per-test projects for parallel-safe state isolation
Playwright config is fullyParallel: true with 4 CI workers, so tests in
this file race against the same backend. Following the codebase pattern
(custom-dashboard.spec.ts etc. use Date.now()-suffixed names per test),
each test that mutates backend state owns its own project. Read-only
tests share the canonical project.
setup.ts now seeds 6 run-groups projects (canonical + 4 mutation + 1
view-save), each with the same 5-run shape. Spec dispatches by test:
#1, #4, #5, #6, #8 → canonical (read-only)
#2a, #2b, #2c, #3 → per-test mutation projects
#7 → per-test view-save project
#2 was split into #2a/#2b/#2c (one mutation per test); #5/#6 assertions
fixed (4 group buckets incl. null bucket from rg-solo).
* [frontend][testing] reset-to-default also clears grouping + tighten e2e waits
handleResetToDefault now clears groupBy and expanded alongside the
columns/filters/sorting/pageSize reset. DEFAULT_COLUMNS doesn't include
the Group column, so without this, picking "Default" while in group view
would leave a dangling groupBy=group pointing at a column that no longer
renders.
The empty positioning span in RunRowActionsMenu drops its aria-hidden
attribute. The span is decorative (no content, no children) so screen
readers ignore it either way; aria-hidden was making it match
tags-column-width.spec.ts's `table tbody [aria-hidden="true"]` selector,
which is meant to find the Tags column's measurement row.
E2E:
- #6: after the reload, don't call waitForSeededTable (which loops over
every run name) — alpha is correctly collapsed, so rg-alpha-1/2 are
hidden. Wait for <thead> instead, then assert collapse state.
- #7: rewritten without a reload. Save preset → switch to Default →
assert group view off + all rows visible → switch back to preset →
assert group view on + alpha collapsed.
* [testing] run-groups e2e: reset target run.group on every mutation test
Playwright CI runs with retries: 2. A mutation test that succeeded at
writing `delta` to rg-gamma and then failed on a later step would leave
that mutation in place for its retry — and the retry would find that
"delta" already exists in distinctGroups, so the "Add new group" button
never renders.
setupProject() now accepts an optional `resets` map and force-writes the
target run.group values via `runs.updateGroup` before the final reload.
Each mutation test passes its own resets:
#2a → rg-solo: null
#2b → rg-gamma: gamma (also drops any stale "delta")
#2c → rg-alpha-1: alpha
#3 → rg-beta: beta
Read-only tests (#1, #4, #5, #6, #7, #8) don't need resets — they share
the canonical project but never write to it.
* [frontend][bugfix] exitGroupView drops Group column when it wasn't present before
The previous snapshot-vs-no-snapshot branching collapsed two cases that
should behave differently:
snap = { entry: null, index: -1 } means the snapshot was captured AND
the Group column was absent before enterGroupView added it. The correct
thing on exit is to drop the column (restore "wasn't there" state).
snap = null means enterGroupView never fired this session (page reload
with groupBy=group from localStorage, or a loaded saved view). We don't
know the prior layout, so unpin Group in place rather than silently
strip a column that may be intentional in the user's saved config.
The old `if (snap?.entry)` mapped null entry into the same branch as
"no snapshot at all" → the column stuck around after a toggle on/off
from a default column layout that didn't include Group.
* [frontend][quality] move localStorage write out of setState updater; tighten reconciliation effect; lift useDeleteRuns to data-table
Three quality fixes flagged in bot review:
- `expanded` is now persisted via a `useEffect`, not inside the state
updater function. State updaters must be pure — React Strict Mode and
concurrent rendering can invoke them multiple times, so any side effect
inside risks double writes.
- The group-column reconciliation `useEffect` no longer lists
`customColumns` as a dep. It writes back to `customColumns`, so the
dep was causing the effect to refire whenever any unrelated column
edit happened while in group view. The effect now reacts only to
`groupBy` transitions and reads the latest columns via a ref.
- `useDeleteRuns` is lifted from `RunRowActionsMenu` (one instance per
visible row) to `data-table.tsx` (single instance for the table).
The menu receives an `onDelete` callback and tracks its own per-row
pending state with a local `useState`. Matches how `onTagsUpdate`,
`onGroupUpdate`, etc. already flow.
* [frontend] lock Group column position while in group view
The Group column must stay at index 0 and pinned while group view is
on — otherwise the grouping state and the column state can disagree
(e.g. user unpins Group and the sticky/dark-bg styling drops out, or
drags Group to index 5 and the visual layout no longer matches the
"grouped by Group" intent).
- Header menu no longer renders the Pin/Unpin entry for Group while
`groupBy === "group"`.
- The TH render skips the drag handle and the grab cursor for Group
while group view is on.
- `handleToggleColumnPin` and `handleReorderColumns` in index.tsx are
no-ops for the Group column while group view is on — defense
against any programmatic / saved-view path that bypasses the UI.
* [frontend][ux] picker: clicking the currently-selected group deselects it
The X chip in the picker footer was the only path to "no group" from
the picker, which was easy to miss — most users tried clicking the
checked row first and got a no-op close.
Match common picker UX: clicking the row whose checkmark is showing
toggles the run back to no group, same as the chip.
* [frontend][backend][cleanup] sticky/full-row group headers + drop dead code + SQL dedup
UX:
- The grouped-row bar is now clickable across its full width (not just
the chevron + label). Cursor changes to pointer.
- The "Group: foo (N runs)" label is `position: sticky; left: 0` so it
stays visible at the left edge when the table is scrolled
horizontally — previously the label scrolled off-screen.
- Keyboard: Space/Enter on the grouped row toggles expand/collapse.
Cleanup:
- Delete the dead `(run)/.../~queries/update-group.ts` — not imported
anywhere; the run-detail Group card is read-only.
- Drop the `ralph/` and `specs/groups/` scaffolding. The autonomous
loop driver and phase docs were dev-only; nothing in production or
CI references them.
- Strip the stale `specs/groups/03-list-filter.md` / `GROUPS_CACHE_TTL`
comments from update-group.ts, runs-openapi.ts, smoke.test.ts, and
use-data-table-state.ts. The "30s TTL" they referenced doesn't exist;
the real "uncached for freshness" decision is documented in
distinct-groups.ts.
SQL dedup (no behavior change):
- Extract `buildGroupFilterSql(groups, params): string | null` and
`export buildGroupFilter` from list-runs.ts.
- Replace the three inline raw-SQL `groups` filter blocks in
list-runs.ts and the matching one in runs-count.ts with calls to
the helper.
- runs-count.ts now imports `buildGroupFilter` instead of carrying its
own copy.
* [frontend] make Group-by and Pin-selected-to-top mutually exclusive
The pinned-overlay table and TanStack's grouped row model render
selected runs in fundamentally different shapes — turning both on
produces the broken layout in #483 where pinned rows show up
ungrouped above a partial group-headers list.
Treat them as mutually exclusive view modes (W&B-style):
- Turning Group-by on auto-disables Pin-selected-to-top.
- Turning Pin-selected-to-top on auto-disables Group-by.
- One-time mount-guard normalizes the inconsistent state for users
whose localStorage was set to both true before this PR landed.
Implementation: lifted pinSelectedToTop from useDataTableState to
index.tsx so the mutex wrappers can sit next to handleGroupByChange.
The hook now receives pinSelectedToTop + onPinSelectedToTopChange as
inputs instead of owning them.
* [backend][frontend][perf] cap distinct-groups at 500 + server-side typing search
Matches the tags-dropdown pattern (PR #477) and column/metric-name
search. Groups can scale on a per-project basis the same way tags did
— without the cap, a project with thousands of distinct groups would
drop the full list into the picker DOM the moment any user clicks a
pencil, and re-render it for every keystroke.
Backend (distinct-groups.ts):
- `LIMIT 500` (`MAX_LIMIT`), clamped server-side.
- `ORDER BY count DESC, MAX(createdAt) DESC, value ASC` — adds the
recency tiebreaker so a newly-created group with count=1 bubbles
ahead of dormant count=1 groups (previously a fresh group landed
at the bottom of an alphabetical tail).
- Optional `search` param with ILIKE + escaped metacharacters.
Frontend:
- New `useGroupSearch` hook (mirror of `useTagSearch`) — 200ms
debounce, fires only when the search term is non-empty.
- `GroupPickerPopover` accepts `organizationId` + `projectName`,
uses server results while the user types and the loaded
`allGroups` (already ≤500 from useDistinctGroups) otherwise.
- Backward-compat: when project context isn't provided, the picker
falls back to a local fuzzy filter over `allGroups` only.
- Wired through `GroupCell` (from columns.tsx) and
`RunRowActionsMenu`.
- Renders a small "Searching…" indicator while the debounced query
is in flight.
Net effect: bounded DOM cost no matter how many groups a project
has, and groups beyond the top-500 default are still reachable by
typing their name.
* [plan] grouping v2 — wandb-style general grouping (write-up only, no code yet)
* [backend][frontend] rip out v1 group column + endpoints + UI
Pivot to wandb-style grouping. Drops:
- runs.group column + index (new drop migration, v1 add migration deleted)
- runs.updateGroup, runs.distinctGroups tRPC procs
- /api/runs/group/update OpenAPI route + create-body group field
- GroupPickerPopover, GroupCell, useGroupSearch frontend pieces
- group-by toggle in TableToolbar, group system column,
RunTableViewConfig.{groupBy,expanded}
Backfilled existing dev data: rows with `group` get a `group:<value>` tag
before the column drops. Smoke tests around groups/sort-by-group deleted;
the new wandb-style grouping API will be smoke-tested in a follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [backend] grouping v2 — backend API + group:* tag invariant
Adds the W&B-style grouping primitives on the backend:
- runs.distinctGroupValues({ field, parentFilters, search?, valueSearch?,
limit?, offset?, ...regularFilters }) — paginated distinct values for
one grouping field, scoped by the same filter set the table uses.
Supports four field kinds: `system:status|name|creator.name`,
`config:<key>`, `systemMetadata:<key>`, `tag-prefix:group`. Per-key
dataType is loaded from ProjectColumnKey so numeric configs use
numericValue (not textValue) for the GROUP BY.
- runs.list and runs.count accept `groupFilters: { field, value? }[]`,
translated at the input boundary into existing systemFilters /
fieldFilters / tags arrays. Null values drill into the "unset" bucket
for config / systemMetadata (operator: "not exists"). Tag-prefix null
buckets are surfaced in distinct-group-values but drill-in is deferred.
- New lib/group-tag.ts utility: GROUP_TAG_PREFIX = "group:", helpers
`isGroupTag`, `extractGroupValue`, `dedupGroupTagsKeepLast`,
`hasMultipleGroupTags`.
- Enforce the at-most-one `group:*` invariant at all write boundaries:
- tRPC runs.updateTags throws BAD_REQUEST on ambiguous payloads (UI
must resolve the conflict before submit).
- POST /api/runs/create and POST /api/runs/tags/update silently
dedup last-wins (SDK-derived group:* clobbers stale ones).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [frontend][backend] grouping v2 — wandb-style picker UI
Adds the chip-stack picker in the runs-table toolbar:
- Group button → popover with ordered chip stack of active fields
- Each chip: field label + source badge + ▲/▼ reorder + ✕ remove
- Clicking the chip's label swaps in the field picker so users can
replace one level without removing it
- + Add grouping field → field picker with four sections:
System, Tag-derived, Config (recent 100), System metadata (recent 100)
- Clear button wipes the stack
State lives at the project page (run-table-groupBy:v2 localStorage
key); flows DataTable → TableToolbar → GroupByPicker. Picker is opt-in
and renders nothing in the table yet — multi-level bucket rendering
ships in the next phase, but the new endpoints already return correct
data when called from elsewhere.
Backend fix folded in: the groupFilter translator was emitting operator
"equals" for numeric fieldFilters, which buildValueCondition's number
switch doesn't accept. Switch to "is" (the canonical operator the
number switch recognises, and a synonym for "equals" in the text
switch). Verified end-to-end via runs.distinctGroupValues +
runs.list/count with `groupFilters` against live ryandevvm2 data.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [backend][testing] grouping v2 — unit + smoke tests
- tests/group-tag.test.ts (13 tests): GROUP_TAG_PREFIX helpers — isGroupTag,
extractGroupValue, dedupGroupTagsKeepLast, hasMultipleGroupTags. Includes
the surprising case where dedupGroupTagsKeepLast preserves order of the
surviving group:* tag at its original position.
- tests/group-field.test.ts (22 tests): parseGroupField/encodeGroupField
round-trip + applyGroupFiltersToInput — covers all four field kinds,
null-bucket semantics (config → not exists, system → drop, tag-prefix →
no-op), numeric/text dataType routing, and the no-mutation guarantee.
- smoke.test.ts Test Suite 17c (8 tests): exercises runs.distinctGroupValues
with parentFilters / valueSearch / pagination + runs.list/count with
groupFilters against the seeded run-groups-test project. Verifies the
unknown-field / malformed-input quiet-empty paths.
- smoke.test.ts Test Suite 17d (2 tests): runs.updateTags rejects 2 group:*
tags but accepts exactly one.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [backend][frontend] grouping v2 — persist groupBy in saved views
RunTableViewConfig gains an optional groupBy: string[] field.
- server schema: z.array(z.string()).optional() on RunTableViewConfigSchema
- frontend selector: currentGroupBy threaded through hasUnsavedChanges
+ view-load flow. Legacy views without the field normalize to [] so
they don't show "unsaved changes" on open.
- handleLoadView / handleResetToDefault now reset groupBy alongside
columns/filters/sorting.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [frontend] grouping v2 — inline bucket tree (MVP)
Renders when groupBy is non-empty: a lazy multi-level tree of bucket
headers inside the runs-table body, replacing the flat row list.
- BucketLevel queries runs.distinctGroupValues({field, parentFilters}),
paginated 10-at-a-time with a "Show 10 more" affordance per level.
- BucketHeaderRow shows the field source badge, label, value, count;
click to expand/collapse. Indent grows with depth.
- Leaf buckets lazily fetch runs.list({groupFilters: trail}); each run
renders as a single-row link with status + tag chips + relative time.
- Expand state lives in a top-level Set<string> keyed by the JSON-
stringified bucket trail.
MVP intentionally drops per-row selection, custom-column rendering,
pinning, drag-zoom, and inline sort inside the grouped view — the user
is browsing buckets, not selecting runs. Re-adding those is the next
iteration; the backend API is already shaped to support them.
Verified end-to-end via curl against ryandevvm2 dev data:
config:batch_size → activation nested → 506 runs in 3 buckets ✓
tag-prefix:group bucket drill-in returns runs with correct group:* tag ✓
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [backend] grouping v2 — lenient parse of legacy groupBy
v1 saved RunTableViews stored `groupBy: "group" | null` (a string sentinel).
The v2 schema introduced groupBy: string[], so opening a project that had any
v1 view crashed in RunTableViewConfigSchema.parse with "expected array,
received string".
Added a preprocess transform on the groupBy field:
- null / undefined → undefined (no grouping)
- string "group" → ["tag-prefix:group"] (semantic equivalent)
- any other lone string → single-element array
- arrays pass through
Dev DB backfill (manual SQL, applied) converts the same two semantics so the
stored rows look canonical to the v2 path on write.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [frontend] grouping v2 — bucket rows use the real run table
Phase 8 follow-up: drop the hand-rolled simplified run row and render
buckets through the standard <RunRow> + memoizedColumns. Each leaf bucket
now mounts its own mini useReactTable that shares:
- the parent's column defs (visibility eye, name, tags, notes, custom
config/metric columns — all of it)
- columnOrder (so pinned columns stick in the same place)
- rowSelection derived from selectedRunsWithColors (toggling the eye
in any bucket flows into the same selection state the flat table
uses)
- pinnedColumnMap + tableBodyRef
Knock-on fixes:
- tags overflow tooltip ("+2" now lists the hidden tags) — comes for
free since TagsCell is rendered by the real column
- dropped the redundant "tag" badge from bucket headers; the picker
already conveys the field source, repeating it on every row was noise
- removed the simplified status badge + TagBadge imports
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [frontend] grouping v2 — tighten status column
Shrink the status-indicator column from 36px to 20px and left-align the
dot. The previous right-align (justify-end pr-1) left ~50px of empty
status-column space between the visibility eye and the green status
dot, which was barely noticeable in the flat table but stood out under
bucket headers in grouping view. Status dot now sits flush against the
eye in both views.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [backend][frontend] grouping v2 — persist expanded buckets in saved views
Lifts the bucket-expand `Set<string>` from inside <GroupedBucketTree> up
to the project page level, then threads it through saved views.
- RunTableViewConfig schema gains `expanded: z.array(z.string()).optional()`
with a lenient preprocess that drops v1's `true | Record<string, boolean>`
sentinel (no v2 translation; collapse all).
- index.tsx owns `expandedGroups: string[]`; handleLoadView restores it
from the saved view, handleResetToDefault clears it, and
handleGroupByChange clears it on field change (since the prior trails
reference fields that may no longer apply).
- DataTable / RunTableViewSelector / GroupedBucketTree receive the
lifted state + onChange callback.
Per the user: no localStorage backing — reload-without-loading-a-view
collapses every bucket.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [frontend] grouping v2 — fix status clipping, X overlap, sticky bucket headers
Three layout fixes the user surfaced after PR #483:
1. Status indicator was clipped on the right.
Root cause: I shrunk the status column to 20px to close the eye →
dot gap, but <TableCell> has px-2 = 16px combined padding, leaving
only ~4px content area for a ~10px dot. Bumped to 28px (just enough
to clear px-2 with the dot's ring).
2. The deselect-X badge on a selected row was being covered by the
status cell. Root cause: every pinned cell got z-index:1, so DOM
order (status renders after select) put the status cell's
background on top of the X overflowing from select via `-right-3`.
Lifted the select cell to z-index:2 so its overflow renders above
neighbors.
3. Bucket-header label disappeared off-screen during horizontal scroll
in grouped view. The header is a single colspan cell spanning the
whole table width, which scrolls with the body. Wrapped the inner
button in a `position: sticky; left: 0` div so the label stays
pinned at the left edge of the visible viewport while the rest of
the row scrolls under it — mirroring wandb's grouped-table sticky
bucket labels.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [backend][frontend] grouping v2 — sticky/full-row group headers + drop dead code + SQL dedup
Two user-visible fixes:
1. Whole bucket-header row is now the click target and the hover bg
tints the entire bar. Previously the inner button had its own
`hover:bg-accent/40` while the cell behind it had a static
`bg-muted/15`, producing a brighter rectangle inside a darker bar.
Lifted onClick + aria-expanded to <TableCell>, used a
`group/bucket` parent + `group-hover/bucket:bg-muted/30` so the
whole cell tints uniformly.
2. Runs without any `group:*` tag now show up as an "(unset)" bucket
under `tag-prefix:group` (and any future tag-prefix grouping).
- Re-added FilterableInputShape.tagPrefixExclusions; the
translator pushes `"group:"` onto it for null tag-prefix buckets.
- New buildTagPrefixExclusionConditions helper emits
`NOT EXISTS (SELECT 1 FROM UNNEST(r.tags) AS t WHERE t LIKE $1 || '%')`
called from every raw-SQL path in list-runs / runs-count /
distinct-group-values.
- The default-cursor (Prisma) path now forks to raw SQL when
tagPrefixExclusions is present.
- distinct-group-values runs a separate `NOT EXISTS` count to
surface the null bucket (only on offset=0, skipped when
valueSearch is active since `null` can't substring-match).
Verified against ryandevvm2/updatedSeededData: 4 named groups + 598
ungrouped runs returned by distinctGroupValues; drill-in returns only
runs without a `group:*` tag.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [frontend][cleanup] drop run-row actions menu
The "..." dropdown in each row's name cell (View run logs + Delete run)
was the only consumer of run-row-actions-menu.tsx; removing it lets us
delete:
- run-row-actions-menu.tsx file
- the menu's import / render block in columns.tsx
- ColumnsProps.onRunDeleted + onDeleteRun + their destructure
- DataTableProps.onRunDeleted + the lifted useDeleteRuns mutation +
handleDeleteRun (the toolbar's delete-runs button still uses
useDeleteRuns directly)
- index.tsx's onRunDeleted={...} prop
View-run-logs already accessible from the run detail page; deleting a
run still works via the toolbar Delete button with rows selected.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [frontend] grouping v2 — bucket coloring (Phase 9, A)
Each bucket gets a deterministic color from the Kelly-dark palette,
hashed off the JSON-stringified bucket trail (so a given
{tag-prefix:group, value:ca} bucket lands on the same color across
reloads, page navigations, and after re-grouping/re-saving views).
UX:
- Bucket header gains a small color swatch between the chevron and
the field label, matching the bucket's assigned color.
- Each leaf bucket's BucketRuns pushes the bucket color into the
page-level color state (handleColorChange) on fetch, with a dedup
guard so we don't re-fire when the color already matches.
Chart color propagation (no chart code change needed):
- The page's selectedRunsWithColors / runColors is the source of
truth for chart line colors. Since BucketRuns updates that state,
runs that were lines on the chart automatically re-paint to the
bucket color the moment their bucket is expanded.
Phase 9 is scoped to color only — wandb-style chart aggregation
(one line/group, min-max band, group-aware tooltip) is Phase 10
and comes in its own PR with a separate plan doc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [frontend] grouping v2 — hide per-row color picker in grouped view
When groupBy is non-empty, the name column's ColorPicker is suppressed
— the bucket owns the color for every run in it, and the eye +
bucket-header swatch already convey that. Eliminates the confusing
case where a user could mutate a single run's color and break the
bucket-wide visual association.
Threaded a new `isGrouped` flag from data-table.tsx → columns(). Flat
mode behaviour is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [frontend] grouping v2 — subgroup counts + leaf-only colors
Mirrors wandb's grouped header behavior:
- Color swatch now renders only on LEAF buckets (the deepest grouping
level — the one whose runs share the color). Parent buckets in a
multi-level groupBy are pure navigation containers and don't carry
a color, matching wandb where `User: rhayame3` has no swatch but
`Group: group1` underneath does.
- Non-leaf parent headers gain a "K subgroups" badge alongside the
existing "N runs" badge, e.g. `Group: ca · 3 subgroups · 4 runs`.
Each parent fires a background distinctGroupValues against the next
groupBy field; the probe doubles as a prefetch so expand is instant.
Beyond PAGE_SIZE (10) we show "10+" rather than running a separate
exact count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [plan] grouping v2 — chart aggregation (Phase 10, B)
Scope-locked design doc for the line-chart side of the grouping
feature. Backed by the user's decisions on:
- mean + min/max band only (v1)
- per-chart override deferred (wandb has it, future PR)
- hidden runs excluded from the aggregate
- lineage stitching off in grouped mode
- drag-zoom works for free
Not implemented yet — code lands in follow-on commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [backend] grouping v2 — chart aggregation endpoint (Phase 10, B-1)
Backend half of the chart-side grouping work.
queryRunMetricsGroupedBatchBucketed (lib/queries/run-metrics.ts):
- New ClickHouse helper. Same column shape as the flat path
(BucketedMetricDataPoint) but groups by `(logName, groupKey,
bucket)` instead of `(logName, runId, bucket)`. Single CH query
via an inline CTE that joins runId→groupKey arrays passed from
Node — perfect bucket alignment across all groups, no per-group
fan-out.
- count is now COUNT(DISTINCT runId) so the frontend can tell how
many runs contribute at each bucket (useful for sparse groups
where a band collapses to a single line).
graphMultiMetricBatchBucketedGrouped proc:
- Resolves the run universe from base toolbar filters + parent
groupFilters, then computes each run's leaf-bucket trail
(system fields off the row; config/sysmeta off run_field_values;
tag-prefix off r.tags) before calling into the CH helper.
- hiddenRunIds are subtracted from the universe before aggregation
per locked decision #3 — toggling the eye reshapes the band.
- Output keyed by groupPathKey matching the runs-table tree.
lib/group-run-assignment.ts:
- Pure helper that maps a Run row + field-values bag → its bucket
pathKey. 13 unit tests cover tag-prefix, system fields, config
numeric/text, nested chains, unknown fields.
Verified against ryandevvm2/updatedSeededData (`train/loss`,
groupBy=[tag-prefix:group]): 5 buckets returned with mean lines +
min/max envelopes; the `ca` group's high-variance band
(min=3.84, max=204.4) confirms the across-runs aggregation works.
Frontend (uPlot band rendering + tooltip rewrite) lands next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [frontend] grouping v2 — chart aggregation render path (Phase 10, B-2 MVP)
When the page has an active groupBy, chart widgets in CUSTOM DASHBOARDS
now render through a new GroupedLineChart component instead of
MultiLineChart.
What it does:
- Fires the new graphMultiMetricBatchBucketedGrouped endpoint with the
page's groupBy + hiddenRunIds.
- Builds LineData[] with one main line series (the mean) per group + an
envelope (min/max) wired via the existing `envelopeOf` / `envelopeBound`
hooks, so buildBandsConfig.ts in line-uplot draws the band.
- Color comes from bucketColorFor(pathKey) — the same hash that drives
the runs-table bucket header swatch + in-bucket run colors. Chart and
table now agree on color.
- Tooltip / legend / zoom / log scale all flow through LineChartUPlot
unchanged.
Plumbing:
page index → MetricsDisplay → DashboardBuilder → WidgetRenderer →
ChartWidget → GroupedLineChart
Out of scope for this MVP (TODO follow-ups, documented in
PLAN-grouping-v2-charts.md):
- Default "All Metrics" view (not a custom dashboard). The All Metrics
grid still uses the per-run line path. Users hit grouping by opening
any custom dashboard view.
- Zoom-refetch in grouped mode (the existing useZoomRefetch is tied to
per-run logic).
- Smoothing/EMA on aggregated lines.
- Time x-axis (only "step" for v1).
- Lineage stitching (locked off per decision #4).
Verified against ryandevvm2/updatedSeededData earlier with curl:
groupBy=[tag-prefix:group] over train/loss returns 5 buckets with
mean+min+max columns; the GroupedLineChart pipeline now feeds those
into the same uPlot bands code path the flat per-run envelope already
exercises.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [frontend] grouping v2 — chart aggregation in the Charts tab (Phase 10, B-3)
Extends the grouped chart path from custom dashboards to the default
"All Metrics" / Charts tab.
- MultiGroup gains an optional groupBy prop. Inside its renderChart
callback, METRIC widgets branch to <GroupedLineChart> when groupBy is
non-empty, and stay on <MultiLineChart> otherwise. Non-line widget
types (histogram, audio, image, video) keep their existing per-run
rendering — matches the locked scope (media untouched by grouping).
- arePropsEqual now also short-circuits the memo on groupBy changes,
by value. Without that, toggling grouping at the page level would be
swallowed by the memo and the user would see stale per-run lines
until something else invalidated the props.
- metrics-display.tsx threads `groupBy` from its existing props into
MemoizedMultiGroup so the Charts tab picks it up automatically.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [frontend] grouping v2 — fullscreen chart widget honors groupBy
The fullscreen <ChartFullscreenDialog> rendered its <WidgetRenderer>
without the groupBy/hiddenRunIds props that drive the new grouped
chart path. So expanding to fullscreen reverted to per-run lines,
exactly matching the user's "19 series instead of 14 grouped" report.
Plus a new tests/e2e/seed_grouping_test.py — predictable 14-run layout
across 4 named groups × 3 batch_sizes + 2 ungrouped runs, every run
logging train/loss at every step. Lets you eyeball "N subgroups in
the runs table === N lines in the chart" without media noise.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [frontend] grouping v2 — populate RUN NAME / METRIC columns in grouped tooltip
The chart tooltip reads `runName` / `metricName` straight off LineData
to fill its RUN NAME and METRIC columns. GroupedLineChart wasn't
setting either, so the tooltip showed an empty RUN NAME for every
grouped line and only the numeric value told the user which row was
which — confusing for a chart with 14+ lines.
Now the RUN NAME column shows the bucket trail
(`Group: ca · batch_size: 8`) — the wandb analogue for grouped
charts — and METRIC shows the underlying metric name.
Known remaining quirks in grouped-mode charts (documented in
PLAN-grouping-v2-charts.md, deferred to a follow-up phase):
- Smoothing/EMA: per the plan, smoothing doesn't apply to the
aggregated path yet. The slider has no effect when groupBy is set.
- Tooltip "mean (min, max)" formatting: still shows just the value;
envelope min/max not surfaced in the tooltip itself.
- Fullscreen tooltip can overflow / overlap on wide screens.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [frontend] grouping v2 — bucket-row hover highlights chart line + preset-switch fix
Two fixes the user surfaced after the first round of chart aggregation:
1. Hovering a leaf bucket row in the runs table now highlights the
matching group's line in every chart (and dims the others), exactly
like hovering a run row does in flat mode. Implementation:
- GroupedLineChart writes each series' _seriesId as
`<pathKey>:<metricName>` (was `<metricName>::<pathKey>`). The
chart-sync `seriesKeyMatches` does a `startsWith(target + ':')`
check, so prefixing with pathKey lets the bucket header dispatch
just the pathKey and match across every chart.
- BucketHeaderRow gains onMouseEnter/onMouseLeave handlers that
dispatch the same `run-table-hover` CustomEvent flat run rows
already use. Leaf buckets only — parents would need their full
descendant pathKey set, deferred to a follow-up.
- Also added runId=pathKey on grouped LineData for tooltip parity.
2. Switching saved presets (e.g. flat preset → grouped preset) no
longer required a hard refresh. MultiGroup's `components` useMemo
was missing `isGrouped` + `groupBy` from its deps array, so its
inline render functions stayed frozen against the previous
isGrouped value. Added both to the deps.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [frontend][testing] grouping v2 — fullscreen legend + DISPLAY ID + verify seed
Three things from the user's review pass:
1. Fullscreen chart now has its legend in the sidebar. MultiLineChart
passes showLegend={true} so uPlot creates `.u-legend` for the
fullscreen dialog's polling code to relocate into the right
sidebar. GroupedLineChart wasn't passing it, so the sidebar stayed
permanently empty and you saw only the leftover divider.
2. DISPLAY ID column in the grouped tooltip no longer shows the raw
JSON pathKey. Dropped runId from grouped LineData — RUN NAME
already carries the meaningful bucket trail ("Group: alpha ·
batch_size: 8"); the DISPLAY ID column simply renders blank for
grouped lines, which is the right answer when there isn't a
per-run display ID.
3. New tests/e2e/seed_grouping_verify.py — `groupingVerify` project
with 12 runs whose train/loss values are CONSTANT per run, so
mean/min/max are mathematically trivial to predict and eyeball.
Verified via curl against the new endpoint:
const-zero/4 line=0 band=[0,0] ✓
const-zero/8 line=5 band=[5,5] ✓
spread-50/4 line=50 band=[0,100] ✓
spread-50/8 line=50 band=[10,90] ✓
spread-25/4 line=25 band=[0,50] ✓
skew/4 line=25.75 band=[1,100] ✓
The skew bucket (mean of [1,1,1,100] = 25.75) confirms the
aggregator weights every run, not just min/max.
Plus a known-limitations section appended to
PLAN-grouping-v2-charts.md covering the parent-hover gap,
smoothing/EMA, tooltip mean-(min,max) format, per-chart override,
and fullscreen tooltip overflow — all explicitly punted to a
follow-up so future-me has a record.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [frontend] grouping v2 — fullscreen sizing wrapper around LineChartUPlot
GroupedLineChart returned LineChartUPlot bare. MultiLineChart wraps it
in `<div className="relative h-full w-full">` so the chart sizes
correctly inside ChartFullscreenDialog's flex container. Without that
wrapper the chart spilled past the dialog padding and read as "no
padding / overflow" — visible as the chart edge running under the
Export / Chart Settings buttons in the header. Adding the same wrapper
in the grouped path closes the gap; mini-widget rendering is unaffected
(the inline flex parent there already constrains size).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [frontend] grouping v2 — DISPLAY ID column shows contributing-run count
Per user request, the previously-blank DISPLAY ID column on grouped
chart tooltips and legends now surfaces the bucket's contributing-run
count (`2 runs`, `47 runs`, etc.). Reading "wide band + 2 runs"
immediately signals "small sample, high variance"; "narrow band + 50
runs" signals "trust the mean". We use the max count across buckets
rather than the per-step count — sparse-logging fluctuations are noisy
and the user mostly cares about how many runs the bucket COULD have.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [plan] grouping v2 — flesh out post-MVP TODO list with analysis
Per user feedback, the limitations section was too light — items were
listed without the analysis a future implementer (or future-me) needs
to prioritise. Rewrote with: what the gap is, where in code it lives,
rough impact, rough complexity, and any open design questions.
Also dropped the bogus "fullscreen tooltip / sidebar overflow" item
after checking the source: `.fullscreen-legend-sidebar` has
`overflow-y: auto` and the in-chart tooltip's `cachedMaxH` path
already applies a content max-height. Pre-existing handling; works
identically with grouped series. Added a "NOT a limitation" footnote
recording the investigation so the next person doesn't re-discover.
No code change.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [frontend] grouping v2 — Phase 10-C: tooltip band, parent hover, smoothing, per-chart override
Four chained TODO items from PLAN-grouping-v2-charts.md, in order.
1) Tooltip VALUE column now renders `mean (min, max)` for grouped
chart rows. tooltip-plugin already collects envelope sibling
values into `minValue` / `maxValue`; the value formatter now
reads both and appends the band edges when present. Flat per-run
charts are unaffected (no envelope siblings).
2) Parent-bucket hover. Bucket headers ONE level above leaf now
synthesise descendant pathKeys from the cached subgroup probe
data and dispatch them as an array via `run-table-hover` (the
chart-sync handler already supported string[] payloads). Higher-
depth parents still no-op for v1 — that's a cascading-probe
problem, deferred. BucketHeaderRow's hover handler was
restructured into a `hoverDispatch` discriminated-union prop so
leaf vs parent-of-leaf logic stays close to the dispatch site.
3) Smoothing in grouped mode. GroupedLineChart now reads
useLineSettings(...) and applies smoothData to ONLY the mean line
when smoothing is on — band edges stay raw min/max per the user's
call (matches non-grouped behaviour). settingsRunId plumbed
through ChartWidget; MultiGroup (Charts tab) passes undefined so
useLineSettings falls back to the workspace-wide "full" key.
4) Per-chart grouping override. ChartScalePopover gains a
"Group runs in this chart" toggle that only appears when the page
has active grouping. The toggle persists into
ChartWidgetConfig.groupingOverride ("auto" | "off") so it's saved
with the rest of the dashboard view config. ChartWidget reads
`effectiveGroupBy = override === "off" ? undefined : groupBy`.
Plumbing: dashboard-builder.tsx → widget-grid.tsx → widget-card.tsx
→ chart-scale-popover.tsx.
Charts tab (chart-card-wrapper.tsx) doesn't have a per-widget
persisted config, so the override only applies in dashboards.
Documented inline.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [backend][frontend][plan] grouping v2 — Phase 10-C: zoom-refetch + time x-axis + plan refresh
Two TODO items from PLAN-grouping-v2-charts.md.
### Zoom-refetch in grouped mode
GroupedLineChart now fires a second graphMultiMetricBatchBucketedGrouped
query with `stepMin`/`stepMax` + `buckets: 1000` when the user
drag-zooms. The base + zoom responses are merged per (logName,
groupKey) with zoom data preferred. Caveat: step-x only — in time-x
mode the step bounds don't translate to a time window, so the zoom
query is `enabled: false`. Timeline zoom needs a separate timeMin/
timeMax pair (deferred follow-up).
### X-axis modes
Backend: queryRunMetricsGroupedBatchBucketed gains an `xAxis: "step" |
"time"` param. Time mode replaces the step-bucket math with
`toUnixTimestamp64Milli(time)` and runs an inline min/max(time)
bounds CTE against mlop_metrics_v2 (mlop_metric_summaries_v2 doesn't
carry time bounds). Returned bucket time is the bucket's start time
as DateTime64(3). Response shape unchanged.
Frontend: GroupedLineChart gains `xAxis?: "step" | "time"`. When
time, x-values are extracted from the columnar `times` field via
parseChTimeMs and `isDateTime` is forwarded to LineChartUPlot so
uPlot renders date labels + tooltips.
Verified against ryandevvm2/groupingVerify (the constant-value
project): time-x mode returns plausible mean-per-time-bucket values
(0+5 → 2.5 for const-zero, etc.) confirming the SQL bucket math.
### Deferred (plan doc updated)
- Relative-time x-axis: needs per-run baselining inside the SQL
aggregator (subtract each run's min(time) before bucketing). Not
trivial.
- Custom-metric x-axis (`train/loss` vs `optim/learning_rate`):
requires a metric-on-metric join on (runId, step), then aggregate
vs the x-metric's value range.
- Lineage stitching in grouped mode (locked off per original
decision #4; needs a design pass on whether parent's pre-fork data
should count toward the child's group).
PLAN-grouping-v2-charts.md was reorganised: items 1-6 marked SHIPPED
or PARTIAL with what landed and what's still open; deferred items now
have a clearer scope so future-me knows what to do.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [backend][frontend] grouping v2 — Phase 10-C revisions per user feedback
Four follow-ups on the Phase 10-C batch:
1. Revert the `mean (min, max)` VALUE-column format. User pointed out
the tooltip already has Min and Max as opt-in columns via the gear
popover — duplicating them inside VALUE was unnecessary.
2. Per-chart override toggle in Chart Settings renamed and flipped:
- Label: "Override Grouping" (was "Group runs in this chart")
- Semantics: ON = override (force per-run for this chart);
OFF / default = follow workspace.
- Plumbing change: `groupingEnabledForChart` (true=group)
→ `groupingOverridden` (true=override).
3. Relative-time x-axis added (was deferred). New SQL branch in
queryRunMetricsGroupedBatchBucketed: a per-run baseline CTE
(`min(time)` per runId) is joined into the bounds + main query so
each run's clock is subtracted before bucketing. Bucket-start
relative-ms is encoded in the response's `step` field; frontend
divides by 1000 to plot seconds since baseline. uPlot's
isDateTime is false in relative mode; xlabel reads "time (s)".
4. ChartWidget's xAxis mapping now covers all three modes (step,
absolute-time, relative-time). Custom-metric-x still falls back
to step — see plan doc.
Verified all 3 modes via curl against groupingTest:
step → bucket0 step=0, value=3.55
time → bucket0 absolute time, value=2.37
relative-time → bucket0 baselined, value=3.88
(distinct values across modes confirm the bucket math actually
differs per axis as expected.)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [plan] grouping v2 — mark relative-time x-axis as shipped
Plan doc was still listing relative-time x-axis as deferred. Updated
the entry to reflect what landed in the Phase 10-C rev…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bumps next from 15.5.10 to 15.5.18.
Release notes
Sourced from next's releases.
... (truncated)
Commits
9ff92cev15.5.1800ebe23[backport] Disable build caches for production/staging/force-preview deploys ...62c97abv15.5.17423623aTurbopack: Match proxy matchers with webpack implementation (#93594)fa78739Turbopack: Fix middleware matcher suffix (#93590)36e62c6[backport] Turbopack: more strict vergen setup (#93588)36589b5[backport][test] Pin package manager to patch versions (#93596)ad6fd4ev15.5.1679d7dffIgnore malformed CSP nonce headers (#103)c4f6908router-server: guard upgrade proxy against absolute-url SSRF (#77) (#102)Maintainer changes
This version was pushed to npm by GitHub Actions, a new releaser for next since your current version.
Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting
@dependabot rebase.Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
@dependabot rebasewill rebase this PR@dependabot recreatewill recreate this PR, overwriting any edits that have been made to it@dependabot show <dependency name> ignore conditionswill show all of the ignore conditions of the specified dependency@dependabot ignore this major versionwill close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this minor versionwill close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this dependencywill close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)You can disable automated security fix PRs for this repo from the Security Alerts page.