Repository navigation
Add DotNetBoost.Auditing, and rebuild the dashboard on shadcn-vue - #1
Merged
Merged
Conversation
Adds the fourth library from nuget.org — Core, EntityFrameworkCore and API at 1.0.0-preview.1 — and composes it the way the other three are: one Apply* line in AppDbContext, one block in AddInfrastructure, its own endpoints mapped in Program.cs, and no edit to any of the three already installed. What it cost is the interesting part, and it is written down rather than worked around. The library's change-tracker capture cannot be attached to this application's DbContext: DotNetBoost.MultiLingual's Translation has a composite key that an audit entry cannot address, and the library's three remedies — a surrogate key, [AuditIgnore], recording by hand — all belong to whoever owns the entity, which for the first two is not this application. So the six catalogue write slices record their own changes through IAuditManager and AuditDiff. Updates and deletes stay in one SaveChanges with the change they describe, because the EF Core store tracks an entry rather than writing it; a creation cannot, and Composition/CreationTrail.cs is that exception and its price. The other libraries' own tables are therefore not in the trail at all, and GET /api/audit/entities says so. [AuditIgnore] sits beside [MultiLingual] on Category.Name and Description: a trail has no language, so a property whose value depends on one cannot be stated in it. Said with the engine's own attribute so every way of recording honours it and the discovery endpoint reports it. The endpoints keep the library's default and require an authenticated caller, which is why the sample grew Api/Authentication/SampleActorAuthentication.cs — a header-trusting stand-in that verifies nothing, says so at length, and gives UseHttpContextActor a real actor to record. Erasure and prune are not mapped. Tests: AuditRuleTests asserts every translated property is an ignored one, read through each library's own metadata; AuditTrailTests drives the four new routes in both language modes, including that a refused write records nothing — the entry rolls back with the save. ZeroCouplingTests covers four assemblies and KeyFormatAgreementTests three addressing versions. 139 tests, and every count the documents state is updated with them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`dotnet build DotNetBoostSample.slnx` has been failing at restore, on main and on
every branch cut from it:
aspire/DotNetBoostSample.AppHost.csproj : error NU1902: Warning As Error:
Package 'MessagePack' 2.5.302 has a known moderate severity vulnerability
Nothing in this repository changed to cause that. GHSA-qhrr-8q5h-9q3h
(CVE-2026-92707, moderate) was published after the last commit landed green, and
NuGet's audit reads the advisory database at restore time rather than at commit
time — so the same source that built yesterday stops building today, and the
first failure arrives on a branch that did not touch a package.
MessagePack is not referenced here and should not be. It reached the AppHost
transitively, Aspire.Hosting.AppHost 13.5.3 -> StreamJsonRpc 2.25.29 ->
MessagePack 2.5.302, and the advisory is fixed in 2.5.303. Aspire 13.6.2 was the
first release to depend on MessagePack 2.5.307 on its own account; that
dependency sits one hop from the AppHost where StreamJsonRpc's sits two, so it
is the one resolution picks, and the advisory is answered by a version number
this repository already states rather than by a direct PackageReference to a
library only Aspire calls. 13.6.4 is the current patch of that line.
So the three Aspire.Hosting.* pins move together — they are one product — and
the Aspire.AppHost.Sdk version in the AppHost csproj moves with them, since the
SDK generates the `Projects.*` handles the hosting packages consume and a split
between the two is not a combination anyone tests. The reason for the number is
written down beside it, including the 13.6.2 floor, so that a later tidy-up does
not quietly walk back into the advisory.
What this is not: no NuGetAuditMode change, no NoWarn, no edit to
TreatWarningsAsErrors, and the audit still runs for every project in the
solution. The fix is a fixed version, not a silenced warning.
AppHost.cs needed no changes; nothing it calls moved across the minor. The two
documents that named the old version — the multi-targeting caveat in
ARCHITECTURE-SLICES.md and the matching paragraph in the README — say 13.6.
Verified: the solution builds in Release with 0 errors, MessagePack resolves to
2.5.307, and the suite is 121 passed / 0 skipped with a real PostgreSQL. `aspire
run` brings up postgres, api and dashboard — /health answers Healthy,
/api/categories serves the seeded catalogue, and the dashboard proxies it.
The build is not warning-free, and this commit does not make it so: it emits six
`Source control information is not available` warnings from SourceLink. Those
are pre-existing — building main's solution with the audit gate relaxed purely
to measure gives the identical six — and they are MSBuild-task warnings with no
code, which is why TreatWarningsAsErrors, a compiler property, never made them
errors. They are left alone here rather than folded into an unrelated fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The fourth library was the only one with no page, and the README said so in as
many words. It is here now, and the sentence is gone.
Two things made it unlike the other three pages. The audit endpoints require an
authenticated caller — the library's own default, which this sample keeps — and
a browser sending no header cannot read a single entry. And every change made
through this dashboard was being recorded with no actor, so the trail answered
"what changed" and "unknown" to "who changed it": half a library, demonstrated.
So the dashboard asks who you are. "Acting as", at the foot of the sidebar, is a
name you type; useApi sends it as X-Sample-Actor on every call it makes, which
makes the trail readable and the catalogue's own writes attributable in one
stroke. Nothing is invented on anybody's behalf: acting as nobody is the
starting state, those changes are still recorded with no actor, and the page
renders a null actor as "unknown" and never as "system". The Nitro proxy is
unchanged — it forwards the request's headers as they arrive, as it always has
for Accept-Language — and now says why adding a name of its own was the wrong
answer: the dashboard knows nobody, so a forwarded name could only be a made-up
one, in the trail of every change anyone makes.
The page follows the media browser. Every filter — entity, key, actor, action,
date range — is a parameter on the query the API is asked, never a pass over the
rows already on screen, which for an append-only trail searches the wrong set.
The entity list is GET api/audit/entities' answer rather than a list written
into the client, and the page bound is the maxPageSize that same response
publishes. Paging is newer/older over the API's own nextCursor: no skip, which
the API refuses alongside a cursor anyway, and no total, which this read
deliberately never asks for. Picking an entity also draws what the MODEL says is
recorded for it, which is where [AuditIgnore] on Name and Description stops
looking like a trail that has quietly stopped working.
test/nuxt/useAuditTrail.spec.ts pins four things, three of which were wrong in
the first cut: a cursor outliving the filter that produced it (the API is then
handed a position in a trail nobody is reading); a filter typed mid-flight being
answered with the page already out, because the entry is registered
dedupe: 'defer' and every deliberate move has to refresh with 'cancel'; the
declared entities read under two keys; and — the one the page hangs off — the
actor header actually leaving the browser, since one that does not looks exactly
like a server that is down.
Two things found on the way. `take` in a page of the trail is how many entries
came back and not the page size asked for, so "is there an older page" cannot be
read off it; the comparison is against the size this page asked for, and it
matters because nextCursor is returned for every non-empty page, the last one
included. And a bare @ in a vue-i18n message is that library's linked-message
syntax: an email-shaped placeholder has to be written alice{'@'}example.com or
the bundle does not compile at all.
@nuxt/test-utils moves 4.3.3 → 4.3.4, inside the range package.json already
allowed. 4.3.3 against the Vite 8 that nuxt 4.6.1 brings in fails to load every
suite — "Failed to resolve import ./diagnostics/core.js" — so npm test was red
before any of this. Verified with npm ci under node:22-alpine, the npm the
runner has.
139 .NET tests still pass, DocumentedCountTests over the rewritten README among
them; lint, typecheck and the eight vitest tests pass; and the page was driven
against a live API in English and in Arabic, where a category created from the
catalogue page came back in the trail as dana@example.com with Slug and
CreatedAt moved and Name nowhere in sight.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Taken the way docs/screenshots/README.md says: a production build rather than `nuxt dev`, a freshly seeded database, 1400px wide captured at 2× and resized to 1800px. That file grew a section, because this one needs something the other four do not. A seeded database has no trail at all — DatabaseInitialiser writes the catalogue without going through the slices that record — so an empty page is what a faithful reading of the recipe produces. The six writes the image shows are now written down beside it: bob adds a product, somebody with no header corrects its SKU, carol deletes the lamp, alice renames a category's slug and reprices the desk, and dana rewrites a product in Arabic only — which records nothing at all, and is the one to keep if these are ever rewritten, because it is exactly what the rules card beside the entries explains. The image is taller than the others. The width is the fixed part of the recipe; the rules card and an opened entry do not fit in 760px, and settings.png was already twice the height of catalog.png. 139 tests still pass over the README it is embedded in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`npm test` really was red when the Audit page was written — every suite failed to load with "Failed to resolve import ./diagnostics/core.js". Moving @nuxt/test-utils 4.3.3 → 4.3.4 made it green, and the commit that did it drew the obvious conclusion: that 4.3.3 is broken against the Vite 8 nuxt 4.6.1 brings in. That conclusion was wrong, and the evidence turned up while moving the branch back into the main checkout: its node_modules held 4.3.3 against this branch's 4.3.4 lockfile, and all eight tests passed. Checked properly afterwards — the dashboard's own source, with package.json, package-lock.json and .nuxtrc taken back to d620b82, installed with `npm ci` — 4.3.3 passes all eight too. The original failure was a node_modules left over from before nuxt moved to 4.6.1, not the version of anything. Reinstalling was the fix; the bump only happened to reinstall. So it goes back, which also restores the lockfile a5b108a generated under the runner's own npm rather than the one npm 11.19 rewrote here — the three files are now byte for byte what that commit left. Lint, typecheck and the eight tests pass on it. Nothing in the Audit page depended on the newer version. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Takes the Aspire 13.5.3 → 13.6.4 move, which answers GHSA-qhrr-8q5h-9q3h on the MessagePack 2.5.302 the AppHost used to resolve transitively — the NU1902 that TreatWarningsAsErrors turned into a restore failure, and the reason the Audit page was built and tested against the test project and a hand-started API rather than through `aspire run`. No conflicts. The two branches touched the same three files and not the same lines: this one added the audit endpoints' packages and the dashboard's Audit page, that one moved the Aspire versions and the two sentences naming 13.5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3f96936 reverted @nuxt/test-utils 4.3.4 → 4.3.3 on the grounds that 4.3.3 was fine and a stale node_modules had been the real problem. It was not fine, and this puts the bump back. What both wrong accounts have in common is that every measurement of 4.3.3 was taken on a **cold vitest cache**. `node_modules/.vitest-cache` goes with the install, so the run straight after `npm install` or `npm ci` is always the first one — and that run passes. The second does not: 4.3.3 run 1: 8 passed run 2: FAILS run 3: FAILS 4.3.4 run 1: 8 passed run 2: 8 passed run 3: 8 passed Measured just now in two scratch trees with identical source, each installed with `npm ci` from its own lockfile, three runs apiece. The failure is the same one as before — every suite fails to load with "Failed to resolve import ./diagnostics/core.js" — and `vitest run --no-cache` does not avoid it, because that flag does not discard the cache that is already there. Deleting `node_modules/.vitest-cache` by hand makes exactly the next run pass. So the original diagnosis was right by accident and the correction was wrong on purpose. Both of them read "it passed after I reinstalled" as "the reinstall fixed it", when the reinstall had only emptied the cache. CI never saw any of this: `npm ci` on a fresh runner runs the suite once. Lint, typecheck and three consecutive `npm test` runs pass on 4.3.4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The dashboard asked who you were before the Audit page would load, and without a name every catalogue change was recorded against nobody. The API's stand-in scheme now signs a request that names nobody in as `sample.user@example.com`, so the dashboard needs no control for it. `X-Sample-Actor` is still honoured when it is sent, which is how the integration tests act as alice, bob and carol. The audit endpoints still require an authenticated caller, the library's default; the scheme simply always provides one. Removed from the dashboard: ActingAs.vue, its plugin and localStorage helper, `useActor`, the header `useApi` attached, and the 401 "needs a caller" alert with its translations. The test that pinned the anonymous 401 now pins the default user instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Audit page failed with a 500, `relation "AuditEntries" does not exist`. Aspire keeps Postgres on the `dotnetboost-sample-pgdata` volume, and a database created there before this branch added auditing never got the library's two tables: `EnsureCreated` does nothing to a database that already exists. The tests never saw it because each run starts from a fresh container. When the database already exists, the initialiser now creates the tables the model has and the database lacks, with their indexes, from EF's own create script. Existing tables and data are left alone. A column added to an existing table is still a migration this sample does not have. Verified against the old volume: both tables created, `/api/audit` answers 200, and all 139 tests pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ble collapsing Picking an entity on the Audit page squeezed the table down to nothing. The panel body is a scrolling flex column and the table's root scrolls on its own, so when the rules card appeared the table gave up its height and became a 0px scroll box. `shrink-0` keeps it at full height and lets the page scroll. The From and To boxes are now one button that opens a two-month range calendar, with an ✕ to clear it. The range is applied once both ends are picked, and the query still holds the same `yyyy-mm-dd` pair, so what the API is asked has not changed. `@internationalized/date` is now a direct dependency; it was already installed through Nuxt UI. A "Sort by" select sits before "Per page": newest first, oldest first, entity, action, actor. It reorders the page on screen only. The API serves the trail newest first with no parameter for another order, and the cursor walk depends on that order. Also fixes the Action filter's options, which looked up `audit.actions.Created` and friends where the keys are lowercase. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Newer and Older under the table read as two more filters, not as a pager. The page now shows "1–25 of 140" and numbered pages, the same UPagination the Media and Catalog pages use, shown once there is more than one page. That needs an offset and a count, so each read now sends `skip` and `includeTotal=true` in place of the `after` cursor. Changing the question still goes back to page 1, since an old page number can be past the end of a narrower filter. The library's own trade-offs come with it: every read counts the matches, a deep page reads past everything before it, and an entry recorded mid-paging shifts rows by one across a page boundary. Fine at this sample's size. The cursor test is now an offset one: jump to a page, read the total, reset on a filter change, and never send `after`. The intro text no longer says the pages are walked by cursor. Not tried against the running API, which was down. The library's docs and binary confirm `skip` and `includeTotal` are bound on `GET api/audit`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… preview.2 DotNetBoost.Auditing 1.0.0-preview.2 adds IgnoreEntity<T>(), the class-level [AuditIgnore] for a type the application cannot annotate, and no longer throws on an excluded entity with a multi-column key. That was the whole reason this sample could not attach the library's SaveChangesInterceptor: MultiLingual's Translation is keyed on four columns and is saved beside every translated entity. - Bump the three Auditing packages to 1.0.0-preview.2. - Chain .UseMultiLingual(sp).UseAuditing(sp) on AppDbContext and add .IgnoreEntity<Translation>() to AddAuditing(). - Give Category and Product HiLo keys (sequences CategoryIds and ProductIds), assigned at AddAsync, so a creation's audit entry goes in the caller's own save instead of a deferred second save nested inside MultiLingual's. DatabaseInitialiser creates the sequences on a database made before them, starting past the ids already there; the existing identity columns accept the explicit ids, so no migration is needed. - Remove the hand-written recording: CreationTrail.cs, the IAuditManager and AuditDiff calls in the Update/Delete handlers, and Application's reference to DotNetBoost.Auditing.Core. IAuditManager stays behind the audit endpoints and the dashboard's reads. - The trail now covers every entity the context saves except Translation — Setting, MediaFile, MediaAttachment and Language too — and the seed's first run, with no actor. - Tests: a category changed straight through AppDbContext is recorded with no handler involved; no Translation entry after a save in three languages; a creation's text survives in every language; a settings change is in the trail; and SchemaUpgradeTests boots on a database from before the HiLo keys. 148 tests, all passing. - README, ARCHITECTURE-SLICES.md, Program.cs and the dashboard comments describe the interceptor instead of the workaround. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DotNetBoost.Settings stores a group one row per property, so the audit interceptor recorded a settings save as a separate Setting entry per value that moved, each addressed by a row id. Setting is now ignored by the capture, and SettingGroupAuditObserver - an ISettingWriteObserver - records each save as one SettingGroup entry, keyed by the group's name, holding every value that changed. The entry is written after the settings save has committed, so it is the one entry in the trail that does not commit with its change. The README, Program.cs and ARCHITECTURE-SLICES.md say so and carry the new counts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The group entries were named SettingGroup because IgnoreEntity<Setting>() also suppresses a write addressed by the name "Setting" alone. They now use Setting, and the write carries the group's class (CatalogSettings, BrandingSettings) as its EntityType, found from the group's stored name the way DotNetBoost.Settings resolves it. Auditing checks an ignore by type when there is one, so the row entries stay out and the group's entry goes in; [AuditIgnore] and [AuditRedact] on a group's properties now apply too. The test now checks that every Setting entry is addressed by a group name rather than a row id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it in the host An `auditing` settings group (PruneEnabled, RetentionDays, PruneIntervalHours) holds the retention policy, and AuditRetentionService reads it every minute and calls PruneOlderThanAsync when a prune is due, so an edit applies without a restart. Off by default. Only retention, because every other AuditingOptions value is read once at Build(). The Audit page's rules card now reports the retention the service applies rather than "kept indefinitely". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ar blocks Nuxt UI is gone. The primitives are shadcn-vue's reka-vega style, added with its CLI into app/components/ui, with Tailwind, @nuxt/fonts and @nuxtjs/color-mode installed directly now that Nuxt UI no longer brings them. The blocks are only published in the new-york-v4 style, so the shell is adapted from dashboard-01 and sidebar-07 by hand: an inset sidebar that collapses to icons, a sticky page header, a ⌘K command palette, and the appearance menu where the blocks keep the user. The overview takes the block's section cards and area chart, drawn from the audit trail's entries per day. Every page and editor is moved over, with shared pieces for what Nuxt UI had and shadcn-vue does not: DashboardPage, EditorSheet, PagerControls, LanguagePills and SuggestInput, plus a drop zone for uploads. shadcn-vue/tailwind.css is imported by hand, because init leaves it out and without it no switch, menu or dialog shows its state. The mobile sidebar sheet, sidebar tooltips and switch thumb are patched to mirror in Arabic. .mcp.json adds shadcn-vue's MCP server, and clients/dashboard/CLAUDE.md the rules for UI work. The lockfile is regenerated in node:22-alpine, as CI installs it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…p Tajawal for Arabic Geist replaces Public Sans at the front of the stack, and Geist Mono now sets the identifiers - SKUs, slugs, language codes, GUIDs - that fell back to the system's monospace before. Geist has no Arabic, so the per-character fallback still hands every Arabic glyph to Tajawal and nothing about the Arabic changes. Both are self-hosted by @nuxt/fonts like Public Sans was. Geist keeps `fallbacks: []` for the same reason; Geist Mono is `global`, because `--font-mono` is inlined into Tailwind's utilities where @nuxt/fonts is not guaranteed to find it. components.json names `geist` so the CLI agrees. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ck uses Measured against the block's own preview, two things were off. Card titles were reka-vega's `leading-normal font-medium`; the block is drawn with new-york-v4's card, which sets them `leading-none font-semibold`, so CardTitle now does too, marked as a local change. The store name in the sidebar header was text-sm, and the block sets its name a size up, in text-base. Everything else measured the same: nav items, labels, card values and footers, tables, badges, buttons and selects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Catalog, Languages and Audit sit in a TableFrame now: a rounded border and a shaded header row that, from lg up, sticks under the page header and toolbar while the page scrolls. Below lg the tables keep scrolling sideways, and a header inside a scrolling box cannot stick to the page, so there it does not; TableFrame.vue says why the corners are overflow-clip rather than hidden. Counts and prices sit at the end of their cells in tabular figures, so a column of them lines up, and the two plural messages that drew "3 products" and "2 images" are gone with the badges. Minor columns drop out as the screen narrows, long audit keys truncate, and on a phone an audit action is its icon alone, so neither table scrolls sideways there. A row opens its editor, or on Audit unfolds, wherever it is clicked; the name or the chevron is still a real button for the keyboard, and the menu, switch and entity link stop the click. Status is an outline badge with a coloured icon, as the block draws it. Names and descriptions carry dir="auto", so an untranslated English row in Arabic truncates from its own end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…enu and a pager footer The table runs on TanStack Table v9 now, which owns two things: the order of the rows on screen and which columns are shown. Each header sorts its column through SortableHead, with aria-sort on the <th>; a column is always sorted, newest first by default, and ties keep the order the API sent. The sort is over the loaded page only, because the API serves newest first and takes no other order, so the footer says "within this page" whenever it is not the default, with a link back. The "Sort by" select and the AuditSort type are gone. A Columns menu at the end of the filter bar shows or hides Entity, Action, Actor and Changes, remembered per browser. The footer sits inside the frame, as the dashboard-01 block's does: the range on the start side, rows per page, "Page 1 of 2" and first/previous/next/last on the end side, mirrored in Arabic. It replaces the pager below the table and the filter bar's page size. CLAUDE.md notes that the installed TanStack Table is v9, not the v8 shadcn-vue's docs show. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ttings' sections nested The Overview stands alone at the top, as dashboard-01's first item does. Content holds Catalog then Media - the daily work, and the files that serve it - and Administration holds Languages, Settings and Audit: how the store is set up, then the record of what changed. Resources stay at the bottom. The "Dashboard" label is gone; "Content" and "Administration" are new in all three locales. Catalog and Settings unfold their sections in sidebar-08's way: the page's row stays a link and a separate chevron unfolds it, so collapsed to icons the icon still opens the page. A section unfolds whenever the page being read is in it. Settings' sections are the groups GET /api/settings reports, so a group added in C# appears here too. Catalog's tabs have addresses now - /catalog and /catalog?tab=products, kept with replace - so the sidebar can link to either and a reload keeps the tab. The command palette reads the same groups from useNavigation, with every section as an entry of its own under its page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ysical classes RTL missed shadcn-vue's RTL transform left a few left/right classes in its components. The one that showed was the menu button's room for its unfold chevron, pr-8: right is the start side in Arabic, so Catalog and Settings sat 24px further in than every other row. It is pe-8 now, and measured, every row's icon and text start where they do in English. The same sweep turned up the sidebar rail (on the outer edge in Arabic), the vertical tab marker, the alert dialog title's text-left, and the joined toggle group's corners and borders. That last one was broken in English too: reka-ui writes data-orientation only when orientation is given, so the group-data-horizontal rules never matched and every button in a joined group was square at both ends. ToggleGroup defaults orientation to horizontal now, marked as a local change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lumns menu and a footer Both tables run on TanStack Table v9 now, as Audit does, through the same SortableHead headers. Categories are the whole list, so a sort sorts all of them; products come a page at a time in the order they were added, so a sort reorders the page, and the footer says so - only when there is more than one page, on Audit too, since on a single page the caveat is about nothing. Unlike Audit, a third click returns to the API's own order. A Columns menu in the Catalog toolbar works on whichever tab is open, remembered per browser. The footer is TablePager, taken out of Audit so all three tables share it: the range or count on the start side, page controls on the end. Products show their page size and link to Settings › Catalog, which decides it, instead of offering a control that would disagree with the setting. The pager under the products table is gone; the shared labels moved to common. The path separator in that link and in the command palette is mirrored in Arabic. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e Products tab GET /api/products takes a search now, beside the categoryId it already had. It matches the name and description in the request's language, and the SKU in any: the handler filters on p.Name as if it were a column, and MultiLingual's query interceptor rewrites that read into the translated value with its fallbacks, so "مكتب" finds the writing desk under Accept-Language: ar and "desk" finds it in English. ToLower on both sides keeps Npgsql out of the Application layer. Four integration tests hold it to that in both modes: per-language matches, SKU, the category narrowing a search, and a total of zero for no match. The Products tab gets the filter bar: a debounced search box and an "All categories" dropdown with each category's count, both sent to the API rather than applied to the page on screen, both back to page 1 on change. A search that matches nothing has its own empty state with Clear filters, apart from "no products yet". The page count no longer drops to 0 on an empty result, which would have sent the pager to page 0. The README and ARCHITECTURE-SLICES.md quote the endpoint and state the test counts, and are brought up to date; DocumentedCountTests checks the counts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it has it It was in the Catalog toolbar beside the New button, away from the table it changes. On Products it now ends the filter bar; Categories has no filter bar, so it gets a row of its own above the table, aligned to the end - the right corner, or the left in Arabic. The toolbar keeps the tabs and the New button. The menu is ColumnsMenu now, shared by all three tables, at the default height of the filter inputs beside it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ne font Administration reads Languages, Audit, Settings now - the audit trail beside the languages, and the store's configuration last, as the page reached least often. The command palette follows, from the same list. Measured across all seven pages and the palette, the only face besides Geist was Geist Mono, chosen on purpose for identifiers: SKUs, slugs, language codes, the api/... badges, Audit's entity column, ids and the Overview's file paths. They are Geist now too - 29 font-mono classes in 16 files gone - and --font-mono points at the same stack, so a <code>, which Tailwind's base styles give the mono stack whatever its classes, and any font-mono a shadcn-vue component brings back read the same. Geist Mono is no longer downloaded. CLAUDE.md records the rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er Audit by action with tabs New category and New product leave the Catalog toolbar, which keeps only the tabs, and sit after the Columns button over the table's end corner: at the end of the Products filter bar, and in the row above Categories, which is there even with no categories so the New button always is. On Audit the action dropdown becomes tabs - All, Created, Updated, Deleted, each with its icon - on the start side of a bar directly over the table, with Columns on the end, as the dashboard-01 block lays one out. A tab is still a parameter on the API query and still goes back to page 1; the filter bar keeps entity, key, actor and dates. "All" is a new label in all three locales, and "Any action" is gone. Also takes out a blank line the font change left inside an input tag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tab icons From md up the entity, key, actor and dates controls share one row equally, the width of the table under it, instead of two rows with the actor box taking whatever the others left. Shared rather than fixed widths, because at md the open sidebar leaves the panel some 480px and four fixed widths cannot fit there. Below md they stack full-width as before. The entity select's value starts after its icon, as the inputs' placeholders do, now that the trigger is wide. The action tabs' icons are text-muted-foreground, the colour of the icons in the inputs above them; the green, accent and red stay on the table's action badges. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sidebar's Search entry takes the shape of shadcn-vue's SearchForm: an input-shaped box with the magnifier inside it and ⌘K at the end. It still opens the command palette rather than taking text, so it is a button wearing SidebarInput's classes, not an input that would drop what was typed. start-2 / ps-8 where the block has left-2 / pl-8, so the magnifier is on the reading side in Arabic; collapsed to icons the field gives way to the magnifier alone, with its tooltip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd a full-width filter row A Grid/List switch over the files, remembered per browser and shared by the page and the product picker. The list is a table like the others - framed, sticky header, Columns menu, TablePager footer - with a row per file: thumbnail and name, type, category, size, upload time, and a menu of Details, Open, Download and Delete, or the Attach button in the picker. A row opens the file's details, as a card does. Its sort is the API's: GET api/media takes sortBy and descending, so the File, Size and Uploaded headers write the query (TanStack's manualSorting) and the whole library is reordered, not the page on screen. The headers and the filter bar's sort select are one state. Kind and category do not sort, because the API does not. The filters get a row of their own, every control sharing its width from md up as on Audit, so nothing drops to a line by itself when the list's Columns button appears; refresh, the switch, Columns and Upload move to a bar over the files with the endpoint and the count. Both views gain a page size - rows per page in the list's footer, files per page under the grid - offering the starting size and its doubles (12/24/48/96, 9/18/36/72 in the picker) so the grid's rows stay full, and going back to page 1 on change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sortable headers A bar over the table, as on Audit: All, Active and Inactive tabs with their counts on the start side, and the Columns menu then Add language on the end, as the Catalog bars end. Add language leaves the page header, which keeps refresh and the language selector. The tabs narrow the rows on screen rather than asking the API, the one place in the dashboard a filter does, and the comment says why it is honest here: GET api/languages answers with the whole list, unpaged, so nothing can be left out, and the counts come from the same list. A tab with nothing on it says so. The table is on TanStack Table like the others: Code and Name sort over the whole list, a third click back to the API's order, and Direction, Role and Active can be hidden, remembered per browser. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ab strip over the page The groups leave the toolbar for a column beside the form, as in shadcn-vue's own settings example: a list can grow without running off the end of a row. Each group is its own card - a bordered, lightly shadowed link with its icon in a square - and the open one is tinted with the accent, with a bar down its start edge. The column sits at the top of the start side, on the right in Arabic, sticks under the page header from lg up while a long form scrolls, and goes above the form on a phone. Each group is still its own address. API reference moves to the page header, where it is about every group at once rather than one of them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The intro read "Three libraries" and named Settings, Media and MultiLingual; it names DotNetBoost.Auditing too now. The composition card's "three independent Apply* calls" was stale in the same way - AppDbContext has ApplySettings, ApplyMedia, ApplyMultiLingual and ApplyAuditing - and says four. All three locales. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Choosing a file showed its name beside a generic type icon, so an image's box stayed empty. The dialog draws the picture now, whole on a muted ground, above the name and size - from an object URL onto the file in the browser's memory, so nothing is sent to draw it. The URL is released when the file changes, is cleared, or the dialog closes. isImage decides, as it does for the library's thumbnails, so an SVG, a PDF or a video keeps its icon. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r libraries and nine files Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…back to English with an RTL shell Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gs.cs's comments, and keep the reasons Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e catalogue slices' comments, and recount their sizes in the README Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-in, the Settings page and Domain's packages Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nces its search, and keeps its page in range Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vue-sonner 2 no longer injects its own styles. Since the rebuild on shadcn-vue every toast was sent and drawn into an unstyled list that nobody could see. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The observer recorded on the request's AppDbContext, so the entry stayed tracked there after its save, and a failed save would have left it pending for whatever saved next. It now records and saves through a fresh scope's context, and passes the write's actor explicitly. The entry still commits after the settings change rather than with it; that needs a pre-commit hook in DotNetBoost.Settings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The move to shadcn-vue dropped vue-sonner's stylesheet and every toast was
raised into an unpositioned list nobody could see, while lint, the typecheck
and the composable tests all passed — none of them has a viewport.
`npm run test:e2e` is Playwright, through @nuxt/test-utils/playwright, against
the dev server, with every `/api/**` call answered from test/e2e/api.ts: no API,
no database. It checks that each of the nine pages draws its title with no
console error and no Vue warning, that a refused language PUT puts its toast
wholly inside the viewport, that an Audit row's tooltip opens on hover, and that
Arabic turns `<html dir>` to `rtl`. An unstubbed API call fails the test rather
than rendering an error state.
Taking 'vue-sonner/style.css' out of nuxt.config.ts fails the toast test
("viewport ratio 0"); with it back, all 12 pass from a cold Vite cache on macOS
and in node:22-bookworm with CI=true.
The dev server rather than a build, because Vue only warns in development.
Starting cold, Vite discovered nine dependencies late and reloaded mid-page
with `504 (Outdated Optimize Dep)`, so they are pre-bundled in
`vite.optimizeDeps.include` — the list `nuxt dev` itself prints. The test files
are typechecked through `typescript.nodeTsConfig`, and named *.e2e.ts so
Vitest's `test/**/*.spec.ts` never picks them up. CI installs Chromium and runs
it after `npm test`, uploading traces on failure. The lockfile still installs
under the runner's npm 10.9.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
All six were still of the Nuxt UI dashboard. Retaken the way docs/screenshots/README.md describes: a fresh PostgreSQL seeded by DatabaseInitialiser, the real API, a production build, generated placeholder imagery with alt text in en/ar/fr, then the six audit writes before the Audit page. 1400px at 2×, resized to 1800px, light theme. The README there was wrong in two places this run showed up. Seeding is audited now — the trail opens with a Created entry per seeded row and no actor — so "a freshly seeded database has none" is gone. And the order is written down: the writes delete the lamp, so the catalogue has to be photographed before them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two pieces of work, in the order they landed on the branch:
DotNetBoost.Auditing, the fourth library. Installed beside Settings, Media and MultiLingual with no reference to any of them. The trail is recorded by Auditing's interceptor on the one
DbContext(seeding included), a settings save is one entry for the group, retention comes from theauditingsettings group and is pruned in the host, and every request is signed in as one sample user. The dashboard gets an Audit page: filters, action tabs, sortable columns, a numbered pager and the rules card for the chosen entity.The dashboard, rebuilt on shadcn-vue. From Nuxt UI onto shadcn-vue after its
dashboard-01andsidebarblocks: a grouped sidebar with nested sections, TanStack Table v9 tables with sortable headers and Columns menus, a list view in the media library, settings groups as cards, Geist with Tajawal for Arabic, and RTL fixed where physical classes had slipped in.Along the way:
vue-sonner/style.css, so every toast was sent but never seen. It is loaded again, and a new browser smoke test (npm run test:e2e, Playwright against the dev server with every/api/**call stubbed) fails if it goes missing. That test also checks each page renders its title with no console error or Vue warning, a tooltip opens, and Arabic flips<html dir>tortl. CI's dashboard job installs Chromium and runs it.docs/screenshots/README.mddescribes.Test plan
Build & test(dotnet test, Testcontainers PostgreSQL, no skips)Dashboard(lint, typecheck, Vitest, browser smoke test)vue-sonner/style.cssremoved, passes with it back, locally and in a Linux Node 22 container🤖 Generated with Claude Code