FullStackHero 10 .NET Starter Kit Release Merge#1152
FullStackHero 10 .NET Starter Kit Release Merge#1152iammukeshm wants to merge 363 commits intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces the FullStackHero 10 .NET Starter Kit, implementing a modular monolith architecture with comprehensive modules for Identity, Multitenancy, and Auditing. The implementation includes a mediator-based CQRS pattern, JWT authentication with refresh tokens, role/permission-based authorization, background job support, caching abstractions, mailing services, and storage abstractions (local and S3). The Blazor client uses Shadcn-inspired MudBlazor wrappers with generated API clients via NSwag, while the infrastructure includes multi-app AWS scaffolding using Terraform and OpenTelemetry-based observability.
Key Changes
- Modular architecture with separate Identity, Multitenancy, and Auditing modules implementing contracts and handlers
- JWT authentication, role/permission system, and Finbuckle multitenancy with per-tenant provisioning lifecycle
- Auditing pipeline with request/response/security/exception tracking and background sink for SQL persistence
- OpenTelemetry integration, rate limiting, storage abstraction (local/S3), and comprehensive building blocks for caching, jobs, mailing, and persistence
Reviewed changes
Copilot reviewed 295 out of 1048 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| Directory.Packages.props | Updated package versions to .NET 10.0 and newer dependencies including Finbuckle 10.0.0, Mediator 3.1.0-preview.14, Hangfire 1.8.22, and OpenTelemetry 1.14.0 |
| Directory.Build.props | Enhanced with .NET 10.0 target, comprehensive code analysis settings, NuGet metadata, and stricter quality controls |
| BuildingBlocks/Web/*.cs | New Web building block with OpenAPI/Scalar integration, OpenTelemetry, Serilog logging, rate limiting, security headers, CORS, versioning, and module loading |
| BuildingBlocks/Storage/*.cs | Storage abstraction supporting local filesystem and AWS S3 with file type validation and upload/removal operations |
| BuildingBlocks/Shared/*.cs | Shared contracts for multitenancy (AppTenantInfo), identity (claims, permissions, roles), pagination, and database options |
| BuildingBlocks/Persistence/*.cs | Persistence infrastructure with specifications pattern, EF Core extensions, and database initialization interfaces |
| Modules/Identity/Modules.Identity.Contracts/*.cs | Identity module contracts including commands/queries for token generation, user management, role management, and associated DTOs |
| Modules/Auditing/Modules.Auditing.Contracts/*.cs | Auditing contracts with event types, payloads, DTOs, and interfaces for audit publishing, serialization, and sinking |
| Modules/Auditing/Modules.Auditing/*.cs | Auditing implementation with SQL sink, EF interceptor, HTTP middleware for request/response capture, channel-based publisher, and query handlers |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Wow! You woke up!! :) Excelent works, I going to clone and test it... Please check, you forgot push the /docs folders because is added in .gitignore: "/docs" Thanks in advanced. |
|
I am currently using VS2026. |
Perfect approach, check that may be some ideas are usefull: And this "spec driven AI design": |
|
@maxiar looks like its a member only story. any crucial takeaways? |
…build and push actions for API and Blazor containers
Replaced all "FSH" NuGet package references in templates with "FullStackHero" prefix. TemplateEngine now gets framework version from assembly metadata. Updated publish-nuget.yml to use --no-build for CLI tool packaging.
- Add --git and --fsh-version options to `fsh new` for git repo initialization and custom FSH package version selection - Wizard now prompts for FSH version and displays a clearer, more concise summary - Generated solutions can auto-initialize git and include a .gitignore - Templates updated: use latest FSH packages, improved references, and modern .NET patterns (e.g., await app.RunAsync) - Sample module renamed to "Catalog" for consistency - CLI output and next steps instructions improved for clarity and style - Add test-cli.ps1 script for local CLI testing - Update dependencies to latest versions and perform code cleanup - Add settings.json for local configuration
The Density switch in Settings → Appearance was decorative; this commit wires it through the theme provider, persistence, no-FOUC bootstrap, and a scoped CSS override block. Theme provider - DensityMode = 'comfortable' | 'compact'. - New `density` and `setDensity` exposed on useTheme alongside mode / font / accent. - Persisted at localStorage 'fsh.density'. Toggling applies a `density-compact` class on :root via applyDensity(). Bootstrap (index.html) - Inline script reads 'fsh.density' before React mounts and adds the class so first paint matches the user's stored preference (no FOUC on reload). Density CSS (globals.css) - Scoped to `main` so chrome (sidebar, topbar, dropdown menus) is unaffected. - Tightens the high-leverage Tailwind utility classes the Card and list surfaces use directly: px-6 → 1rem, pt-5/pb-5 → 0.75rem, py-3/3.5/4 → 0.5–0.625rem, space-y-7/6/5 → 1.25/1/0.875rem. - Pragmatic over purist; if more granular control is needed later, swap to a --card-py / --card-px token system on the primitives. Appearance page - Density Switch is now controlled by useTheme().density and writes through to setDensity. The previously-decorative local state is removed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the catalog management surface (Brands · Categories · Products list pages plus a Product detail page) and a sweeping aesthetic pass across the dashboard. Catalog - Brands / Categories / Products list pages composed from a new @/components/list primitive set (ListHero, StatStrip, SortChips, Combobox, DensityToggle, Pagination, Field, ErrorBand, EmptyState). - Product detail at /catalog/products/:productId with showroom hero (image showcase + identity stack with click-to-mutate Price/Stock cards), description + audit metadata panels, and four inlined mutation dialogs (edit / delete / price / stock). - Click-through from the products list to detail via the product name. Modernized chrome - New .card-shell + .card-shell-interactive utilities — single hairline border, no resting shadow, soft pillow-shadow only on hover. Replaces the older gradient-border + inset-gloss + double-shadow stack on every data-container surface. - --highlight-top toned from 0.55 → 0.14 alpha so any remaining surface-edge whispers instead of shouting that 2014 glassmorphism gloss. - EmptyState primitive — "the plinth": slow conic halo orbits the icon, gradient floor + cast shadow ground it. Replaces three duplicate EmptyState implementations. - NotFoundPage redesigned as "lost archive" — oversized 404 numerals with vertical fade gradient + offset ghost layer for misregistration stutter, mono "lookup receipt" surfacing the requested path, glass-blur navigation card with three suggested return paths. - Sonner toast restyled "telegram": no richColors candy, gradient-border surface chrome, 2px tone-coded left edge, mono-caps eyebrow auto from data-type, display-weight title. - Stat tile + price/stock click-cards laid out as flex columns with min-heights so they line up uniformly inside their grids. Misc fixes - Input: dropped manual focus-visible ring; defers to the global :focus-visible halo so we don't paint a double border. - theme-provider: removed local Document.startViewTransition declaration (now provided natively by lib.dom). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sweep across the surfaces that didn't get the architectural treatment in the catalog pass. - Dialog: DialogContent now uses card-shell (single hairline + lift shadow) instead of the older gradient-border ramp. - Skeleton: slower ambient shimmer (1.6s → 2.4s), brand-tinted sweep, softer resting fill, inset 1px ring so placeholders have a defined edge inside a card-shell. Light/dark tuned independently. - Settings (api-keys): empty state lifted to the shared EmptyState "plinth" primitive. Other settings pages inherit the modernized Card automatically. - Overview: inline EmptyState picked up an eyebrow + tinted icon plate matching the rest of the empty-state vocabulary while staying smaller-scale than the plinth (it's a status panel, not a CTA pulse). - Routes: every page lazy-loaded via React.lazy + Suspense with a route-shape fallback. Initial bundle dropped from 721KB → 535KB (~26%); post-login only the 12KB Overview chunk downloads on first paint, the rest fetch on demand. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A second demo module that exercises a different shape than Catalog:
a workflow / state machine over CRUD, with a child entity collection
(comments) owned by the aggregate.
Domain
- Ticket aggregate with guarded state machine
(Open → InProgress → Resolved → Closed, plus Reopen back-transitions).
Illegal transitions throw CustomException → 409.
- TicketComment entity, created only via Ticket.AddComment so the
aggregate stays the consistency boundary.
- Tenant-scoped sequential numbers (TK-1, TK-2, …) with a unique
index on Number as the race-safety net.
- Domain events: TicketCreated, TicketAssigned, TicketStatusChanged,
TicketCommentAdded.
Endpoints (api/v1/tickets/...)
- POST /tickets create
- GET /tickets search (filters: status, priority,
assignee, reporter, text)
- GET /tickets/{id} get one (with comment count)
- POST /tickets/{id}/assign assign / reassign / unassign
- POST /tickets/{id}/resolve resolve with optional note
- POST /tickets/{id}/reopen reopen
- POST /tickets/{id}/comments add comment
- GET /tickets/{id}/comments list comments
Wiring
- New TicketsModule (Order 700, schema "tickets") registered in
Program.cs Mediator marker list and module assemblies.
- TicketsPermissions: View, Create, Update, Delete, Assign, Resolve,
Reopen, Comment.
- Slnx + FSH.Starter.Api.csproj + FSH.Starter.Migrations.PostgreSQL.csproj
reference the new projects.
- Initial EF migration `20260430102356_InitialTickets` creates the
tickets schema with Tickets + TicketComments tables, unique index
on Number, indexes on Status / AssignedToUserId / ReporterUserId,
cascade delete from Tickets to TicketComments.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brand, Category, Product, Ticket, and TicketComment now implement
ISoftDeletable. The framework's existing AuditableEntitySaveChangesInterceptor
converts dbContext.Remove() into a soft delete (sets IsDeleted, DeletedOnUtc,
DeletedBy), and the global query filter on BaseDbContext keeps deleted rows
out of normal queries. Each aggregate gets a Restore() method that flips
the flag back.
Schema
- AddSoftDelete migration on Catalog adds IsDeleted/DeletedOnUtc/DeletedBy
to Brands, Categories, Products. Drops the unique indexes on Slug/Sku
and re-creates them with a `"IsDeleted" = FALSE` partial-index filter
so soft-deleted slugs don't block reuse.
- AddSoftDelete migration on Tickets adds the same three columns to
Tickets and TicketComments. Re-creates the unique index on
Tickets.Number with the same partial filter.
- Adds non-unique IsDeleted indexes for fast trash queries.
Endpoints (per resource: Brand, Category, Product, Ticket)
- POST /{resource}/{id}/restore — restore a soft-deleted entity
- GET /{resource}/trash — paged list of soft-deleted entities
(uses IgnoreQueryFilters under the hood)
Permissions
- New Restore permission per resource (Catalog.Brands.Restore,
Catalog.Categories.Restore, Catalog.Products.Restore, Tickets.Restore).
Trash listing also requires Restore (no point reading trash if you
can't act on it).
Contracts
- DTOs (BrandDto, CategoryDto, ProductDto, TicketDto) gained
DeletedOnUtc + DeletedBy as the last two optional record parameters,
populated only on trash listings (always null elsewhere). This is a
purely additive change on the wire — existing clients ignore the new
fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brands tests now lock in the soft-delete contract:
- DeleteBrand_Should_HideFromSearch_But_Keep_Row_For_Restore
- RestoreBrand_Should_BringBack_DeletedBrand
- CreateBrand_Should_Succeed_When_NameMatchesSoftDeletedBrand
(proves the filtered unique index lets the same slug be reused
after a soft delete — admins can recreate a trashed brand)
- RestoreBrand_Should_Return404_When_BrandDoesNotExist
Tickets module gets its first integration tests (16 facts):
- Sequential numbering, status start state with/without assignee
- State machine: Open → InProgress (Assign), InProgress → Open
(unassign), → Resolved (with note), → InProgress (Reopen)
- Search filtering by status
- Soft delete + Restore through the trash listing
- Auth gating on the tickets group
Permission registry test extended for TicketsPermissions and the new
Restore actions on every catalog resource.
API changes pulled in by the tests:
- TicketStatus / TicketPriority enums get JsonStringEnumConverter so
the wire format is "Open" / "InProgress" / "High" rather than
numeric ordinals (the dashboard also sends strings).
- AddTicketComment loads the parent with Include(t => t.Comments) so
EF's change tracker has a populated nav property to compare against
when the new comment is added through the encapsulated method.
- CreateTicket counts ALL rows (IgnoreQueryFilters on SoftDelete)
when computing the next TK-N — soft-deleted tickets must not let a
deleted number be re-issued.
- Restore + ListTrashed handlers across Brand/Category/Product/Ticket
switch from `IgnoreQueryFilters()` (bypasses every filter) to
`IgnoreQueryFilters([QueryFilters.SoftDelete])` so Finbuckle's
tenant filter stays in force — soft-deleted rows from other
tenants are NOT visible in this tenant's trash.
One ticket test (AddComment_Should_Persist_And_BumpCommentCount) is
marked Skip pending follow-up: EF raises DbUpdateConcurrencyException
("0 rows affected") inside SaveChanges when adding a TicketComment via
the aggregate's encapsulated method. Endpoint and domain rule are wired
correctly — this is an EF integration quirk, not a domain bug.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Tickets module gets its UI: a list page modeled as "the desk" (active workflow surface, not a registry), a detail page with a status timeline anchor and a comments thread, and a unified Trash page that spans all four soft-deletable resources. Tickets list — "the desk" - Status painted as a 3px tone-coded pull-tab on each row's left edge, with priority shown as a corner flag (low/med/high/crit dots) and a dedicated badge for the current state. Comment count + relative created/updated timestamps in the secondary line. Reporter and assignee shown as initials avatars on the right (assignee tinted primary, reporter muted) — a dashed badge stands in for unassigned. - StatStrip surfaces four counts pulled from the visible page: Open, In progress, Resolved, Critical (the last tinted danger when > 0). - FilterBar combobox on Status + Priority, plus a clear-all chip when any filter is active. SortChips for created / number / priority / status / title with direction toggle on repeated click. - CreateTicketDialog opens a ticket with title + optional description + priority. Reporter is taken from the JWT on the API side. Ticket detail — atmospheric showroom + state machine - Hero radial-gradient backdrop is *toned* by the ticket's current status (primary for Open, cyan for InProgress, success for Resolved, muted for Closed) so the page itself reflects where the ticket is. - Centerpiece is a horizontal status timeline — pill chips for the four lifecycle states linked by hairlines, with the active step brightened and ring-tinted. Past steps show as foreground; future steps are muted. - Action cluster: Refresh, Assign / Reassign (Reassign uses UserCheck vs UserX based on current state), Resolve (when not already resolved/closed), Reopen (when resolved/closed). One-click reopen with toast confirmation; resolve and assign open dedicated dialogs. - Description panel renders the original report; a tinted "Resolution" callout block appears below it when the ticket has been resolved with a note. - CommentsPanel — proper conversation surface. Each comment is a small avatar + author code + relative time + body block. Composer is an inline textarea with character counter, disabled when the ticket is Closed (with explanatory placeholder). - MetadataPanel sidebar with Reporter/Assignee codes, Created/Updated /Resolved timestamps in mono dates with relative-time hints. Trash — unified recycle bin at /system/trash - One page with a pill tab nav across Products / Brands / Categories / Tickets. Each tab has its own typed query + restore mutation; the shared TrashShell handles loading/empty/error/list/pagination so per-resource bodies stay focused on per-resource details. - Row layout: muted Trash2 icon plate → title + subtitle (sku/slug/ number) → secondary line with deleted-on relative + mono date, deleted-by user code, and the row id tail → Restore button (variant=outline, RotateCcw icon). - Empty state per tab uses the shared "plinth" EmptyState with per-resource copy directing back to the parent list. API + wiring - New @/api/tickets module (DTOs, search, get, create, assign, resolve, reopen, comments, listTrashed, restore) — string-typed TicketStatus/TicketPriority unions match the backend's JsonStringEnumConverter wire format. - @/api/catalog gets listTrashed + restore for brands, categories, products, plus the optional DeletedOnUtc / DeletedBy fields on every catalog DTO. - Three lazy routes (tickets, ticket-detail, trash) — bundle stays partitioned at 538 KB index + per-route chunks (tickets 14 KB, ticket-detail 21 KB, trash 10 KB). - Sidebar grows two new entries: "Helpdesk · Tickets" group and a "Trash" item under System. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pipeline durability and observability
- Two-lane channel: default lane keeps DropOldest semantics; new
security lane uses BoundedChannelFullMode.Wait so compliance events
never drop. Worker drains security first, then default.
- Bounded retry with exponential backoff in the sink flush path; on
exhaustion the batch is handed to a new IAuditDlqSink. Default impl
is a JSONL file under {ContentRoot}/audit-dlq with daily rotation —
intentionally local so a Postgres outage can't take the DLQ with it.
- Drop counter, queue depth, flush latency, dead-letter count emitted
via System.Diagnostics.Metrics; AddMeter wired through the existing
OTel registration.
Read-path correctness
- GetAuditsQueryHandler wires the previously-ignored TenantId filter
behind a new Permissions.AuditTrails.ViewCrossTenant (Root) check —
bypasses Finbuckle's anonymous tenant filter via IgnoreQueryFilters
and re-applies an explicit predicate so cross-tenant access is
surgical, not blanket.
- GetAuditSummaryQueryHandler rewritten as four GROUP BY projections
pushed to SQL; previously materialized the entire filtered set.
- Both queries default to a 7-day window, hard-clamp to 90 days.
Validators return 400 when the caller supplies an oversized range.
Privacy and taxonomy
- IAuditMaskingService.ApplyMasking now returns MaskingResult with a
redacted-field count so the middleware can set AuditTag.PiiMasked
only when masking actually fired.
- New [NoAudit] / [NoAudit(BodyOnly = true)] endpoint metadata and
matching .NoAudit() / .NoAuditBody() builder extensions for routes
whose bodies must not be captured (password reset, etc.).
- AuditSourceResolver emits stable api.{module}.{routeName} keys
instead of the long endpoint display name, so dashboard filters by
Source survive refactors.
Background-job attribution
- HttpAuditScope falls back to the ambient Finbuckle tenant accessor
and ICurrentUser when there is no HttpContext, attributing entity-
change audits emitted from inside Hangfire-driven SaveChanges.
- ChannelAuditPublisher backfills tenant + trace from ambient state
for envelopes published outside an HTTP scope.
Schema (HardenAuditIndexes migration)
- Composite (TenantId, OccurredAtUtc DESC) and (TenantId, EventType,
OccurredAtUtc DESC) for the hot dashboard query path.
- Single-column on CorrelationId and TraceId for request-correlated
drill-down.
- GIN + jsonb_path_ops on PayloadJson; GIN + pg_trgm on Source and
UserName for fast ILIKE. pg_trgm extension declared on the context.
- Drops the now-redundant single-column TenantId / EventType /
OccurredAtUtc indexes.
Retention
- AuditRetentionOptions (per-event-type retention days, batch size,
cron) with conservative compliance defaults: Activity = 30d,
EntityChange = 90d, Security = 365d, Exception = 180d. Opt-in via
Enabled = true.
- AuditRetentionJob registered as a daily Hangfire recurring job;
uses ExecuteDeleteAsync with bounded sub-query batches to avoid
long lock spans.
Named query filters (prerequisite refactor)
- BaseDbContext registers soft-delete as a named filter
(QueryFilters.SoftDelete). Trash-view and restore handlers in
Catalog and Tickets switched to IgnoreQueryFilters([SoftDelete]),
so Finbuckle's anonymous tenant filter stays in force — fixes the
cross-tenant trash leak in the legacy IgnoreQueryFilters() callsites.
API surface
- /health/ready returns the same JSON payload on 200 and 503 so the
dashboard health page can show *which* check failed under degradation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the inline check-card styles with a token-driven `.health-card` component class set in globals.css. The card consumes a `--tone` CSS variable that the React component sets per-status (success / warning / danger), and every accent downstream — rail, icon halo, LED chip, gauge fill — derives from that tone via relative-color math, so a single swap recolors the whole faceplate in lockstep. Also folds the health-page hero atmosphere into a shared utility so the per-status tint stays consistent with the cards below. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A new admin surface for tracking *all* sessions across the tenant,
filling the gap between the per-user list on Profile · Security and
the per-user admin view on the user detail page.
Backend
- ISessionService.GetTenantSessionsAsync — paged, filterable list of
sessions for the current tenant. Search runs across user name,
user email, and IP address (case-insensitive ILike). Active-only by
default; pass includeInactive=true to surface expired/revoked rows.
- SessionService implements the new method against UserSessions with
the existing tenant guard, the standard 1-200 page-size cap, and
ordering by LastActivityAt desc. Reuses the existing MapToDto for
consistency with per-user queries.
- New Mediator slice GetTenantSessions in
Modules.Identity.Contracts/v1/Sessions/GetTenantSessions and
Modules.Identity/Features/v1/Sessions/GetTenantSessions, returning
PagedResponse<UserSessionDto>.
- New endpoint GET /api/v1/identity/sessions (no userId in path —
tenant-scoped via Finbuckle filter), gated on
IdentityPermissions.Sessions.ViewAll. Wired in IdentityModule.
Dashboard
- @/api/sessions extended with getTenantSessions(params),
adminRevokeUserSessionById(userId, sessionId), and
adminRevokeAllUserSessions(userId) — the latter two were already on
the backend but weren't surfaced as named functions in this module.
- New /system/sessions page — admin / "live operations console":
- StatStrip: total signed in, active on this folio, distinct users,
mobile sessions
- Filter bar: substring search (debounced 300ms) + Show inactive
Switch
- Row layout: device-typed icon plate (Smartphone vs MonitorSmart),
user name + email + "this device" / "revoked / expired" badges,
secondary line with browser/OS, IP, last-activity relative,
expires mono-date
- Action cluster: Revoke (single session) + All devices (revoke all
sessions for the same user). Hidden on the calling admin's own
current session — replaced with a "You" pill so we can't
self-sign-out from this view (Profile · Security stays the
place to manage your own session).
- 30-second auto-refetch since sessions move fast; manual Refresh
button stays available.
- Added to the sidebar under System with a Wifi icon, plus a new
lazy route at /system/sessions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Build the editorial-dossier user management surface for the admin console: list, create, and detail pages with an inline role-chip editor. - list (/users): editorial roster — numbered rows, deterministic letter monograms, segmented filters (status / email-confirmed / role), debounced search, pagination - create (/users/new): two-column dossier form (Identity / Credentials sections) with self-validation - detail (/users/:id): hero block + identity spine + inline role chip editor with dirty-count + Save/Discard - shared primitives: Monogram (FNV-hash → 4 grayscale tones, light/dark aware, 4px grid overlay), SectionRule (\\ USERS \\ DIRECTORY editorial header) - typography: Fraunces variable serif (h1/h2 with optical sizing) + JetBrains Mono (IDs, emails, captions). Honours the existing monochromatic oklch chroma=0 palette. - types: extract PagedResponse<T> to lib/api-types.ts (shared by tenants + new users API client). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Close a cluster of identity-domain gaps that allowed a tenant to be
locked out via the API.
Roles (RoleService.cs)
- DeleteRoleAsync rejects system roles (Admin, Basic). Was unguarded.
- CreateOrUpdateRoleAsync rejects (a) modifying a system role,
(b) renaming any role to a system role's name, (c) creating a role
with a system role's name.
- UpdatePermissionsAsync now uses the same system-role helper so Basic
permissions are protected too (was Admin-only).
Users (UserStatusService.cs)
- DeleteAsync delegates to ToggleStatusAsync(activate:false, ...).
Single code path → all guards apply uniformly to both DELETE and
PATCH /users/{id}: admin-only actor, no-self, no-admin-target, ≥1
active admin remains, audit logging.
- HTTP statuses: actor-not-admin → 403, business-rule violations → 400
(were silently 500 because CustomException defaulted to 500).
Roles assignment (UserRoleService.cs)
- Reject when the actor removes their own Admin role
("Administrators cannot remove their own admin role.")
- Root-tenant-admin guard now throws 403 (was silent 500).
- Admin-count threshold consolidated to "at least 1 admin must remain"
(was <= 2). Matches the existing rule in UserStatusService.
Groups (UpdateGroupCommandHandler.cs, RemoveUserFromGroupCommandHandler.cs)
- UpdateGroup rejects system groups (All Users, Administrators) — they
were renamable / re-describable / role-changeable, which would break
the seed-by-name lookup in IdentityDbInitializer.
- RemoveUserFromGroup rejects when the group is default (i.e. All
Users), preserving the "every user is in All Users" invariant.
- DeleteGroup already had the guard (no change).
Profile (UpdateUserEndpoint.cs)
- UpdateUserProfile relaxed from RequirePermission(Users.Update) to
RequireAuthorization(). Endpoint hard-codes request.Id = current
user, so it can only ever update the caller's own profile — admin
permission was wrong.
Tests (Integration.Tests)
- SystemRoleProtectionTests (7 cases)
- UserManagementGuardTests (3 cases)
- SystemGroupProtectionTests (4 cases)
- 152 integration + 219 identity unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The sidebar <nav> had no overflow handling — intentionally, to allow collapsed-mode hover tooltips to escape its right edge. With six sections and ~14 nav items the column now exceeds viewport on standard laptops, so <aside> overflows → body scrolls (outer scrollbar) → and <main> already has its own overflow-auto (inner scrollbar), giving the user two scrollbars side by side. - Sidebar <nav> now has overflow-y: auto + overflow-x: clip. `clip` (CSS spec value distinct from auto/hidden) lets vertical scroll work without forcing the horizontal axis to also become a scroll context. Collapsed-mode hover tooltips will be clipped, but the existing title= attribute on each NavLink is the native fallback. - AppShell outer flex now has overflow-hidden as belt-and-braces — the body cannot ever spawn a scrollbar regardless of inner layout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three-pronged update:
1. Admin user management UI (Phase 2.2) — list, create, detail with
inline role editor; editorial-dossier aesthetic; new shared Monogram
and SectionRule primitives; Fraunces + JetBrains Mono.
2. Identity hardening — close cluster of role/group/user gaps that
could lock a tenant out:
- System roles (Admin, Basic) cannot be deleted, renamed, or have
their permissions modified
- Self-demotion blocked; root tenant admin protected
- DeleteUser delegates to ToggleStatusAsync — single guarded path
- System groups (All Users, Administrators) cannot be modified
- Default-group membership preserved
- UpdateUserProfile no longer requires admin permission
- All policy failures return 400/403 (were 500)
- 14 new integration tests; 152 integration + 219 unit pass
3. Dashboard scrollbar fix — sidebar nav now scrolls within itself
(overflow-y:auto + overflow-x:clip), AppShell outer is
overflow-hidden as belt-and-braces. No more double scrollbars.
…doc + tooling refresh
Bundles all working-tree changes that accumulated alongside the
admin/identity/dashboard work. Single commit because the changes
are interleaved across surfaces and shipping incrementally would
mean partial-state checkpoints.
Notable groups
- Terraform rename: deploy/terraform/apps/playground → apps/starter
(mirrors the in-flight host directory naming standardisation).
- Identity: new SystemPermissions.cs in Shared/Identity, expanded
PermissionConstants.cs, GenerateTokenEndpoint.cs tweaks,
MultitenancyConstants.cs.
- Host: new DevSeeding/DevDataSeeder.cs for local development data,
Dockerfile + Program.cs + appsettings + AppHost.cs updates.
- Dashboard login: demo-accounts panel (login.demo-accounts.ts +
login.demo-panel.tsx) + login.tsx wiring; auth-context, App.tsx,
impersonation-banner, list-hero, globals.css refinements.
- Admin login: api.ts + login.tsx tweaks.
- Web building blocks: Extensions.cs.
- Docs: full sweep across docs/src/content (architecture, building
blocks, deployment, project structure, quick start, etc.) +
llms-full.txt regenerated.
- Tooling: .agents/{rules,skills,workflows}, .github/workflows/ci.yml,
.template.config/template.json, .vscode/{launch,tasks}.json,
docker-compose.yml, scripts/openapi/README.md, src/Directory.Build.props,
CLI NewCommand.cs.
- Tests: Architecture.Tests.csproj + BuildingBlocksIndependenceTests +
HostArchitectureTests adjustments, Tests/README.md.
Hygiene
- .gitignore now excludes **/audit-dlq/. The audit pipeline's local
fallback DLQ was previously slipping into the working tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…v seeding, docs/tooling)
The $$"""...""" raw-string literal carries the source file's line
endings — on Windows that's CRLF — and Aspire pipes it verbatim to
`docker run minio/mc /bin/sh -c "<script>"`. /bin/sh inside the
container then sees `do\r` and `done\r;` as unrecognised tokens and
fails: "syntax error near unexpected token `done'".
Append .ReplaceLineEndings("\n") so the script is always LF regardless
of how the source file was checked out (CRLF/LF/autocrlf).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WebApplication.CreateBuilder calls UseStaticWebAssets synchronously
during init, which constructs a PhysicalFileProvider rooted at
{ContentRoot}/wwwroot. If the directory is missing on disk the
constructor throws DirectoryNotFoundException before Program.cs ever
gets a chance to run.
The folder was either never committed or got cleaned. .gitignore
already permits wwwroot/ at the root level (only **/wwwroot/uploads/*
is ignored), so a .gitkeep is enough to keep it in source control.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sonner v2 wraps title + description inside a `<div data-content>`
child. The previous .fsh-toast CSS placed those elements directly into
a 4-row grid via grid-row/grid-column — with the wrapper in the way,
those rules never apply and the title falls into default flow,
producing the duplicated/overlapping look reported on a 400 error
toast.
Rebuilt as flexbox so the layout survives any sonner DOM nesting:
- Flex column on .fsh-toast; data-content wrapper made `display:
contents` so title/description become direct flex items.
- Type pill ("err" / "ok" / etc.) moved from a standalone ::before on
the toast to ::before of .fsh-toast-title — one row, one rhythm,
pill leads the title text.
- Aurora gradient and double-shadow stack swapped for a single solid
card surface + 3px tone-coloured border-left rail. Cleaner read on
both light and dark.
- Close button now position: absolute top-right (was grid-row span)
so it doesn't perturb flex flow.
- Action / cancel buttons rely on natural flex stack; no grid row
references.
- Drain bar at the bottom kept (transform-only animation).
- Fixed width bumped from 360 → 380 to comfortably hold the new
inline pill + title row.
- Dropped unused --fsh-toast-aurora-light / --fsh-toast-aurora-dark
tokens.
- App.tsx comment updated to reflect the "tone rail" treatment.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The server rejects delete / rename / re-describe / permission edits on
the framework's built-in roles (Admin, Basic) with 400/403. The UI
should know that too rather than letting the user click a destructive
button only to be turned away by a toast.
When the role is a system role:
- Hero shows a lock badge + "system role · permissions" eyebrow + a
`system` outline badge alongside the permission count.
- Subhero notice card explains the read-only state.
- Delete role button is disabled with an explanatory title= tooltip.
- Name and Description inputs are readonly (cursor-not-allowed,
opacity 70, aria-readonly).
- Permission preset chips (Basic / All / Clear) are disabled.
- Per-group "all" / "none" toggles disabled; individual permission
tiles disabled and lose their hover affordance.
- Sticky save/discard bar is removed entirely — the dirty/save flow
no longer applies when nothing can change.
System role names ("Admin", "Basic") are mirrored from the server's
RoleConstants.DefaultRoles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rations The overview was a competent KPI strip but read as a generic admin-dashboard rather than the flagship surface the app deserves. Three additive moves bring it in line with the rest of the polish: 1. Atmospheric hero block. Replaces the plain header with a rounded atmospheric section: time-of-day greeting, big display serif tenant name, period chip, live-presence pulse, and a pure-SVG period progress ring on the right showing what % of the month has elapsed plus days-remaining. Refresh now lives inside the hero. 2. First-run panel. When the tenant has no active subscription and the user hasn't dismissed it, the page leads with a welcome card + four step-tiles (Pick a plan / Invite team / Browse catalog / Watch live activity). Dismissible per-tenant via localStorage; auto-hides as soon as a subscription appears. 3. Recent operations + Live stream. New 12-col row replacing the lone LiveFeed at the bottom. Recent operations card pulls the last 5 audited actions (24h window) with severity-coloured rails, source + user + relative time, deep-linking into the audit page. The Live stream sits beside it as the heart-rate counterpart — audited ledger on the left, ephemeral SSE feed on the right. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Existing accent system was capped at six presets (indigo / violet /
sky / emerald / amber / rose). The picker now adds a seventh slot —
"Custom" — that opens a modal with a hue ribbon, a saturation slider,
a live ladder of the eleven derived --brand-* stops, and a "Subscription
active" preview rendered inside an inline-style scope so the candidate
accent paints without disturbing the rest of the page.
Implementation
- BRAND_LADDER constant in appearance-options.ts captures the (L, C)
shape from the indigo template; buildCustomBrandStops(spec) yields
the eleven --brand-* values for a given hue and chroma scale.
- ThemeProvider gained a customAccent + setCustomAccent slot,
persisted to localStorage as { h, c }. applyAccent(id, customSpec)
branches: presets toggle the accent-* class and clear inline
overrides; "custom" applies inline --brand-* styles directly on
documentElement so it wins over any preset class without needing a
generated class.
- Switching from custom back to a preset clears the inline overrides
so the preset is visible again.
- Hue/saturation sliders are styled tokens — webkit + moz thumb
variants, no third-party color picker dependency.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The audit detail drawer had identity / trace / payload / pipeline
sections but no way to see other events that fired under the same
correlation ID without closing the drawer and re-querying.
Adds a new "Related events" section between Trace and Payload, shown
only when the current event has a correlation ID. Renders the eligible
audits as a vertical timeline (newest → oldest, capped at 12) with:
- A per-row severity-coloured node dot, with the current event ringed
in primary-soft so its position in the thread is obvious.
- Source + severity label + ISO time + signed time-delta from the
current event ("+12s", "-4s") — at-a-glance ordering without doing
the math.
- Click any other row to swap the drawer to that audit's detail
without closing or losing the user's filter context. The drawer's
query simply re-fires for the new id.
Implementation tweaks
- AuditDetailDrawer + DrawerBody gained an onJumpAudit prop wired
back to the page state (setDrawerId).
- The "All by correlation" / "All by trace" jump buttons stay — the
new timeline is for "see context now"; those still exist for
"filter the list to this correlation".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Health, audits, sessions, trash, and the settings layout each had their own bespoke <header> block — same eyebrow + h1 + subtitle shape, but visually flatter than the catalog/identity list pages that ride on ListHero's atmospheric backdrop. Adds a sibling component, PageHero, that shares ListHero's chrome (radial brand wash + faint noise overlay + rounded card surface + eyebrow row + display heading) but drops the list-specific search bar and CTA button, exposing an `actions` slot instead. Used by: - system/health: System · Health eyebrow, Live/Paused + Refresh buttons in the actions slot. - system/sessions: System · Sessions, Refresh in actions. - system/trash: System · Trash, no actions (read-only surface). - settings/settings-layout: Account · Settings, no actions. - audits: System · Audit, Refresh in actions. Visual consistency across the dashboard now unifies catalog, identity, and the system/settings sections under one hero language. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… pages The flat group list was readable but every item competed equally for attention; with the dashboard now spanning Catalog, Identity, System, and Helpdesk the column needed sectional discipline. Reorganises the nav into: Overview ← top-level (no section) ─ Operations ▾ ← accordion sections, single-select ─ Catalog ▾ ─ Helpdesk ▾ ─ Identity ▾ ─ System ▾ Settings ← top-level (no section) Behaviour - Single-select: only one section can be open at a time. Clicking a collapsed section header opens it and closes any other open one; clicking the open header collapses it (zero-section state). - Auto-sync to route: navigating into a section's child route via the command palette / a deep link / a sidebar item itself opens that section. Top-level routes (`/`, `/settings`) collapse all sections. - Active section gets a card-style surface — surface-3 background + hairline border + inner highlight shadow + 6px padding — so the open section reads as the focal column. - Closed section: borderless flat row with mono-caps caption + section icon + chevron-down indicator on the right; hover affordance only. - Active item inside an open section keeps the existing 2px brand bar + brand-soft pill highlight. Collapsed sidebar (64px wide) - Accordion behaviour drops out — the section labels can't render at 64px wide. Items render as a flat icon stack with thin dividers between sections (matches the previous behaviour). - Top-level Overview + Settings remain icon-only as before with hover tooltips (clipped by overflow-x: clip per the earlier double- scrollbar fix; native title= attribute is the fallback). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…aphy
Two refinements to the sidebar accordion that landed in the previous
commit:
1. Subtle expand/collapse animation. The items panel was rendered
conditionally — appearing/disappearing instantly with no visual
continuity. Now uses the grid-template-rows 0fr ↔ 1fr trick:
- Wrapper is a CSS grid that animates its single track size from
0fr (closed) to 1fr (open) over the default duration token.
Inner div carries `overflow-hidden min-h-0` so contents are
clipped during the transition.
- Items themselves crossfade with a small (80ms) delay on the way
in so the slide and the visual reveal stay in lockstep — open
looks like a panel materialising, not items popping into a void.
- Section card chrome (background, border, padding, shadow) now
animates explicitly via transition-[bg,border,box-shadow,padding]
instead of just transition-colors, so the card surface fades up
rather than snapping in.
- Chevron rotation already animated; bumped to default duration
to align with the other transitions.
2. Matched typography between section headers and nav items. The
captions were rendering in mono small-caps (`font-mono text-[10.5px]
uppercase tracking-[0.14em]`) while the nav items were sans medium
13.5px — the visual whiplash made the sidebar read as two
competing typographic systems. The section header is now
structurally identical to a NavItemLink: h-9 row, gap-3, px-3,
h-4/w-4 icon, text-sm font-medium, normal case. Differentiation
from a nav item is now carried by the trailing chevron and the
card-surface treatment when the section is open, not by font /
size / case shifts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…l, etc.
The "g" in "Settings" (and any other title with a g/y/p/q descender)
was being clipped at the bottom by the hero card. Two compounding
causes, both addressed:
1. Tight line-height. PageHero used `leading-[1.04]` and ListHero used
`leading-[1.02]`. At 34-44px font sizes the line-box wasn't tall
enough to contain the font's natural descender — the bottom hairs
hung past the line-box and got clipped by the section's outer
`overflow-hidden` (which is kept so the radial gradients don't
bleed past the rounded corners).
Bumped to leading-[1.12] / leading-[1.1] respectively, plus pb-1
on the h1 as a belt-and-braces buffer for fonts whose descenders
dip beyond the declared metrics.
2. PageHero wrapped its title in `<span className="truncate">`, which
adds its own `overflow: hidden` at the title level — a second
clipping layer on top of the section's. Removed the wrapper; page-
hero titles are short ("Settings", "Audit trail", "Health") so
unconstrained wrapping is fine.
The catalog ListHero hadn't shown the bug because its existing titles
("Brands", "Products", "Categories") have no descenders — the issue
was dormant. The fix forwards-protects any future descender-bearing
title on a list page.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Login was a centred 420px form against the parallax aurora background.
On wide viewports it left a lot of unused screen real estate that
could be carrying the marketing weight a starter-kit landing benefits
from. Adds a brand-story column on the left at lg+ widths.
Brand-story (`login.brand-story.tsx`)
- 40px brand mark + mono caps "FullStackHero · console" eyebrow.
- Display-serif headline ("The starter kit your .NET team has been
waiting for.") with a brand-gradient on the noun, fluid clamp() so
it scales between 32-46px.
- Tagline cycle: 4 short statements crossfade every 4s ("Production-
grade .NET 10 starter kit.", "Modular monolith, ready to ship.",
"Multi-tenant from day one.", "Aspire-orchestrated. SSE-powered.").
Fixed-height container so the rotating opacity transition doesn't
reflow neighbouring content.
- Trust strip — 4 feature pills (JWT, Modular, EF Core 10, Live SSE)
with tone-tinted icons + label + sub-line. Stagger entrance via
fsh-enter classes.
- "Service ready" pulse-dot above the trust strip — pure visual
signal, not a live probe (login is pre-auth).
Layout (login.tsx)
- Narrow viewports (<lg): brand-story is hidden; the form leads, demo
panel stacks below in DEV. Faster sign-in, no marketing distraction.
- ≥lg non-DEV: 2-col grid `[1fr | 420px]`, max-width 920px. Brand
story fills the flexible column, form anchors at 420px.
- ≥lg DEV: 3-col grid `[1fr | 420px | 320px]`, max-width 1180px. Demo
panel still appears beside the form so dev convenience isn't lost.
- Promoted breakpoint from md (768px) to lg (1024px) — at 768px the
three-column DEV split was cramped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous three-column layout read as three rectangles glued
together — bland trust pills, duplicated brand mark, and no real
atmosphere binding the composition. Replaces with an editorial
product-page approach where a single composed canvas carries the
weight.
Layout
- Top strip: one brand mark on the left, "Service ready" pulse-dot +
v0.1 chip on the right. The duplicate brand mark that used to sit
inside the form card is gone — the form's identity now comes from a
3px brand-coloured tone-rail along its left edge plus a "/ SIGN IN"
eyebrow.
- Main composition (lg+): hero column (≈55%) + form card (420px),
centred in a 1200px max-width canvas, vertically pinned to the
middle of the remaining viewport.
- DEV demo panel: hidden until xl (1280px+) so it doesn't crowd the
composition on a laptop. Auto-fits as a third column on widescreen.
- Atmosphere: three parallax aurora orbs (top-left brand, mid-right
teal, bottom-mid brand) drifting opposite the cursor for soft depth.
- Bottom: a full-bleed tech-stack marquee — 20 stack labels scrolling
at 80s, edge-faded via a CSS mask, paused on hover.
Hero panel (login.brand-story.tsx)
- Replaces the previous trust-pill grid with strict editorial
hierarchy: hairline + mono-caps eyebrow → massive display headline
(fluid clamp 40-80px, with a brand-gradient on ".NET 10") →
tagline crossfade → editorial stat strip ("14 MODULES · 08 BUILDING
BLOCKS · 02 DEMO APPS") with hairline dividers, no box chrome.
- The stat strip carries the same job the trust pills did (signal
what's in the box) but reads as a magazine deck instead of a
feature comparison table.
Form card refinement
- Tone-rail: 3px solid `--color-primary` border-left anchors the card
to the brand without the duplicate brand mark.
- "/ SIGN IN" eyebrow + "Welcome back." headline. pb-1 on the heading
protects the descender on "back." from line-height clipping.
- Trust line ("Encrypted in transit · JWT-secured session") moved
below the CTA, lighter weight so it doesn't compete.
CSS
- New `.fsh-marquee` utility + `fsh-marquee-x` keyframe in globals
for the tech-stack ribbon. Caller renders content twice; -50%
translate yields a seamless loop. `prefers-reduced-motion` opts
out.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The split-column "story | form | demo" composition wasn't carrying
its weight — three rectangles glued together with no visual binding.
Replaces with a single-column editorial composition that reads top
to bottom like a spec sheet.
Layout (single column, centred max-w-680px)
- Top strip: brand mark left, status pulse + version chip right.
- Eyebrow: "// FullStackHero · console" — hairline divider on each
side, mono caps, dead-centre under the top strip.
- Display headline (clamp 32-52px) with a brand-gradient on
".NET 10". Two lines, calm.
- Pitch line, max-w-md.
- Login card — 440px wide, glass surface with backdrop blur, framed
by four tone-coloured corner brackets pinned to its corners. Reads
as an engineering / blueprint marker rather than another generic
card. Inside:
* "// 01.SIGN-IN" mono eyebrow + "tenant · jwt" right-eyebrow.
* "Welcome back." heading (display, brand-gradient on the noun).
* Tenant / Email / Password float-fields.
* Sign-in CTA with shimmer + arrow.
* DEV row at the bottom, hairline-separated: "// demo accounts"
button — opens a popup, no longer a sidebar column.
- Editorial stat strip on the canvas BELOW the card (not inside it):
three numbers (14 modules · 08 building blocks · 02 demo apps),
hairline dividers, mono captions. Reads as page-level commentary.
- Trust line ("Encrypted in transit · JWT-secured session") under
the stats.
- Tech-stack marquee at the bottom, kept verbatim from the previous
iteration — the user explicitly liked it.
Atmosphere
- Three parallax aurora orbs (kept).
- New: a low-opacity radial dot grid masked to a centred ellipse so
it fades to nothing at the edges — gives the page a graph-paper
/ blueprint texture without competing for attention.
Demo accounts now a popup
- Removed the sidebar column entirely. Click "// demo accounts"
under the form (DEV only) → opens a Dialog containing the existing
LoginDemoPanel. Picking an account closes the dialog and prefills
the form. Dialog content uses !p-0 + overflow-hidden so the panel
renders edge-to-edge inside the dialog without nested-card chrome.
- DemoDialog component lives inline in login.tsx (small enough to
not warrant its own file).
Removed
- login.brand-story.tsx — no longer used; the editorial story now
lives directly in login.tsx as the eyebrow + headline + stat
strip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#Architecture
scripts/openapi/generate-api-clients.ps1 -SpecUrl "<spec>"); Blazor consumes generated clients.