Skip to content

feat(auth-ui): asset editor - #153

Merged
CptSchnitz merged 17 commits into
masterfrom
feat/auth-ui-asset-editor
Sep 17, 2026
Merged

CptSchnitz merged 17 commits into
masterfrom
feat/auth-ui-asset-editor

Conversation

@CptSchnitz

@CptSchnitz CptSchnitz commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Assets were the only part of the authorization system with no user interface. Answering "what is deployed right now?" meant database access, and changing a policy meant hand-crafting an HTTP request with a base64 payload and a guessed version number.

This adds a fourth managed entity to auth-ui: browse every asset, open one into a full page, read it with syntax highlighting, edit it, and create new ones. Everything runs offline and no runtime dependency is added.

Commits, one per ticket

Commit Ticket
07690ea 01 — test harness for auth-ui, scoped to this feature
6163e3d 03 — asset content codec
d5c8f7a 04 — assets list
b864c6b 02 — Rego language support
d4fc688 05 — open an asset read-only in a full page
f2c43d5 06 — edit an asset and save it
32e0c60 07 — make lost work hard
c22bdda 08 — create an asset
1a464a0 09 — asset version dropdown and comparison
db3c86a code review findings folded in
16748c6 pr review findings folded in
5bf1804 second round of pr review findings folded in
c2ff05d an asset page's action button lands where the list page's is
a14d308 the way back is named, the promised version is gone, the sidebar marks child routes

Changed after review

The nine points from the review, in order.

The filters take the shape the other entity pages use — search box, a filter toggle carrying a count, the filters in a panel, the active ones as removable badges. Every part of that block is mounted before any filter is chosen, and the loading spinner moved inside the table, because the real cause of the list shifting was the full-page spinner: starting a new query unmounted the header and the filters mid-click. The wording that read as machine-generated is rewritten — "All targeted environments" is now "All environments", "Templates and not" is "Templates and regular assets".

An asset opens read-only. Editing is a mode with one way in — an Edit button — and two ways out: Save, which asks before it writes, and Cancel, which asks before it throws edits away. Metadata follows the editor in and out, so the page cannot be half-editable.

A create and a save both say so. Both land on a page that otherwise looks exactly as it did before, so both leave a notice naming the asset version they produced. The toast alone was too easy to miss behind a navigation.

Latest and in-use are now told apart. An environment builds its bundle from the highest asset version targeting it, so a version the latest one stopped targeting stays live for that environment — previously the page called it "not the latest, read-only" and said nothing about it still running. The version dropdown annotates each entry (3 (latest), 1 (in use: np)), the non-latest banner adds what the version is still in use for, and the asset's header carries an "In use" badge. All of it computed the way the bundler computes it. The list is unchanged: a row still opens the latest.

Third round

The way back is the path, not an arrow. An icon-only arrow aligned the buttons but said nothing about where it went. The title row now reads Assets / authz.rego, the first half a muted link. The heading itself is still just the asset's name, so it is still the page's heading rather than a breadcrumb, and the row keeps the list page's height.

A new asset no longer promises its version. "Will be saved as asset version 1" was a promise about something the api assigns and that is always 1 here. The create page says nothing about it; the notice on the page a create lands on states it once it is a fact.

The sidebar marks an entry from anywhere underneath it. It compared the path for exact equality, and assets is the first entry with routes below it, so creating or opening an asset left nothing in the sidebar marked. It now matches an entry's children too — no change for the other entries, which have none.

Two smaller ones. The environments hint holds its line whether or not there is a hint in it, so ticking a box no longer moves the controls beside it. And a new asset's version is stated beside the title rather than sitting in what looked like a fillable field.

Second round

The filter row follows the other pages' order exactly. The first attempt reserved space for the count badge, the clear button and the badge row so that turning a filter on moved nothing — but that pushed the filter toggle off the right edge, which is the more visible of the two problems. Each of them now appears only when there is something to show, as on every other page. The one stability fix that stays is the loading spinner, which belongs in the table: it was the reason a filter change tore out the header and the filters.

