Skip to content

Add DotNetBoost.Auditing, and rebuild the dashboard on shadcn-vue - #1

Merged
sadeqabuhattem merged 45 commits into
mainfrom
shadcn-vue-dashboard
Oct 28, 2026
Merged

sadeqabuhattem merged 45 commits into
mainfrom
shadcn-vue-dashboard

Conversation

@sadeqabuhattem

Copy link
Copy Markdown
Member

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 the auditing settings 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-01 and sidebar blocks: 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:

  • Invisible toasts, fixed and now guarded. The rebuild dropped 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> to rtl. CI's dashboard job installs Chromium and runs it.
  • Screenshots retaken of the new dashboard, from a freshly seeded database, the way docs/screenshots/README.md describes.
  • The README is roughly halved and its counts brought up to four libraries.

Test plan

  • CI: Build & test (dotnet test, Testcontainers PostgreSQL, no skips)
  • CI: Dashboard (lint, typecheck, Vitest, browser smoke test)
  • Smoke test fails with vue-sonner/style.css removed, passes with it back, locally and in a Linux Node 22 container
  • Look over the screenshots in the README

🤖 Generated with Claude Code

sadeqabuhattem and others added 30 commits October 24, 2026 01:10
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>
sadeqabuhattem and others added 15 commits October 27, 2026 22:28
…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>
@sadeqabuhattem
sadeqabuhattem merged commit 1838c73 into main Oct 28, 2026
2 checks passed
@sadeqabuhattem
sadeqabuhattem deleted the shadcn-vue-dashboard branch October 28, 2026 23:25
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.

1 participant