Skip to content

FullStackHero 10 .NET Starter Kit Release Merge#1152

Draft
iammukeshm wants to merge 363 commits intomainfrom
develop
Draft

FullStackHero 10 .NET Starter Kit Release Merge#1152
iammukeshm wants to merge 363 commits intomainfrom
develop

Conversation

@iammukeshm
Copy link
Copy Markdown
Member

#Architecture

  • Modular monolith with modules for Identity, Multitenancy, Auditing; mediator-based CQRS; background jobs; caching; mailing; storage abstraction.
  • Minimal API host with Identity (JWT, refresh, roles/permissions), Multitenancy (Finbuckle, provisioning lifecycle), Auditing (request/response/security/exception with background sink).
  • Shadcn-inspired MudBlazor wrappers; Dashboard/Profile/Audits pages wired to generated API clients; BFF-style auth delegating handler; theme/layout shell.
  • NSwag config + script to regenerate clients (scripts/openapi/generate-api-clients.ps1 -SpecUrl "<spec>"); Blazor consumes generated clients.
  • Multi-app AWS scaffolding (API/Blazor) with modular structure using Terraform.
  • Mediator Handlers and Validation
  • RateLimiting / Storage / Outbox Pattern

@iammukeshm iammukeshm requested a review from Copilot December 9, 2025 09:12
@iammukeshm iammukeshm self-assigned this Dec 9, 2025
@iammukeshm iammukeshm added the enhancement New feature or request label Dec 9, 2025
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

@iammukeshm iammukeshm marked this pull request as draft December 9, 2025 09:13
@maxiar
Copy link
Copy Markdown
Contributor

maxiar commented Dec 10, 2025

#Architecture

  • Modular monolith with modules for Identity, Multitenancy, Auditing; mediator-based CQRS; background jobs; caching; mailing; storage abstraction.
  • Minimal API host with Identity (JWT, refresh, roles/permissions), Multitenancy (Finbuckle, provisioning lifecycle), Auditing (request/response/security/exception with background sink).
  • Shadcn-inspired MudBlazor wrappers; Dashboard/Profile/Audits pages wired to generated API clients; BFF-style auth delegating handler; theme/layout shell.
  • NSwag config + script to regenerate clients (scripts/openapi/generate-api-clients.ps1 -SpecUrl "<spec>"); Blazor consumes generated clients.
  • Multi-app AWS scaffolding (API/Blazor) with modular structure using Terraform.
  • Mediator Handlers and Validation
  • RateLimiting / Storage / Outbox Pattern

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"
BTW.... What IDE or stack you use or recommend to work better, get a good experience with this starter kit, may be VS 2026 + Copilot, or VS Code + Codex or Cursor + Another IA Model, what is your experience creating this template, Did you use any AI Assistance with some .MD spec files to define the software architect guidelines or something like that?

Thanks in advanced.

@iammukeshm
Copy link
Copy Markdown
Member Author

@maxiar

I am currently using VS2026.
Docs is ignored purposely, as they will be on another repo. It's still a WIP.
For AI Code Guidance, currently testing with Codex CLI. Trying to formulate a framework for a nice workflow experience. Will write about it on my blog once I figure it out.

@maxiar
Copy link
Copy Markdown
Contributor

maxiar commented Dec 12, 2025

@maxiar

I am currently using VS2026.

Docs is ignored purposely, as they will be on another repo. It's still a WIP.

For AI Code Guidance, currently testing with Codex CLI. Trying to formulate a framework for a nice workflow experience. Will write about it on my blog once I figure it out.

Perfect approach, check that may be some ideas are usefull:

https://medium.com/@mikhail.petrusheuski/steal-these-25-prompts-the-rules-workflows-that-made-our-net-team-faster-27899ece4dcc

And this "spec driven AI design":

https://medium.com/@mikhail.petrusheuski/steal-these-25-prompts-the-rules-workflows-that-made-our-net-team-faster-27899ece4dcc

@iammukeshm
Copy link
Copy Markdown
Member Author

iammukeshm commented Dec 12, 2025

@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
iammukeshm and others added 30 commits April 30, 2026 03:57
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>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants