The preview URL is cached against the object's path for a week and the
object itself is served with `cache-control: 3600`, so after writing a new
version the panel kept rendering the bytes from before it.
The default preview now pins to the current version's id, which changes the
query key and the URL whenever a new version lands, and `replaceFile`
invalidates every cached URL for the path.
Also reports what Storage actually rejected on a replace: a bad mime type
(415) and an oversized file (413) surfaced as a raw tus error before, while
the ordinary upload path has always explained both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
Nothing in the dashboard wrote a new version of an existing object. You
could upload a file beside it under a different name, or delete it and
upload again — which on a versioned bucket leaves a delete marker in the
history rather than a clean succession.
Both the row menu and the file preview panel now offer it, reading
"Upload new version" on a versioned bucket and "Replace file" elsewhere,
since that is what each one does.
- `pickFile` creates and clicks the input synchronously, keeping the
picker inside the user gesture that opened it
- `replaceFile` writes to the object's existing path with `x-upsert`,
which is what produces the new version. It skips `findPreviousUploads`,
so an interrupted upload to the same path can't be resumed over this
one, and invalidates that object's version history on success
- `getStorageItemPath` gives a row's full bucket path one definition;
`ConfirmPurgeModal` was computing the same thing inline
Uploads have no platform endpoint, so this goes over the same resumable
transport as the existing upload path rather than through `data/`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
The pane said no preview was available and the version rows drew a generic
icon, so an archived file was unreadable before restoring it. Every
preview names its version id: the object's own path resolves to the delete
marker that archived it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
The bucket list had its own plain Enabled / Suspended badges, so the two
surfaces could drift. Both now render BucketVersioningPill, which carries
the icon and the tooltip; the list drops the "Versioning" prefix because
its column header already supplies it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
The badge now sits left of Policies rather than in the breadcrumb, spells
out the state it reports, and leaves the tooltip to say what that state
means instead of repeating the label. Enabled reads as a neutral fact, so
it uses the default badge rather than success.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
Restoring invalidated the archived list, so the archived row went away, but
the file did not come back: the live listing is the explorer's own state
rather than a React Query cache, so nothing refetched it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
The bucket page named the bucket and flagged it as Public, but said
nothing about versioning — you had to open bucket settings to find out
whether writes were keeping recoverable copies.
`BucketVersioningPill` sits beside the bucket name, brand for enabled and
warning for suspended, with the distinction in its tooltip. A bucket that
has never been versioned renders nothing: that is most buckets, and a
badge on all of them says nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
The archived mutations take the object's full bucket-relative path, which
is what the delete endpoint addresses. `ArchivedObject` already carries it.
The buckets list's Versioning column needs no change: it reads
`getBucketVersioningState`, which now reports the bucket's real status
instead of `disabled` for everything.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
Clicking an archived row now opens a preview pane for it: where the file used to
live, when it was archived, its retained versions, and the actions to bring it
back or delete it for good. This is what makes the Archive action in the file
preview panel a round trip rather than a one-way door.
- `ArchivedFilePreviewPane` — the pane, replacing `PreviewPane` while an archived
row is selected
- `ArchivedVersionRestoreWidget` — takes over the top of the pane when a specific
retained version is selected
- `ArchivedVersionRow` — one version, with its restore / delete actions
- `StorageExplorer` — the pane swap
Plus the tail of the feature, all behind the feature preview:
- The buckets list gains a Versioning column. The prototype hardcoded the column
total in three places; a `useVersioningColumnSpan` hook now supplies the delta
so the several `colSpan`s can't drift. Note the table was restructured on
master into a shared standard/virtualized shape since the prototype, so this is
a fresh implementation rather than a port.
- `DeleteBucketModal` and `EmptyBucketModal` say what destruction takes with it
once a bucket is retaining archived files and versions.
Fixes carried over rather than ported:
The prototype's version rows were an `<li role="button" tabIndex={0}>` with a
dropdown trigger nested inside — interactive controls inside an interactive
element. They're a real `<button>` beside the menu now, so Enter and Space work
and the menu isn't swallowed.
The prototype routed permanent version deletes through two separate mutations
depending on whether the version was current at archive time; that's one
mutation with a `wasCurrentAtArchive` variable, so the API shape stays a
one-file change.
The prototype duplicated the same confirmation copy, ternary-inside-ternary in
JSX, across two files. There's one consumer now and the copy is a flat helper.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
An archived folder had no menu, so an archived empty folder could only be
reached through the placeholder file the listing now hides. Restore and
Delete permanently apply to every archived object under the prefix, which
is all a folder is. Live folders on a versioned bucket gain the same pair,
with Delete renamed to Archive to match what it does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
An archived row has no checkbox to swap in, so the archive icon takes the
leading slot outright and the trailing badge goes away. That drops the
badge's solid background, which only existed to keep the icon readable
with the file name running under it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
Archived rows dropped the row menu entirely, leaving the badge as their
only control. They get it back with the two actions an archived file can
take: restore, and delete permanently. Downloading, copying a URL,
renaming and moving have no live object to act on.
The menu markup moves into `RowActionsMenu` so both kinds of row render the
same control rather than a copy of it, and the context menu opens the same
options for an archived file. A synthesized folder still has none, since it
is not a real object.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
Archiving a file refetched the folder listing, so the row vanished, but
left the archived list cached — so it never came back as an archived row
until a manual refresh.
The archived overlay is part of what the explorer shows, so invalidating it
belongs in `refetchAllOpenedFolders` rather than at each call site. That
covers deleting, purging, uploading and the refresh button in one place.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
An archived row has no checkbox, but it still carried the rule that hides
the row icon on hover to make room for one — so hovering left the slot
empty. The rule now applies only where a checkbox actually swaps in.
The Archived label becomes the archive icon with the word as a tooltip, on
a solid background: the row's stripes and a long file name both run
underneath it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
Adds "Show archived" to the file explorer's view options. With it on, archived
files appear in place in the listing — dimmed and striped, with an Archived
badge — instead of being invisible. Folders that exist only because something
archived sits inside them are synthesized too, so a nested archived file is
reachable by drilling in.
The badge is the only way to reach the file for now; the archived file preview
pane it opens arrives in the next PR.
- `useStoragePreference` — `showArchivedInline`, stored per project
- `ArchivedFilesContext` — owns the archived list and which object is selected
- `FileExplorer` — merges archived rows into each column and re-sorts, since the
server-side sort never sees them
- `FileExplorerRow` — dimmed striped rendering, no checkbox, no context menu
- `FileExplorerHeader` — the toggle, rendered only behind the feature preview
- `StorageExplorer` — mounts the provider
Three prototype problems fixed rather than carried over:
The prototype fetched the archived list with no `enabled` gate, so every bucket
page fetched it whether or not the feature was on. It also mounted a second
subscription **per archived row** just to resolve that row's id on click. Both
are now one query in the provider, gated on the flag, the toggle and the bucket.
The prototype's archived marker was an `aria-hidden` span, and archived rows drop
both the checkbox and the context menu — so they had nothing keyboard-reachable
and nothing a screen reader could announce. The badge is now a real button that
opens the file, with folders keeping a plain label since they have no object to
open.
The stripe pattern was a hardcoded `rgba(120,120,120,0.05)`; it now derives from
`currentColor` so it tracks the row's text color across themes.
The overlay is deliberately not gated on the bucket's versioning state: a
never-versioned bucket simply has nothing archived, and a suspended one can still
be retaining files, so the query result is the honest answer either way.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The live listing filters it out of every folder, but the archived overlay
was rendering it as a file. It still rolls its folder up, so an archived
empty folder stays visible rather than exposing the placeholder.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
An archived object is one whose top row is a delete marker: gone from the
live listing, versions still retained. `list-v2` reports those rows once
`deleteMarkers` and `noncurrentVersions` are included, so the archived
list is a grouping of that listing rather than a dedicated endpoint.
The flat, undelimited listing is deliberate — the inline overlay
synthesizes folders from full paths, so it needs the whole bucket, not one
level at a time. Paging is bounded.
- Restoring deletes the delete marker, which promotes the version beneath
it back to current; nothing is copied
- Purging deletes every retained version and the marker, since leaving the
marker would keep the object listed as archived with nothing under it
- Deleting one retained version addresses it by `{ path, versionId }`
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
On a versioned bucket a delete is a soft delete, and the file preview panel
already calls that action Archive and promises the versions stay recoverable.
Nothing in the dashboard lets a user see or restore an archived file yet. This is
the data layer for that, with no UI: query and mutation shapes written to the
studio conventions, endpoints stubbed, returning empty.
- `archived-objects-query.ts` — `ArchivedObject` / `ArchivedObjectVersion` types
and `archivedObjectsQueryOptions`
- `archived-object-restore-mutation.ts` — bring an archived object back
- `archived-object-purge-mutation.ts` — delete it and every version, permanently
- `archived-object-version-delete-mutation.ts` — remove one retained version
- `archivedOverlay.utils.ts` — synthesizes the explorer rows for one folder from
the archived list
- `archivedVersions.utils.ts` — an archived object's history as one flat list
Two prototype problems fixed rather than carried over:
The prototype identified the "was current when archived" row by the sentinel
`versionId === objectId`, which was load-bearing across three files and would
break the moment real version ids arrived. `ArchivedObject` now carries a
`currentVersion` record, so merging invents no fields and the distinction is an
explicit `wasCurrentAtArchive` flag.
The prototype's object had both `name` and `originalPath`, inconsistently — the
path normalization existed mostly to strip a duplicated leaf folder that
inconsistency produced. There is now a single `path`, and `getArchivedSegments`
is the only function that interprets it, so a different API shape is a one-place
change.
Also drops `deletedBy` and `expiresAt`, which the prototype never rendered.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The breakdown is the only place on the org usage page that tells a user how to
act on the noncurrent bytes they're being billed for, and the navigation path had
been trimmed out of it.
Also flags why the per-bucket rows aren't links: buckets are per-project and this
is an org-level page, so the endpoint needs to return a project ref before a row
can be linked — or even disambiguated, since two projects can both have an
`avatars` bucket.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Splits Storage Size into current versus noncurrent objects, and attributes the
noncurrent portion to the buckets responsible, so the cost of object versioning
sits with the metric it inflates.
The split is flag-aware. With the preview off, Storage Size renders exactly the
single real series it does today; `USAGE_CATEGORIES` takes an options argument
and only emits the two stacked segments when the preview is on.
Noncurrent is deliberately not split further into noncurrent versions and
soft-deleted files: the platform can't reliably tell the two apart after the
fact, and the user's lever for shrinking either is the same lifecycle policy.
Using S3's own terms also reads better than Studio-only words like "live".
- `storage-retention-usage-query.ts` — org-scoped (the prototype keyed this
org-level data under `['projects', undefined, …]`), real `queryOptions` shape
with the endpoint stubbed
- `StorageRetention.constants.ts` — one definition of the two segments, used by
both the chart attributes and the breakdown table, so labels, colors and key
casing cannot drift
- `StorageRetention.utils.ts` + tests — maps retention days onto chart points
- `StorageRetentionBreakdown.tsx` — the segment table, retained-data warning,
and per-bucket list, rendered through the existing `additionalInfo` hook
- `Usage.colors.ts` — `COLOR_MAP` and `AttributeColor` moved out of
`Usage.constants` so the breakdown can read a color token without a cycle
back through the module that renders it
Known limitation: with the preview on, the stacked chart and the breakdown read
zeros until the retention endpoint exists. No mock data is shipped.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The redesign renamed the panel's "Get URL" button to "Copy URL" while the row
context menu kept "Get URL", leaving one action with two names, and gave the
panel's file name a `title` — the attribute the explorer rows use as their
handle, so `getByTitle` matched two elements once a preview was open.
Also snap five off-token sizes to the scale the ratchet enforces, and point the
E2E delete helper at the confirmation's real label now that it says what it
does instead of ConfirmationModal's "Submit" default.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
Leads with what happened and that it is permanent, then bounds the wait.
The previous wording blamed a missing schedule; the real reason there is no
exact timestamp is that the cleanup spans several systems.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
"Expiring now" claimed a moment that does not exist: cleanup is a periodic
pass, so a version that has met the policy stays listed until it runs. The
tooltip says why, within a max width.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
Restoring a noncurrent version invalidated the version list but left the
explorer row showing the old size, type and modified date, since the live
listing is the explorer's own state rather than a React Query cache.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
The version history rendered a generic mime-type icon per row, and the
compare widget put that same icon on both sides — so every entry for a
file looked identical and the comparison showed nothing to compare.
The sign and public-url endpoints already accept `options.versionId`;
nothing asked for it. `useFetchFileUrlQuery` now takes a `versionId` and
keys on it, and the preview rendering moves out of `PreviewPane` into a
`FilePreview` component both the pane and the compare widget use, so a
version preview cannot silently fall back to the current bytes.
Image rows in the history list render their own thumbnail under 5MB;
larger files and other mime types keep the icon rather than pull a whole
object down for a 28px box.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
The file preview panel already splits the two on a versioned bucket, but
the explorer's own row menu still offered a single "Delete" — which
archives there, without saying so.
The row menu now reads "Archive" on a versioned bucket and gains a
"Delete permanently" entry beside it.
`ConfirmPurgeModal` is mounted once by the explorer and driven by
`itemToPurge` on the store, the same shape the row delete already uses.
The preview panel routes its own permanent delete through it too, so the
confirmation copy exists in one place rather than two.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
The preview panel now addresses versions by the object's full path, which
is what the list, move and delete endpoints take, rather than by the leaf
name the explorer renders. `VersionHistory` keeps `objectName` for copy
and takes `path` separately.
Expiry countdowns read the bucket's stored lifecycle policy instead of an
empty placeholder, so a row's fate reflects the policy that governs it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
The empty state read "Overwriting this file will retain a recoverable copy here"
regardless of whether the bucket was actually versioned. Since
`getBucketVersioningState` reports `disabled` for every bucket until the API
exposes the field, that promise was showing on every file in every bucket.
Splits it: `disabled` now says versioning is off and points at the bucket
settings; `enabled`/`suspended` keep the original copy.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>