- Remove the 'Restoring is non-destructive' admonition from the
bottom of the Versions tab
- Refactor getMockObjectVersions to accept the bucket's current
cap and expiryDays and adapt its output so version-list labels
are always consistent with the policy:
* Never returns a version older than the retention window
(no 'Past Nd limit' surprises when the user set fewer days)
* Never returns more noncurrent versions than the cap
(no '#3 of 2' when the user tightened the cap)
- Anchor all version timestamps to the real clock via
daysAgoFromNow so the mock stays fresh regardless of when the
prototype is opened, instead of drifting away from the fixed
BASE_DATE
- Wire useObjectVersionsQuery to look up the bucket's protection
and pass it into the mock on every fetch, so policy edits from
the bucket modal reflect in the panel next time it opens
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
Show expiration policy context directly in the Versions tab:
- Cap badge and retention badge in the header section
- Combined mode indicator (both/either) when both policies are set
- At-cap and nearing-cap warnings
- Per-version VersionExpiryIndicator showing days remaining and
cap position (#N of M) with warning colors based on combined
and/or policy logic
- Pre-computed noncurrent indices for efficient lookup
- Versioning-suspended admonition banner
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
- Call trigger() after each field.onChange so zod validation runs
immediately as the user types, surfacing errors via FormMessage
- Simplify the S3 cap error message to 'Cannot exceed 100 versions'
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
- Add S3_MAX_NONCURRENT_VERSIONS constant (100) and enforce it in
superRefineBucketProtection independently of plan limits
- Remove HTML min attributes from both inputs so only Supabase inline
errors (FormMessage via FormItemLayout) appear — no browser tooltips
- Plan-specific bounds still enforced as before; the S3 cap is checked
first with a distinct error message
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
- 'both' mode: show a second example where one condition isn't met,
making it clear the version is kept because both must be exceeded
- 'either' mode: show two examples — one where only the version cap
is exceeded, one where only the age limit is exceeded — to
illustrate that either condition alone triggers deletion
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
- Days only: show a version at days+1 (deleted) vs days-1 (kept)
- Versions only: show versions+1 total, oldest one gets deleted
- Both conditions: show versions+1 at days+1, explain the and/or logic
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
- Change InputGroupText suffix from 'max' to 'versions' on the retained
noncurrent versions input
- Show 'See how this works' explainer when either expiration field has a
value, not only when both are set
- Add context-specific explainer text for days-only, versions-only, and
both-conditions cases
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
- Replace custom raw <input> elements with FormItemLayout + InputGroup +
FormInputGroupInput + InputGroupAddon pattern from the design system
- Use layout='flex-row-reverse' for label-left, input-right alignment
- Both version_expiry_days and max_noncurrent_versions default to empty
(no prefill on toggle)
- Add warning Admonition when no expiration policy is configured to
alert about ongoing storage costs
- Remove unused useFormState import and prefill logic
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
Replace the standard form-field layout with the design-handoff UI:
- Sentence builder paragraph that adapts to which fields are filled
(both empty → 'retained indefinitely', one filled → describes that
condition, both filled → sentence with inline toggle)
- Inline [both | either] pill toggle embedded in the sentence text
when both conditions are set (maps to ExpirationMode 'and' / 'or')
- Compact number inputs with suffix labels ('days' / 'max'),
right-aligned text, and em-dash placeholder
- Collapsible 'See how this works' section with a dynamic example
that uses the actual input values and updates on toggle
- Both fields are now optional — empty means 'no limit' for that
condition. Validation only checks min/max when a value is provided.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
Replace bg-border-stronger (oklch with alpha) with solid hsl values:
- Light mode: hsl(0 0% 80%) — neutral mid-gray
- Dark mode: hsl(0 0% 30%) — neutral mid-gray
This prevents the accentuated color at the intersection where
the vertical and horizontal lines overlap.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
- Lift expandedVersionIds state to DeletedFilesContext (shared between
DeletedFilesList and FileExplorerHeader)
- Move 'Expand all' / 'Collapse all' buttons from the table header into
the FileExplorerHeader top bar with outline button variant
- Change tree-connector line color from bg-foreground-muted to
bg-border-stronger for a subtler appearance
- Increase gap between horizontal tree branch and version title text
(pl-[22px] → pl-[36px], w-[11px] → w-[8px])
- Apply same tree line changes to standalone Trash page (TrashList)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
- Tree connector lines use bg-foreground-muted instead of bg-border for
better visibility
- Horizontal branch shortened from w-[15px] to w-[11px], creating a 4px
gap between the line end and the version text (text still aligns with
parent name)
- Replaced the icon-only expand/collapse toggle in the table header with
two separate text buttons: 'Expand all' and 'Collapse all', separated
by a dot divider
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
Replace CornerDownRight icon with CSS tree-connector lines for nested
version rows. Each version draws:
- A vertical line from the top border to the junction point (center)
- A horizontal branch from the junction to the version text
- For non-last versions, the vertical line continues to the bottom
border, connecting seamlessly with the next row
- For the last version, the vertical line stops at the junction (└ shape)
Lines are positioned at left-[23px] to align with the parent row's
chevron center (16px cell padding + 7px icon center). Version text
starts at pl-[22px] to align with the parent row's name text.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
- Version rows are now selectable via checkboxes for batch multi-selection,
using composite keys (objectId::versionId) in the shared selection state
- Clicking a version row opens the preview panel with version-specific
metadata (version ID, action, created date, size) and a 'no preview
available' placeholder
- Aligned CornerDownRight icon with parent row chevron and version text
with parent name text (both use gap-x-2 and size-14 icons)
- Added expand/collapse all toggle button in the table header top right
(ChevronsUpDown / ChevronsDownUp icons)
- Restore and delete actions on version rows now perform real client-side
effects: version-level mock store mutations + query cache invalidation
- Removed the Expires column from both DeletedFilesList and TrashList
- Extended DeletedFilesContext with selectedDeletedVersion state
- Added version-level mutation functions to protection-mocks.ts
- Added useTrashVersionRestoreMutation and useTrashVersionDeleteMutation
hooks to bucket-trash-query.ts
- Updated Trash page (standalone) with version-level mutation wiring
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
- PreviewPane: add Info hint below Delete button when versioning is
active, explaining that deleting soft-deletes the object and all
versions and they can be restored from the deleted versions view
- VersionHistory: add Get URL (Copy) icon button on every version row
alongside existing Download/Restore/Delete actions
- DeletedFilesList & TrashList: implement collapsible nested grouping
for noncurrent versions under parent objects with ChevronRight/Down
expand toggle, version count badge, and per-version Restore/Delete
actions on hover
- protection-mocks: expand mock data from 8 to 12 trash objects with
richer version histories (up to 6 noncurrent versions per object)
to properly showcase the nested deleted versions UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
Task A: Add ExpirationMode (and/or) toggle between retention-days and
max-versions fields in bucket settings. 'and' = single S3 lifecycle policy
(both conditions must be met), 'or' = two independent rules (either triggers).
Wired into form schema, create/edit bucket modals, and mock store.
Task B: Move 'Show deleted versions' toggle from breadcrumb bar into the
file explorer table header, right of the search input. Switch is on the left
of the label text.
Task C: Add noncurrent version rows beneath each deleted object in TrashList.
Nested rows have left indentation with a CornerDownRight icon and lighter
background. Added DeletedObjectVersion interface and mock data to
protection-mocks.ts. Updated all user-facing terminology from 'files' to
'versions' across Trash, TrashList, TrashSelectionBar, DeletedFilesList,
DeletedFilesHeaderSelection, DeletedFilePreviewPane, and bucket page tabs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
- Reverted the timeline row's Restore action back to its icon-only
text-variant button — the ask to make Restore a default-variant
button was about the "Back to latest" action in the version
preview banner, not the per-row action in the Versions tab
- The version preview banner's action is now labeled Restore, styled
as a default-variant button, and actually restores the previewed
version as current (instead of just clearing the preview)
- Versioning can no longer be turned fully "off" on a bucket that's
ever had it enabled — it moves to a new suspended state instead,
matching how bucket versioning actually behaves. Suspending only
stops new noncurrent versions from being created; nothing already
retained is deleted, so the destructive typed-confirmation flow for
disabling versioning is gone
- Added `hasVersioningHistory` (enabled OR suspended) alongside the
existing `isBucketVersioned` (enabled only) and repointed every
surface that manages or warns about retained version/trash data to
the former, since a suspended bucket can still be sitting on plenty
of it
- Bucket list "Versioning" column, plan-downgrade admonition, and the
Versions tab now all reflect the three states (Enabled / Suspended /
never enabled)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
- Versions tab in file preview now only shows when the current bucket
has object versioning enabled, instead of the global feature flag
- Renamed the current-version badge from Latest to Current
- Previewing-version banner now puts the version date on its own line
- Restore action is now a labeled default-variant button instead of
an icon-only text button
- Hover-underline moved from the timeline dot to the version date, and
the currently-previewed version is now highlighted by coloring its
date text and dot to match the brand color
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
Usage breakdown & chart:
- Split "Object versions" into "Noncurrent versions" and "Soft-deleted files"
in the Storage Size breakdown, in the mock data model, and in the chart's
stacked bars.
- Wire the mock retention series into the Storage Size chart itself so the
daily bars actually visualize the live / noncurrent / soft-deleted split,
not just the inline breakdown.
Edge cases:
- Tightening retention (fewer days or fewer versions than the bucket already
has) shows an inline admonition in the edit modal explaining that some
retained data will be permanently expired on save.
- EmptyBucketModal: when the bucket is versioned, the destructive banner
copy and an extra warning admonition call out that emptying also purges
every noncurrent version and soft-deleted file.
- DeleteBucketModal: appends a note that all versions and soft-deleted files
will be lost, when the bucket is versioned.
- Plan downgrade auto-disable-and-purge: on the buckets list, an effect
detects when the org plan no longer supports versioning and invokes a new
purgeVersioningOnPlanDowngrade() helper that flips every enabled bucket in
the mock store back to disabled and clears the shared trash store, then
shows a warning admonition listing the affected buckets.
- Public bucket + versioning: inline admonition in the modal explains that
public buckets expose every version by default and points to an RLS-based
mitigation (filter storage.objects by an isCurrent metadata flag).
Version cap indicator:
- In VersionHistory, show a `{count} / {cap} noncurrent` badge when a cap is
configured, with contextual copy at ≥80% and at cap explaining that older
versions auto-expire on the next overwrite.
Copy:
- Reword "Noncurrent version retention" and "Max noncurrent versions"
descriptions to be concise/factual instead of using "this many" phrasing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
- Disabling object versioning on a bucket that already has it enabled now
requires typing the bucket name in a destructive TextConfirmModal,
warning that every noncurrent version and soft-deleted file will be
permanently deleted once saved. New buckets (never had versioning) skip
this since there's nothing to lose. An inline Admonition also reflects
the staged, not-yet-saved change in the form.
- Resume a (snapshot-free, adapted for this branch's object-level-only
scope) version of the org Usage page's Storage Size breakdown: a
Live objects vs Object versions split, a "retained recovery data"
warning, and a per-bucket breakdown of retained bytes. Backed by a new
mock retention-usage query (data/storage/protection/storage-retention-usage-query.ts),
rendered via the Storage Size category's additionalInfo slot in
/org/[slug]/usage.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
- Prefill "Noncurrent version retention" and "Max noncurrent versions"
with the plan's default values when the Object versioning switch is
turned on (only if the fields are still empty), so users can save
immediately instead of starting from a blank input.
- In the deleted files view, render the preview panel as an absolutely
positioned overlay over the rows instead of a flex sibling that
shrinks the table. Rows keep their full width and stay horizontally
scrollable underneath the panel.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
- Show an "Enabled" badge instead of "object-level" text under the
buckets list "Versioning" column, since only object-level versioning
exists in this scope.
- Tighten vertical padding on deleted files table rows (px-4 py-2
instead of the default p-4).
- Only show the "Show deleted files" toggle for buckets with versioning
enabled; move the Switch to the right of its label.
- Move isShowingDeleted from local useState to a nuqs query param
(?deletedFiles=true) so the deleted files view is linkable/bookmarkable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
- Fix "Cannot read properties of undefined (reading 'content')" crash:
the snapshot-removal pass dropped the required `tooltip` prop from two
ButtonTooltip calls (TrashList's per-row delete, Trash's "Delete all
permanently"). Swap both to plain Button since no dynamic tooltip is
needed there.
- Rework DeletedFilesList (the "Show deleted files" switch view, now
the default buckets page) from stacked card rows into a proper single-line
Table matching the Files/Deleted files tab's TrashList row styling and
column headers (Object, Original location, Deleted, Size, Expires).
- Add per-row Restore/Delete permanently actions that only reveal on row
hover, alongside the existing multiselect checkbox and click-to-preview
behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
Snapshots aren't implemented in this branch yet, so drop the
heldBySnapshot field from TrashObject/ObjectVersion mocks and all
related UI: disabled states, tooltips, badges, and copy across the
deleted files list/preview, version history, and bulk trash actions.
Delete-all/permanent-delete mutations now act on every selected/listed
item unconditionally.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
- Bucket versioning: create/edit bucket modals now write to an in-memory
mock store (setMockBucketProtection) on save, so the buckets list
"Versioning" column reflects the change immediately. Resets on page
refresh since it's plain module state (no real API yet).
- Deleted files: restore/permanent-delete mutations now actually mutate
the trash mock store instead of no-op delaying, so the deleted files
list updates after restoring or hard-deleting items.
- Added 5 more dummy trash entries (8 total) with varied held/expiry
states.
- Changed all "Delete permanently"/"Delete all permanently" trigger
buttons to the `danger` Button variant.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
- Reduce per-item bottom padding from pb-8 to pb-4
- Remove the hover/selected background on each version row entirely;
instead underline the timeline dot on hover to signal interactivity
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
- Move min/max/required validation for version_expiry_days and
max_noncurrent_versions into a shared superRefine (BucketDataProtectionFields.schema.ts),
called from each modal's own superRefine, instead of imperative
validateVersioningFields() + form.setError() calls in onSubmit. Errors
now surface the same way as the rest of the form (FormItemLayout/FormMessage),
matching Studio's established form-error convention.
- Switch the fields to the '' empty-sentinel + z.coerce.number() union
pattern (per the react-hook-form skill) instead of z.string().optional(),
keeping inputs fully controlled without special-casing undefined.
- Build each modal's schema dynamically via useMemo(() => schema(planLimits), ...)
since the versioning bounds depend on the async-loaded org plan.
- Replace destructured useFormContext().watch() with useWatch({ control, name })
in BucketDataProtectionFields, avoiding the ratcheted no-use-watch lint warning.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
- Move versioning fields (retention days, max noncurrent versions) from
local useState into the parent form via react-hook-form + zod, fixing
the bug where clearing an input forced it back to "0" and blocked
typing a fresh single digit
- Show a validation error on submit if versioning is enabled but a
field is left empty
- Gate object versioning by org billing plan: disable the toggle and
show an upgrade prompt on the Free plan (which also doesn't support
lifecycle policy management); enforce plan-specific min/max ranges
for retention days and max versions on Pro/Team/Enterprise
- Add getVersioningPlanLimits() + validateVersioningFields() helpers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
Shows "object-level" for buckets with versioning enabled, "-" otherwise,
based on the existing isBucketVersioned() mock helper. Snapshots-level
versioning will be added in a future iteration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
Deleted files batch actions:
- Move Restore/Delete permanently buttons into the StorageExplorer header
row (same position as the normal file selection bar)
- Show "X items selected" text on the far left, actions on the far right
- Update select-all row to show "Select/Unselect all X files" label
Bucket versioning fields:
- Use FormItemLayout with flex-row-reverse layout for retention and max
versions fields (label left, input right)
- Use InputGroup with InputGroupAddon for unit suffixes ("days", "versions")
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
- Reduce vertical gap between detail fields from space-y-6 to space-y-3
- Make Restore/Delete permanently actions sticky at the bottom with a
border-t separator, only the preview + details area scrolls
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
- Move "Show deleted files" toggle from StorageExplorer header to
page-level breadcrumbs as a Switch control, left of Policies button
- Add checkbox multiselect for deleted files with bulk Restore/Delete
permanently actions and shift-click range selection
- Keep search input visible when showing deleted files, with contextual
placeholder text
- Add file preview thumbnail to deleted file preview pane
- Change "Delete permanently" button to default variant (matching Restore)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
Add an alternative bucket view at /storage/files/bucketsV2/[bucketId]
that replaces the separate Files/Deleted files tabs with a unified
StorageExplorer. A "Deleted files" toggle button in the header's
actions bar (left of Navigate) switches between live files and
soft-deleted items inline.
When showing deleted files:
- The file listing is replaced with a DeletedFilesList showing all
soft-deleted objects with name, size, deletion time, and origin
- Clicking a deleted item opens a dedicated DeletedFilePreviewPane
with Original location, Deleted at, Deleted by, Size, Expires,
and Restore / Delete permanently actions
- Upload, search, view options, and folder creation are hidden
The existing bucket page is unchanged (wrapped in DeletedFilesProvider
with enabled=false). The bucketsV2 route is navigable only via URL.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
Scoped-down storage protection prototype with three features:
- Bucket-level versioning policy (retention days, max versions) in create/edit modals
- Object version history in the file preview pane with restore/delete actions
- Per-bucket deleted files (trash) view with bulk selection, restore, and permanent delete
All data is mock-backed behind the STORAGE_PROTECTION_ENABLED feature flag.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
## Context
As per PR title - should not have any visual nor functional change
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **Changes**
- Standardized infrastructure, region, and database configuration around
AWS-based environments.
- Removed Fly.io-specific region and provider options from project
creation, instance sizing, and infrastructure settings.
- Enabled disk validation, spend-cap eligibility, backup restoration,
and extension setup consistently across supported projects.
- Updated billing and region displays to use the applicable AWS
configuration.
- **Bug Fixes**
- Corrected project-specific restrictions that could incorrectly hide
configuration and billing controls.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Context
Adds a banner toast for the database connections feature preview
<img width="315" height="322" alt="image"
src="https://github.com/user-attachments/assets/8caaab88-10a0-4a06-b678-25fc9c44dd81"
/>
## Other changes
As the observability page currently has a number of banner toasts
(metrics API, unified logs, index advisor for query performance), am
opting to REMOVE the metrics API's banner toast by virtue of how long
its been around for. Mainly to prevent over stacking of banner toasts as
it can be annoying.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added a dismissible Database Connections banner with SQL examples and
a link to its feature preview.
* Banner dismissal and CTA interactions are now tracked.
* Dismissed banners can reappear when reintroduced.
* **Bug Fixes**
* Banners are hidden after the feature is enabled or dismissed.
* Improved banner handling to prevent duplicate active banners.
* **Changes**
* Replaced the Metrics API banner with the Database Connections banner.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Context
Given that our number of feature previews have been expanding, am opting
to group them into categories for easier understanding of the context of
each feature preview.
Ideally we're able to tag all feature previews into categories (or add
more categories), but leaving the unclassified ones under "others" for
now
<img width="936" height="661" alt="image"
src="https://github.com/user-attachments/assets/b48bd9a2-fe33-4cb0-9288-1cd9c8264da0"
/>
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Feature previews are now organized into expandable categories.
* Observability and database previews are grouped for easier browsing.
* Uncategorized previews remain available under an “Others” section.
* Existing feature selection options and sorting behavior are preserved.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## What kind of change does this PR introduce?
Bug fix stacked on #48478.
## What is the current behavior?
Storage Explorer renders the custom closed-folder icon at `1.5` but
leaves the Lucide open-folder icon at its default `2`. The duplicated
file-picker implementation also uses a different set of stroke-width
overrides.
## What is the new behavior?
A shared `StorageRowIcon` renders loading, open and closed folder,
image, audio, video and generic file icons at `1.5` across Storage
Explorer, row editing and the bucket file picker.
Test by comparing open and closed folders and file-type rows in Storage
Explorer and the bucket file picker.
| Before | After |
| --- | --- |
| <img width="524" height="336" alt="CleanShot 2026-08-03 at 18 38
06@2x"
src="https://github.com/user-attachments/assets/15eb8b9f-69fc-4eee-8428-d7ec26dce8dc"
/> | <img width="522" height="328" alt="CleanShot 2026-08-03 at 18 39
20@2x"
src="https://github.com/user-attachments/assets/17b7dddd-8d36-421c-8356-c9c1bd7456e8"
/> |
| _Thicker image and file icon compared to folder icon_ | _Every icon
has the same stroke thickness_ |
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Improvements**
* Standardized file, folder, media, and loading icons across storage
views.
* Improved visual consistency with unified icon sizing, styling, and
stroke width.
* **Tests**
* Added coverage for loading, folder, media, and generic file icon
states.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Context
As per PR title - brings Database Connections into feature preview
Should be working for both hosted + self-host/local
Also adjusts existing feature previews to remove "New"
- Platform webhooks
- Temporary database access
<img width="600" alt="image"
src="https://github.com/user-attachments/assets/b18ae8ca-ce0b-4649-975c-e70749a87dcd"
/>
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added a Database Connections preview highlighting live activity, query
blocking detection, session termination, and AI-assisted summaries.
* Added access to project-specific observability connections from the
preview.
* Added a Database Connections entry to the observability menu when
enabled.
* **Improvements**
* Updated feature previews and labels, including changes to “new” status
indicators.
* Added controls to manage Database Connections preview visibility.
* **Bug Fixes**
* Improved blocker detection so results respect the selected role
filters.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Enabling a feature preview that has a route (e.g. Column-level
privileges) closed the modal but never navigated to the feature's page
on the TanStack runtime (local + staging). The modal closed itself via a
nuqs query-param update *and* called `router.push` — the queued nuqs
flush navigates to the pathname it captured before the push, landing
after the redirect and reverting it. The Next runtime was unaffected
because the stock nuqs pages adapter patches the URL shallowly via the
history API instead of navigating.
**Changed:**
- When the enabled preview has a `getRoute`, skip the explicit
`toggleFeaturePreviewModal(false)` — `router.push(route)` navigates
without the `featurePreviewModal` param, which is what closes the modal.
One URL update instead of two racing ones; works on both runtimes.
- Previews without a route keep the explicit close (unchanged behavior).
## To test
- On a project page, open Feature Previews (avatar menu), select
**Column-level privileges**, click **Enable feature** → modal closes and
you land on `/project/{ref}/database/column-privileges` with the "We've
taken you to where you can try it out." toast (no bounce back to the
previous page)
- Repeat with **Disable Advisor rules** → lands on
`/project/{ref}/advisors/rules/security`
- Enable a preview without a route (e.g. **PG Delta Diff**) → modal
closes, stays on the current page, "It's now active across the
dashboard." toast
- Disable a preview → modal stays open, "disabled" toast, no navigation
- Verified locally on the TanStack runtime; worth a quick click-through
on the Vercel preview (Next runtime) too
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved feature activation navigation to prevent conflicting URL
updates.
* Non-route features continue to close the preview modal and display the
activation confirmation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
## What kind of change does this PR introduce?
Bug fix and internal tooling update. Resolves FE-3472.
## What is the current behavior?
Custom Studio icons use inconsistent source stroke widths, and some
child-level styling prevents component props from overriding them. Mixed
custom and Lucide icon sets can therefore appear uneven.
## What is the new behavior?
Custom stroke icons use a root-level `stroke-width="1.5"`; fill-only
logos use `stroke="none"`. The build validates that contract and
regenerated components preserve existing exports and props.
Studio applies the same `1.5` weight across Reports categories and uses
one shared destination icon mapping in the replication selector,
destination rows and diagram.
| Before | After |
| --- | --- |
| <img width="418" height="516" alt="56398"
src="https://github.com/user-attachments/assets/6afa7042-e6be-40e7-9911-af2f61238c9d"
/> | <img width="390" height="550" alt="CleanShot 2026-07-30 at 17 12
37@2x"
src="https://github.com/user-attachments/assets/870f49cf-c8fa-40db-8be8-2eb5f264ff4a"
/> |
| <img width="510" height="734" alt="CleanShot 2026-07-30 at 17 19
28@2x"
src="https://github.com/user-attachments/assets/a5b2c088-dcd2-4907-976b-5820794d06e3"
/> | <img width="554" height="742" alt="CleanShot 2026-07-30 at 17 16
06@2x"
src="https://github.com/user-attachments/assets/ed3a77c4-5d94-4ca7-b9e4-1403b725a981"
/> |
## Testing
At 100% zoom, compare custom and Lucide icon weight in:
- Reports: **Add your first chart** and **Add block**
- Database > Replication: the destination selector, destination rows and
replication diagram
- Command menu (`⌘K`): **Search Database Tables**, **Search RLS
Policies**, **Search Edge Functions** and **Search Storage**
- Authentication > Users: right-click a user row and compare the
context-menu icons
- Database > Schema Visualizer: open a table node overflow menu
- A paused project: **Export your data > Download backups**
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added consistent destination icons across replication panels, rows,
and diagrams.
* Updated instance health and metric icons for clearer identification.
* Standardized icon stroke weight and reduced default icon stroke
thickness.
* **Documentation**
* Clarified custom icon requirements, default properties, and validation
guidance.
* **Bug Fixes**
* Improved consistency of icon rendering across replication destinations
and reports.
* **Tests**
* Added coverage for icon SVG validation and replication destination
icon rendering.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Context
This is pre-requisite work for adding support to managing custom reports
from the Assistant. Planning to break this into a number of PRs, briefly
- Adding read support for custom reports
- Adding write support for custom reports
- Adding run support for custom reports
- Should be able to infer data from the results then
This PR starts with adding support for listing and reading custom
reports from the Assistant
## Other changes involved
- Updates setting up of the home page report to have better title and
description
- Swaps the variant of the ToggleGroup in the SQL block for custom
reports as the default variant blends into the background color of the
PopoverContent
## To test
- [ ] Assistant should be able to list custom reports + read its
contents
<img width="428" height="755" alt="image"
src="https://github.com/user-attachments/assets/6a15b660-c0ee-4a06-984c-87eff3943eec"
/>
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Added AI-assisted tools to list reports and retrieve report details,
including chart counts, layouts, configurations, and SQL-backed chart
information.
- Added clearer empty-state messaging when no snippets are available.
- **Improvements**
- New homepage reports now use the name “Homepage Report” and include a
descriptive project-home summary.
- Updated query controls with refreshed visual styling.
- Improved content requests to support additional request context.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## What kind of change does this PR introduce?
Bug fix. Resolves DEPR-539.
## What is the current behavior?
When a focused child unmounts, Radix can move focus to the Sheet wrapper
and break the expected tab order. Several callsites suppress the
wrapper's tabindex individually.
## What is the new behavior?
Sheet still focuses its first interactive child when opened, but the
wrapper itself is no longer focusable by default. Callers can opt in
with an explicit `tabIndex` when needed.
## Additional context
### Testing
Compare this Studio experience on both this branch and `master`:
1. Open any project with an Edge Function.
2. Go to **Edge Functions**, open the function, then click **Test**.
3. Under **Headers**, click **Add Headers**. Click the first header key
input, then Tab slowly through the header inputs and remove buttons.
On `master`, focus can jump to the whole Sheet. On this branch, focus
stays on the controls in order.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved keyboard focus behavior across sheets and panels.
* Sheets now focus the first available interactive element when opened,
without adding unnecessary focus targets.
* Preserved support for programmatic focus and prevented focus from
unexpectedly moving to the sheet when focused content is removed.
* Updated authentication, integrations, connection, logging, storage,
and other sheet interfaces consistently.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## What kind of change does this PR introduce?
Bug fix (dirty form dismissal for Replication destination sheets), plus
small docs/skill updates so agents pick up the existing modality
pattern.
## What is the current behavior?
Closing the Add/Edit destination sheet (Cancel, Escape, or backdrop)
discards in-progress form state with no confirm. Same for the nested
Create publication sheet.
## What is the new behavior?
Dirty closes go through `useConfirmOnClose` +
`DiscardChangesConfirmationDialog`, matching other Studio sheets.
Successful submit still closes without prompting.
Also: skills + `forms.mdx` now point at Modality “Dirty form dismissal”.
| After |
| --- |
| <img width="1024" height="759" alt="Replication Database Chisel
Toolshed Supabase"
src="https://github.com/user-attachments/assets/6f568a2a-c76b-442a-b592-d638bb36adc4"
/> |
### How to test
1. Studio → Database → Replication → **Add destination** (any pipelines
type with access).
2. Change a field so the form is dirty.
3. Try Cancel, Escape, and backdrop click → discard dialog appears;
**Keep editing** stays open; **Discard changes** closes.
4. Submit successfully with a valid config → sheet closes with no
discard dialog.
5. Repeat for **Edit destination** from a destination row menu.
6. Optional: Add destination → create a new publication from the
publication picker → dirty that nested sheet and dismiss the same way.
7. Optional: Add destination → Read Replica → change region → dismiss →
discard dialog; deploy still closes without prompting.
## Additional context
Sheet owns the close guard; forms report dirty via a ref because RHF
lives in the child. Nested `NewPublicationPanel` wires the guard
locally.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Added unsaved-changes tracking to replication destination and
publication forms.
- Added confirmation prompts before closing forms with unsaved changes
via Cancel, Escape, or backdrop dismissal.
- Forms now reset appropriately after successful submission or confirmed
dismissal.
- **Documentation**
- Updated form and UI pattern guidance to document dirty-form dismissal
behavior for sheets and dialogs.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
## What kind of change does this PR introduce?
Accessibility / UX fix
([DEPR-630](https://linear.app/supabase/issue/DEPR-630)).
## What is the current behavior?
In Storage **As columns** view, keyboard select is unclear: the checkbox
stays hidden until hover, so Tab/Space selection is hard to see. The row
actions (three dots) menu also shows a browser default blue outline on
Tab.
## What is the new behavior?
Same icon/checkbox swap as list/hover, but also on keyboard focus:
- Checkbox replaces the icon on hover, `:focus-within`, and when
selected (no layout shift)
- Checkbox becomes visible when focused via keyboard (without needing
hover)
- Row gets an inset outline while the checkbox is focused
- Folders have no checkbox (non-focusable spacer only)
- Row actions trigger uses `focus-ring` instead of the browser blue
outline
| Before | After |
| --- | --- |
| <img width="1046" height="362" alt="CleanShot 2026-07-31 at 14 37
47@2x"
src="https://github.com/user-attachments/assets/9b827627-17e6-49a6-87a3-1253b4cef1da"
/> | <img width="1046" height="390" alt="CleanShot 2026-07-31 at 14 37
13@2x"
src="https://github.com/user-attachments/assets/2fd2b426-a8e2-4fda-b1ba-4df728b7b2f1"
/> |
| _Checkbox focussed but not visually shown_ | _Checkbox focussed and
visually shown_ |
## To test
1. Open the **Studio preview** for this PR.
2. Go to **Storage → Files** → open a bucket with several files.
3. Set view to **As columns**.
4. Tab until a **file** checkbox is focused.
**Expect:**
- Icon is replaced by the checkbox (same slot; neighbouring row icons
should not look shifted)
- Checkbox visible without hovering
- Row shows an inset outline
5. Press **Space** to select. Checkbox stays in the icon slot; selection
background applies.
6. Hover another file. Same icon to checkbox swap as before.
7. Tab to the three-dot actions control on a row. Expect the shared
focus ring (not a blue browser outline); the menu icon should become
visible.
8. Folders: no checkbox in the tab order; click icon/name still opens
the folder.
9. Smoke **As list**. Same swap behaviour.
## Additional context
From Kemal's DEPR-621 review.
## What kind of change does this PR introduce?
Feature + docs. Stacked on #48161 (logo contract /
[DEPR-604](https://linear.app/supabase/issue/DEPR-604/define-connect-logo-asset-and-variant-contract)).
## What is the current behavior?
After #48161, curated logos only resolve from allowlisted `redirect_uri`
hosts. A requester can still present a trusted partner **name** (e.g.
Claude) while redirecting to an unrelated remote host; the UI shows
Supabase alone but does not call out the mismatch.
## What is the new behavior?
- Shows a caution admonition when the requester name looks like a
trusted partner (Claude, Cursor, ChatGPT/OpenAI, Perplexity) but
`redirect_uri` is a **remote** host outside that partner's allowlist.
- Skips localhost / loopback redirects for the caution (common for local
MCP clients); those still get curated logos when the name matches a
trusted partner.
- Highlights the footer redirect URL in warning colour when the caution
is shown.
- Documents the behaviour in the Connect interstitials pattern.
### To test
Real MCP clients (Claude, Cursor, etc.) only send users to
**production** `/authorize`, so you cannot drive a local or preview
Studio build from those tools. Use a Network override instead:
1. Start Studio and sign in (`pnpm dev:studio`, or use the [Vercel
preview](https://studio-staging-git-danny-oauth-impersonation-warning-supabase.vercel.app/)).
2. Open `/dashboard/authorize?auth_id=foo` (any `auth_id` is fine; the
real response may 404) ([Vercel
preview](https://studio-staging-git-danny-oauth-impersonation-warning-supabase.vercel.app/dashboard/authorize?auth_id=foo)).
3. DevTools → **Network** → find `GET
…/platform/oauth/authorizations/foo` (or whatever id you used).
4. Right-click → **Override content** (enable Local Overrides / pick a
folder if prompted).
5. Paste one of the payloads below (status **200**), save, then reload
the authorize page.
6. Keep `expires_at` in the future so the request does not look expired.
#### Impersonation caution (trusted name + remote non-allowlisted
redirect)
Expect:
- Supabase alone (no curated Claude mark)
- Caution: “Redirect does not match this app name”
- Footer redirect URL in warning colour
```json
{
"name": "Claude",
"website": "https://claude.ai",
"icon": null,
"domain": "claude.ai",
"redirect_uri": "https://evil.com/callback",
"expires_at": "2099-01-01T00:00:00.000Z",
"scopes": ["organizations:read", "projects:read"],
"approved_at": null,
"registration_type": "dynamic"
}
```
| Preview |
| --- |
| <img width="764" height="958" alt="Authorize Claude Supabase"
src="https://github.com/user-attachments/assets/e6eee016-5710-41ba-9925-87511e009e22"
/> |
#### Localhost MCP: no caution
Expect curated Claude + Supabase pair (name match + loopback), **no**
caution, normal footer colour. Local MCP clients often use loopback
redirects.
```json
{
"name": "Claude",
"website": "https://claude.ai",
"icon": null,
"domain": "claude.ai",
"redirect_uri": "http://127.0.0.1:42813/callback",
"expires_at": "2099-01-01T00:00:00.000Z",
"scopes": ["organizations:read", "projects:read"],
"approved_at": null,
"registration_type": "dynamic"
}
```
| Preview |
| --- |
| <img width="764" height="958" alt="Authorize Claude Supabase"
src="https://github.com/user-attachments/assets/79f36865-3c8e-43e5-9490-24288efc74aa"
/> |
#### Legitimate curated partner: no caution
Expect curated Cursor + Supabase pair, no admonition, normal footer
colour.
```json
{
"name": "Cursor",
"website": "https://cursor.com",
"icon": null,
"domain": "cursor.com",
"redirect_uri": "https://cursor.com/callback",
"expires_at": "2099-01-01T00:00:00.000Z",
"scopes": ["organizations:read", "projects:read"],
"approved_at": null,
"registration_type": "dynamic"
}
```
| Preview |
| --- |
| <img width="764" height="958" alt="56164"
src="https://github.com/user-attachments/assets/412333a3-a74f-42eb-9f63-d56b6a26bf91"
/> |
#### Unrelated name + remote redirect: no caution
Expect Supabase alone (no icon), no admonition.
```json
{
"name": "Acme Tools",
"website": "https://evil.com",
"icon": null,
"domain": "evil.com",
"redirect_uri": "https://evil.com/callback",
"expires_at": "2099-01-01T00:00:00.000Z",
"scopes": ["organizations:read", "projects:read"],
"approved_at": null,
"registration_type": "dynamic"
}
```
| Preview |
| --- |
| <img width="764" height="958" alt="Authorize Acme Tools Supabase"
src="https://github.com/user-attachments/assets/dab24817-5c26-4aa1-a447-796c4af5868b"
/> |
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
## Summary by CodeRabbit
- **New Features**
- Added an OAuth caution when a requester name matches a known partner
but uses an unapproved remote redirect host.
- Improved trusted partner logo selection for localhost/loopback
redirects while preserving safe fallbacks for untrusted redirects.
- **Documentation**
- Updated Connect interstitial guidance for redirect mismatches and
localhost/loopback behavior.
- **Tests**
- Expanded coverage for caution visibility, messaging, localhost logo
pairing, and trusted redirect scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## What kind of change does this PR introduce?
Bug fix and design-system update.
## What is the current behavior?
API authorisation and AWS Marketplace action failures use transient
toasts. The inline action-error treatment introduced for organisation
invitations is implemented locally.
## What is the new behavior?
Action failures remain visible below their actions and clear on retry or
organisation change.
This PR adds a shared `InterstitialActionError` component, updates the
connect-interstitial guidance and demo to use it, and retroactively
applies it to `OrganizationInvite`.
Mutation errors are read directly from their mutation hooks rather than
copied into component state.
| Before | After |
| --- | --- |
| <img width="1024" height="759" alt="Authorize API Access Supabase"
src="https://github.com/user-attachments/assets/9520aff3-496d-44b1-b5b5-02b331872e32"
/> | <img width="1024" height="759" alt="Authorize API Access Supabase"
src="https://github.com/user-attachments/assets/2d09e337-573a-45b5-80ac-7c546ed1401d"
/> |
| <img width="1024" height="759" alt="Link AWS Marketplace Supabase"
src="https://github.com/user-attachments/assets/bb1a4581-0399-432a-8037-d84ab15ecc4b"
/> | <img width="1024" height="759" alt="Link AWS Marketplace Supabase"
src="https://github.com/user-attachments/assets/f9d43cd9-c661-42ed-91c0-e45ccb9c19f5"
/> |
_Note since taking that AWS screenshot: the error message now replaces
the prior footer text. I.e. “Learn more about billing through AWS.” is
now gone when an error message is present._
## To test
### AWS Marketplace
For a visual check with local Studio running:
1. In
`apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboarding.tsx`,
immediately before `if (!buyerId)`, temporarily add:
```tsx
return (
<AwsMarketplaceInterstitial>
<div className="flex flex-col gap-5">
<InterstitialAccountRow displayName="reviewer@example.com" />
<OrganizationSelector
organizations={[
{
name: 'Example Organization',
slug: 'example-organization',
plan: { id: 'pro', name: 'Pro' },
} as Organization,
]}
selectedSlug="example-organization"
disabled
onSelect={() => undefined}
/>
<div className="flex flex-col gap-5">
<div className="flex flex-col gap-2">
<Button variant="primary" block>
Link organization
</Button>
<InterstitialActionError error="Failed to link organization: Test error"
/>
</div>
<p className="text-center text-xs text-foreground-lighter text-balance">
<InlineLink href={`${DOCS_URL}/guides/platform/aws-marketplace`}>
Learn more
</InlineLink>{' '}
about billing through AWS.
</p>
</div>
</div>
</AwsMarketplaceInterstitial>
)
```
2. Open `http://localhost:8082/aws-marketplace-onboarding?buyer_id=test`
while signed in.
3. Confirm the error appears below **Link organization** with a divider.
Remove the temporary return before committing anything.
### API authorization
For a visual check with local Studio running:
1. In
`apps/studio/components/interfaces/ApiAuthorization/ApiAuthorization.Valid.tsx`,
immediately before `if (isLoading)`, temporarily add:
```tsx
return (
<ApiAuthorizationMainView
approvalState="indeterminate"
form={form}
requester={{
name: 'Test App',
website: 'https://example.com',
icon: null,
domain: 'example.com',
scopes: [],
expires_at: '2099-01-01T00:00:00.000Z',
approved_at: null,
registration_type: 'static',
}}
organizations={{
_tag: 'success',
organizations: [
{ name: 'Example Organization', slug: 'example-organization' } as
Organization,
],
}}
requestedOrganizationSlug={undefined}
actionError="Failed to authorize request: Test error"
onOrganizationChange={() => undefined}
onApprove={() => undefined}
onDecline={() => undefined}
/>
)
```
2. Open `http://localhost:8082/authorize?auth_id=test` while signed in.
3. Confirm the error appears below the authorisation actions with a
divider.
Remove the temporary return before committing anything.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added consistent inline error messaging for authorization,
organization invitations, and AWS Marketplace onboarding.
* Error messages now appear within the relevant interstitial and replace
supporting footer content until resolved.
* Retry and action buttons remain available after failed operations.
* **Bug Fixes**
* AWS Marketplace linking failures no longer trigger toast
notifications.
* Billing guidance is hidden while an onboarding error is displayed.
* **Tests**
* Added coverage for authorization, cancellation, and AWS Marketplace
failure states.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.
YES
## What kind of change does this PR introduce?
Performance improvement
## What is the current behavior?
The column privileges page in Studio only ever renders one table, but
the underlying query still `aclexplode`s every column in the whole
schema and filters the result client-side.
## What is the new behavior?
Adds a scoped SQL path that prunes `pg_class`/`pg_namespace` to the
requested schema+table before exploding ACLs, gated behind the
`pgMetaScopedIntrospection` flag, with a plan-guard test asserting
`pg_class`/`pg_attribute` stay index-driven. Studio's query hook and
cache keys now thread the selected table through so column-privilege
invalidation and cold-load races are scoped correctly, and the page
fetches per-table instead of per-schema.
## Additional context
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Improvements**
* Column privileges are now scoped to the selected schema and table for
more accurate results.
* Changing schemas automatically updates the table selection and
refreshes the displayed privileges.
* Privilege updates now refresh only the relevant schema, table, and
column data.
* Loading states are handled more accurately when no table is selected.
* **Bug Fixes**
* Improved consistency between scoped and unscoped column privilege
results, including table-, column-, and grant-option privileges.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->