The full-page asset views were missing their padding. Every list page adds p-6 on top of the main element's own, and the asset, create and version pages added none. That was half the jump between the list and an asset; the other half was the way back sitting on a line of its own above the title, which started the title row — and the button in it — one line lower than on the list page. The way back is now an icon button at the head of the title row, so every asset page opens with the same 36px row the list pages do: Add asset and Create sit in the same place, as do Edit and Save. The version view kept its version dropdown in that row, making it taller than any other page's; it joins the stack underneath, where the asset page already had it.

The save confirmation was warning about the wrong thing. It claimed an asset keeps no content history. The asset version is the history; what a save actually changes is what each targeted environment builds its next bundle from. It now names them — "This writes asset version 4. np, prod build their next bundle from it, in place of asset version 3." — and says so plainly when an asset targets nothing. The read-only warning made the same wrong claim and is corrected with it.

Worth a reviewer's attention

The application moves to a data router. App.tsx used <BrowserRouter> with a single flat level of children. The unsaved-work guard needs useBlocker, which only a data router provides, so the tree moves to createBrowserRouter over route objects in src/routes.tsx — shared with the tests, so both mount the same thing. The routes themselves are unchanged. One consequence: a data router catches render errors itself, so the root route gains an errorElement; without it a page that throws would reach the router's own default screen rather than this application's.

Saving goes through the shared fetch client, not $api.useMutation. Telling a 409 from any other failure needs the response status, and the query wrapper only surfaces the error body. src/pages/assets/save.ts is the one place that reaches past it, and the one place the schema's read-only-yet-required createdAt is papered over.

Safety around an irreversible save. There is no content history and no delete, so: a diff before every save, a confirmation on navigating away with edits, a stale save reported as a conflict that keeps the working content and shows it against the refetched stored content, a read-only mode for content that is not valid text, and a size warning before the api's 1 MB body limit turns into an opaque server error.

Rego highlighting is lexical only, per ADR-0002 — no parsing, no diagnostics. Syntax errors keep surfacing downstream where the bundle is built.

Testing

auth-ui had no test runner; this adds the first one, confined by include patterns to this feature. Existing application code gets no tests here.

One seam: the global fetch, stubbed by the setup file before any page module loads, because the shared api client resolves its base url at module load behind a top-level await. Tests render real components through the real fetch client, query library and component tree. One forced seam: the editor cannot run under jsdom, so its React wrapper is aliased to a text area and a two-sided diff — highlighting and diff rendering are therefore outside component-test coverage, and the tokenizer is tested against the real library instead.

163 tests across 12 files. turbo run build, test and lint are green, and knip is clean.

Deliberately not done

Replication to other sites, deleting assets, Rego validation, server-side pagination, any API or schema change, and the existing JSON editor's dark-mode bug. API gaps found along the way — including a uri with no server-side traversal validation, and an asset version write path that overwrites rather than inserts — are recorded in the spec's Further Notes rather than worked around.

🤖 Generated with Claude Code

CptSchnitz and others added 10 commits September 10, 2026 16:19
auth-ui had no test runner. This adds vitest on jsdom with one seam at HTTP:
the global fetch is stubbed by the setup file, before any page module loads,
because the shared api client resolves its base url at module load behind a
top-level await on /config.json.

The editor library cannot run under jsdom, so its React wrapper is aliased to a
text area that forwards content and changes plus a two-sided diff component.
Highlighting and diff rendering are therefore outside component-test coverage;
the real library still loads in the tokenizer tests.

Include patterns confine the runner to tests/assets. Existing application code
gets no tests here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A pure module owning the conversion between the api's base64 representation of
an asset's content and the text an editor shows, so no page hand-rolls it.

Decoding reports whether the bytes were valid utf-8; content that is not opens
read-only later, because re-encoding a lossy decode would corrupt an asset that
cannot be restored. Encoding is chunked: the whole byte array spread into one
String.fromCharCode call blows the argument limit long before the api's 1 MB
body limit is reached. Encoded size is estimated without encoding, so a page can
warn on every keystroke.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An Assets entry appended to the sidebar after Domains, and a list answering
"what is currently deployed?" without database access: name, asset version,
type, targeted environments, the template flag and the uri, with an asset that
targets nothing marked rather than left blank.

Environment, type and template filters go to the server; the environment filter
is single-select because the server's compiles to an array-contains, so a
multi-select would read as "targets all of these". Name search, sort and
pagination run client-side, because GET /asset offers none of them. All of it
round-trips through the url.

The route tree moves to src/routes.tsx and the application to a data router.
Tests mount the same tree, and the asset editor's later unsaved-work guard needs
useBlocker, which only a data router provides. The existing routes are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Monaco ships no Rego, so this registers one: a Monarch tokenizer and a language
configuration written here, covering keywords including the current-generation
syntax, built-ins, literals, numbers, quoted and raw strings, comments,
operators and delimiters. Highlighting only, per ADR-0002 — syntax errors keep
surfacing downstream where the bundle is built.

A second id, rego-template, adds one rule for the handlebars interpolations
auth-bundler substitutes into a template asset, ahead of the operator and
delimiter rules so an opening interpolation is not read as two braces. The two
share every other rule, so they cannot drift apart.

Registration is idempotent, which hot reload and strict mode's double
invocation both need.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A row on the list opens /assets/:assetName — the application's first nested
route — where an asset's content is readable as highlighted source. A full page
rather than a dialog, per ADR-0001, and a linkable url.

The page reads the named-asset response as a list of asset version rows of
unknown length and shows the latest; a single-element response gets no special
case. Language resolves from the asset type, and for a data document from the
name's extension. Name, asset version and creation time are shown but never
offered for editing: the name is half the asset's identity and there is no
delete to undo a second asset.

The new editor component wraps the library at a fixed height filling its
container, so a save action will stay on screen on a long policy, and follows
the application theme by prop rather than by remounting. The json editor the
opa validator uses is left untouched. Content that is not valid text opens
read-only with an explanation, because re-encoding a lossy decode would corrupt
an asset that cannot be restored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Content, asset type, uri, targeted environments and the template flag become
editable; name, asset version and creation time stay read-only. The type is
deliberately not locked after creation — locking it would make a duplicate
asset the only route to a type change, with no delete to clean up — so changing
it switches the editor language at once and warns the content will not follow.

The uri accepts a leading slash, which the bundle build treats as relative, and
rejects parent-directory segments and backslashes before any request. An empty
set of targeted environments is permitted, matching the api.

Saving is irreversible, so a diff against the stored content is one click away
first. The request carries the asset version that was loaded as its concurrency
token and is built from a local type that omits creation time, which the shared
schema marks both required and read-only. Ctrl/Cmd+S saves and swallows the
browser default. On success both the collection and named-asset queries are
invalidated, so the list is not stale on return.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three failure edges of an irreversible save.

A 409 is reported as a conflict — a save refused because it declared a stale
asset version, with nothing merged. The working content is kept, the stored
content is refetched, and the two open in the diff so the author decides what
to re-apply. Telling a conflict from any other failure needs the response
status, which the query wrapper does not surface, so the mutation goes through
the shared fetch client directly.

Navigating away with unsaved edits raises a confirmation through the router's
blocker, and only when there are edits. There is no draft persistence, so the
guard is all there is between leaving and losing the work.

Content is measured against the api's 1 MB body limit as it is typed: a warning
as it approaches, and a refusal here rather than an opaque server error above.

Also fixes a save test whose stub did not keep what it was sent, which the new
guard exposed by blocking the navigation the test made afterwards.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A full page at /assets/new, like the single-asset route rather than a dialog,
per ADR-0001. The defaults leave only what is specific to the asset: version 1,
which the api requires for one that does not yet exist, type POLICY, uri /,
template off, no targeted environments. A name shorter than three characters
is rejected before any request. On success the author lands in the editor for
the asset just created.

A 409 here means the name is taken, and is worded as that — a different
failure with a different remedy from the stale asset version an edit hits.

The editable metadata moves into one component the two pages share, and
uri.ts becomes validation.ts now that it holds the name rule too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The editor header gains an asset version dropdown, sorted descending with
latest marked, reading the asset version rows the named-asset endpoint returns.
Which one is on screen is a url search param, so it stays linkable.

Selecting a non-latest asset version opens it read-only behind a banner and a
route back to latest — overwriting what is deployed with an older body is
exactly the mistake this guards — with a comparison against latest in the diff
view, older on the left.

Today the api leaves exactly one asset version row per name, so the dropdown
usually lists one entry. That is a probable backend bug, recorded in the spec's
Further Notes and deliberately not worked around: the page reads the response
as a list of unknown length, so it fills in with no change here if the write
path is corrected.

The unsaved-work guard now covers a search-only navigation too, since on this
page that is a switch of asset version away from the edit surface.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Spec gaps closed: the create page had neither the unsaved-work guard nor the
request-size warning, so a freshly typed policy was one sidebar click from
gone and a large one would have failed as an opaque server error. The size
alert is now one component both pages render, and the redirect after a create
moves into an effect so the guard sees the save land before the navigation.

Correctness: a save no longer raises the guard in the window between success
and the refetch; `?page=abc` no longer renders "Page NaN"; content that is not
base64 at all is flagged as not-valid-text instead of throwing out of atob and
taking the page down with it; and the root route gains an errorElement, since
a data router catches a render error itself and the react boundary around the
tree no longer sees it.

Standards: the save transport, the timestamp formatter and the asset-version
dropdown's derived props each had a duplicate or a redundant parameter; the
per-render base64 decode is memoised; the url's filter values are checked
against the enums before they travel to the server; and the environment labels
take the glossary's plural.

The save shortcut is also registered as an editor command, not only on the
wrapper, so it is bound where the spec says it is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@CptSchnitz CptSchnitz left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. the structure of the filters is different than the other pages, it should be the same.
  2. the english is not good - "all targeted environments", "templates and not" are not clear.
  3. in the new asset page, when you choose environment, the "No targeted environments" is gone and changes the layout. the control should stay at the same place.
  4. the asset version in the new asset page is not clear, something about the design.
  5. before saving changes there should be "are you sure" prompt.
  6. when creating a new asset or new version, its not clear that it worked, as you go to the asset page that looks the same, you dont get any indication.
  7. there should a button to enter edit mode in the asset page, and also a undo / cancel button. it should not always be with edit enabled.
  8. in the assets page, when choosing stuff in the filter, it changes the layout (might not matter after the filter changes.
  9. right now when if for example version 1 is np, and version 2 is prod, when filtering by np you cant see it, even when its still the working version for np.

CptSchnitz and others added 4 commits September 15, 2026 10:28
Nine points from the review of #153.

The assets list takes the filter shape the other entity pages use: a search
box, a filter toggle carrying a count, the filters themselves in a panel, and
the active ones as removable badges. Every part of it is mounted before any
filter is chosen, and the loading spinner moves inside the table, so choosing
a filter changes what the table holds and nothing about where the controls sit
— a full-page spinner was previously unmounting the whole block mid-click.

An asset now opens read-only. Editing is a mode with one way in and two ways
out: Save, which asks first, and Cancel, which asks before throwing edits
away. Nothing about an asset is irreversible-by-accident any more.

A create and a save both land on a page that used to look untouched, so both
now leave a notice naming the asset version they produced.

Latest and in-use came apart with no way to tell: an environment builds its
bundle from the highest asset version targeting it, so a version the latest
one stopped targeting stays live. The version dropdown and the non-latest
banner now say which of the two a version is, computed the way the bundler
computes it.

Also: the environments hint holds its line, so ticking a box does not move the
controls beside it; a new asset's version is stated beside the title rather
than sitting in a field that looks fillable; and the filter wording that read
as machine-generated is rewritten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The filter row takes the other pages' order exactly: the search box takes the
width, the filter toggle sits hard against the right edge, and the count badge,
the clear button and the active-filter badges each appear only once there is
something to show. Reserving their space kept the layout still but pushed the
toggle off the right edge, which is the more visible of the two.

The three full-page asset views carried no padding of their own while every
list page adds p-6 on top of the main element's, so moving between the list and
an asset jumped. They now match.

The save confirmation warned about losing history. The asset version is the
history; what a save actually changes is what each targeted environment builds
its next bundle from, so that is what it now says — by name, and saying so
plainly when an asset targets nothing at all. The read-only warning made the
same wrong claim and is corrected with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s is

The way back sat on a line of its own above the title, so the title row — and
the button in it — started one line lower than on the list page. Moving
between the assets list and an asset jumped by exactly that line, which the
padding fix did not touch.

The way back is now an icon button at the head of the title row, so every
asset page opens with the same 36px row the list pages do: Add asset and
Create sit in the same place, as do Edit and Save.

The version view kept its version dropdown in that row, which made the row
taller than any other page's. It joins the stack underneath, where the asset
page already has it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sidebar

The way back was a bare arrow, which said nothing about where it went. It is
now the path it stands for — "Assets / authz.rego", with the first half a
muted link — which names the destination, keeps the heading itself to the
asset's name, and still sits on the title's line so the action button holds the
list page's height.

"Will be saved as asset version 1" was stating a promise. The version is the
api's to assign and is always 1 for a new asset, so the create page no longer
mentions it; the notice on the page a create lands on states it once it is a
fact.

The sidebar marked an entry only on an exact path match. Assets is the first
entry with routes underneath it, so creating or opening an asset left nothing
marked at all. It now matches an entry's own children too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
return await saveAsset(body);
} catch (failure) {
// A conflict here is a name already taken — a different failure, with a different
// remedy, from the stale asset version a save on an existing asset hits.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A create silently overwrites an existing asset whose version is 1, rather than reporting a name collision.

This page always posts version: 1, and upsertAsset (apps/auth-manager/src/asset/models/assetManager.ts:63-79) only throws AssetVersionMismatchError when maxVersion !== asset.version. An asset already at version 1 takes the update branch: its content is replaced by whatever was just typed and stored as version 2. No 409 ever reaches here.

Every asset that has never been edited sits at version 1, so this is the ordinary case, not an edge one. There is no delete and no content history, so the overwritten content is gone. tests/assets/create-asset.spec.tsx:99 stubs the 409 and asserts the wording, which is why the suite is green over the hole — the server's unit tests (apps/auth-manager/tests/unit/asset/models/assetManager.spec.mts) cover "version is not 1" and "version doesn't match", not this one.

The spec's whole safety story is that destructive mistakes are made hard; this is the one path that destroys an asset with no confirmation at all. GET /asset/{assetName} is already wired for AssetPage and answers with an empty list for an unknown name — check it before posting and refuse when it returns rows. If that is out of scope, it at least belongs in the spec's Further Notes with the other API gaps, because the API change it implies is not optional.

Comment thread apps/auth-ui/src/App.tsx

// A data router rather than <BrowserRouter>: the asset editor's unsaved-work guard
// needs useBlocker, which only a data router provides.
const router = createBrowserRouter(appRoutes);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The move to a data router changes the meaning of what three existing pages already do to the URL. ClientsPage.tsx:63, ConnectionsPage.tsx:60 and DomainsPage.tsx:46 each call window.history.replaceState({}, '', url.toString()) on every filter change. Under <BrowserRouter> that was harmless. createBrowserRouter keeps {usr, key, idx} in history state and reads idx on every push — index = getIndex() + 1 — so after the wipe every entry pushed thereafter carries idx: NaN. I confirmed that in this repo with a throwaway spec: filter the clients list, click a sidebar link, history.state.idx is NaN.

useBlocker is the only consumer of that number, and it is this PR's unsaved-work guard. On a POP it computes delta = NaN, which passes its own delta != null check, and calls history.go(NaN); react-router's warning string names this exact scenario ("navigating outside the router via window.history.pushState"). I could not reproduce the browser-side symptom under jsdom — its history.go is not a faithful oracle — so please check it in a browser: filter the clients list, open an asset, edit it, press Back. Story 47 asks for no regression on the existing pages, and the guard is the one thing between an edit and losing it.

The change is one token in each of the three pages:

window.history.replaceState(window.history.state, '', url.toString());

or switch them to setSearchParams(..., { replace: true }) the way AssetsPage already does.

const sort = parseSort(searchParams.get('sort'));
const page = positiveInteger(searchParams.get('page'), 1);
const pageSize = positiveInteger(searchParams.get('pageSize'), 10);
const showFilters = searchParams.get('showFilters') === 'true';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

environment, type and template are all checked against their allowed values — "the url is external input" — but pageSize takes any positive integer. ?pageSize=999 slices correctly and leaves the page-size Select rendering nothing, since no SelectItem matches and there is no placeholder. Consider running it through oneOf(..., PAGE_SIZES).

import { describe, expect, it } from 'vitest';
import { decodeAssetContent } from '@/lib/asset-content';
import { appRoutes } from '../../src/routes';
import { MAX_ENCODED_BYTES } from '@/lib/asset-content';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fold into the import on line 4 — same module, two statements.

…tate alone

A create posts asset version 1, and the api's upsert refuses a post only when the
declared version is not the name's current maximum. An asset already at version 1 —
every asset that has never been edited — therefore took the update branch: its content
was replaced by whatever had just been typed. No 409 was ever emitted, so the page had
nothing to report. With no content history and no delete, that content was gone.

The create page now reads the named-asset endpoint and refuses a name that already has
an asset, before it posts anything. It is a check and not a lock, so the 409 handling
stays as the backstop and both paths word it the same way. The declared 404 the endpoint
does not emit today is read as a free name rather than as a failure to check. The api
gaining a create that is not an upsert is recorded in the spec's Further Notes.

The three existing list pages wrote the url with `window.history.replaceState({}, ...)`.
Under the plain router that was harmless; the data router this branch introduced keeps
its history index in that state, so wiping it left every entry pushed afterwards with an
index of NaN — the number the unsaved-work guard reads on a back navigation. They now
pass the state through. The new spec proves the three pages preserve it; the consequence
for the guard is browser behaviour and still wants a check in one.

A page size from the url is checked against the four the select offers, as the other
three url filters already were. `?pageSize=999` used to slice correctly and leave the
select rendering nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
</TableCell>
</TableRow>
) : (
assets.map((asset) => (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An asset name is not unique in the /asset collection response — every version of an asset is a separate row with the same name. Your own seed data has this today (olKmsNiH v1/v2/v3, skpsBeRC v1/v2, fuYkBNgE v1/v2), so this fires on a stock checkout, no special setup needed.

To see the visible symptom (not just the console warning)
Take a screenshot or just note the order of rows on /assets right after it first loads.
Type a character into the search box, then delete it (so the filter is back to "no filter" — same data as before).
Compare the row order to step 1 — it's different, even though it's the exact same 19 assets with no filter applied. That's React silently reusing/misplacing DOM nodes because it can't tell rows with the same key apart.

Image

export const UnsavedChangesDialog = ({ when }: { when: boolean }) => {
// Search included, not just the path: on the asset page the search is which asset
// version is being viewed, and switching it leaves the edit surface just the same.
const blocker = useBlocker(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] - kind of 😄
useBlocker only intercepts in-app (react-router) navigations. It does not intercept a hard reload, a tab close, or a same-page window.location.reload()

CptSchnitz and others added 2 commits September 16, 2026 16:09
Two versions of the same asset share a name, so keying table rows by
name alone let React confuse rows across re-renders. Unsaved-edit
guard also only covered in-app navigation, missing reload/tab close.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tent

The diff editor only ever showed the content change. A save that only
touched type, URI, environments, or the template flag looked like it
had no diff at all, so nothing was there to review before saving it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@CptSchnitz
CptSchnitz merged commit 54dbdee into master Sep 17, 2026
6 of 8 checks passed
@CptSchnitz
CptSchnitz deleted the feat/auth-ui-asset-editor branch September 17, 2026 11:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants