Repository navigation
Add the audit log screen with export download and item history - #2460
Merged
Merged
Conversation
# Conflicts: # src/FluentCMS.Admin/AdminRoutes.cs # src/FluentCMS.Host/FluentCmsHost.cs
4 tasks done
pournasserian
added a commit
that referenced
this pull request
Oct 18, 2026
Refs #2434 ## What - **Theme**: the root element carries `light` or `dark` when someone chose one and no class otherwise, so the stylesheet follows the system preference. The `dark` variant and the dark tokens apply with the `dark` class or, with no stored choice, under `prefers-color-scheme: dark`; token values are unchanged, and `color-scheme` now follows the theme so native controls and scrollbars match. A toggle in the main layout header (shadcn's mode toggle: Light, Dark, System on the existing DropdownMenu) is handled by `admin.js`, which sets the class and keeps the choice in a `fluentcms-theme` cookie. The host reads the cookie through `IThemePreference` (defined in Admin, implemented in Host, like `ICurrentInitiator`) and renders the class into `<html>`, so there is no inline script and no flash of the wrong theme. `?theme=` is gone; `?dir=rtl` stays until the UI culture sets the direction. - **Reconnect**: `ReconnectModal.razor` renders the element Blazor looks for (`#components-reconnect-modal`), so Blazor uses it instead of its default display, whose injected `<style>` the CSP blocked. Blazor reports the state as a class; `app.css` shows the matching part (rejoining, retrying with the countdown, failed with Retry, paused with Resume, resume failed with Reload) and `admin.js` runs the buttons, retries when the tab becomes visible after a failure and reloads after a rejected circuit, as the default display does. It is a status card at the bottom end, not a modal. `data-permanent` keeps enhanced navigation off it. The CSP is unchanged. - **`/ui`** replaces `/ui-spike`. It renders every component in `Components/Ui` (Button, Badge, Input, FormField, Card, Table, Dialog, DropdownMenu, Command) with its keyboard hints, for any signed-in user in every environment, as #2434 asks once sign-in exists. Server actions report to a status line, so a round trip and a reconnect are visible. `AdminOptions` goes with the Development-only gate. Keyboard hints use shadcn's Kbd classes on a plain `<kbd>`; no component was added for the page. - **Two `admin.js` fixes found on `/ui`**: opening a DropdownMenu with the arrow keys from its trigger called `showPopover()` without a source, so the menu lost its anchor (in RTL it landed in the opposite corner); it now passes the trigger. Command's active option used `scrollIntoView`, which scrolled the page down to the palette on every load; it now scrolls only the list. - `CONTRIBUTING.md`: a sentence on `/ui`, and that a new component goes on it in the same pull request. ## Boxes on #2434 Ticked now, already true on v2 before this pull request: - Blazor Web App in `FluentCMS.Admin`: `App`, `Routes` and the layouts; interactive server pages (`/ui`) next to static ones (setup, sign-in, home, audit); composed by Host with `MapRazorComponents<App>().AddInteractiveServerRenderMode()`; `ProjectReferenceRulesTests` keeps Admin on Core only. - Root element carries `dir` and the theme class. This pull request moves the class from the query string to the stored choice. - Components in `Components/Ui`, self-contained, no base classes, no project or package. - Localization and RTL: every string goes through `IStringLocalizer` (there are no resource files yet, so the English text is the key), only logical utilities, and `dir` flips the layout. To tick when this merges: default reconnect handling; shadcn tokens with light and dark switched by a root class and defaulting to the system preference. Left open: the interop module (focus trapping and roving tab index are there, global keyboard shortcuts are not; they come with the desk, the first screen that needs one), the tailwind-merge port, Lucide icons, the rest of the initial set, bUnit per component (Button has none, and keyboard behaviour lives in `admin.js`, which bUnit does not run) and the `/ui` acceptance, which includes the WCAG checks. ## Why - **A cookie the server reads, not local storage.** The class has to be on `<html>` before the first paint, the CSP allows no inline script to read storage that early, and `admin.js` is a deferred module. A cookie lets the host render the class. The toggle itself stays in the browser, so it works on static pages and while the circuit is down. - **Light, Dark and System rather than a switch.** With two states, choosing once loses the system default for good. It is shadcn's own mode toggle. - **Sun and Moon as inline SVG.** The paths are Lucide's, written inline the way `DialogContent` and `CommandInput` already do. Generating icon components from the set needs a tool or a package, so that box stays open. - **Not modal.** ADR-0011 chose native primitives partly because they keep working through a lost circuit; a modal overlay would make the page inert for exactly that gap. No new dependency: no NuGet package, icon set or test tool. ## Tests - bUnit: the toggle (named trigger, three choices left to `admin.js`, no inline script, style or handler attributes); the reconnect markup (the element Blazor looks for, one part per state, the countdown inside the translated sentence, a button per failure, no inline style and no display utility that would override the stylesheet); `/ui` renders every component type in `Components/Ui` and fails when one is missing, lists keyboard hints in every section, reports a click on the status line and switches direction. - Stylesheet, from the compiled CSS: dark applies with the class or, with no stored choice, under `prefers-color-scheme: dark`; the reconnect element is hidden until a state class shows one part. - Host: the shell has the toggle and the reconnect element; the root class follows the cookie (`dark`, `light`) and is absent for no or an unknown value, on admin and account pages; `/ui` redirects to sign-in when anonymous and renders for a signed-in user outside Development; `/ui-spike` answers 404; the CSP has `style-src 'self'` and no `unsafe-inline` or `unsafe-eval`; sign-in, home and `/ui` carry no inline script, style or event handler. The sign-in tests now use `/ui` as their protected route. Checked by hand in the browser: the system theme with no choice; Light, Dark and System from the header by mouse and keyboard (the choice survives a reload, System clears it); the menu end-aligned in LTR, RTL and at phone width; `/ui` in RTL; the reconnect card with the server stopped (the dialog still opens meanwhile), pause and Resume, the failed and resume-failed parts, and the reload after a rejected circuit. No CSP violations in the console. ## Proposed, not done here - Automated WCAG 2.1 AA checks for `/ui` (UX-04) need a new test tool, for example axe-core run through Playwright. Playwright arrives with the desk flows (F-DSK); the checks fit there. - Unpublished runs outside Development serve the admin's `_content` assets as empty 200 responses: the integration test host, and `dotnet run --project src/FluentCMS.Host` as CONTRIBUTING describes it, run without the stylesheet and `admin.js`. The existing asset test checks only the status, so it passes. That is why the stylesheet test above reads the compiled file. Worth its own issue. ## Rebased on v2 Merged `origin/v2` (cd2e26c) after #2460, #2466 and #2467. No conflicts: the header keeps the audit and user nav entries next to the theme toggle. The Table components from #2466 are now a section on `/ui`, as the showcase test requires. ## Checklist - [x] Linked to an issue a maintainer has confirmed - [x] Targets `v2` (or `dev` for a v1 security fix) - [x] Tests or docs updated where behaviour changed - [x] No tool-generated co-author trailers or footers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2458
Refs #2434
What
The third slice of F-AUD (#2424): the first screen on the log, behind the sign-in from #2450.
IAuditExporter.ExportAsyncnow commits anaudit.exportedevent in its own unit of work before it writes anything. The event names the current initiator and, when the selection is one item's history, that item as its subject. An invalid query or format records nothing. The query runs only when the export starts writing, so a selection that reaches the present contains its own export event.AuditErrorCodesreplaces the string literals inAuditQueryand the exporter./audit, server-rendered without interactivity like the account pages, with an "Audit log" link in the main layout.AuditExportFormmaps the posted fields to anAuditQueryand a format and reports every problem at once, including unreadable dates and a malformed id.IStringLocalizer, and the layout uses logical utilities only.POST /audit/exportmaps the form, calls the exporter and streams the file withContent-Disposition: attachmentandCache-Control: no-store. The file name isaudit-logoraudit-history-<id>, so nothing typed into the form reaches the header. It requires a signed-in user, and form binding applies the anti-forgery check. A rejected form goes back to/auditwith its values and error codes in the query, and the page puts each message next to its field.GET /audit/exportredirects to the page, for someone whose session ran out, who signs in again and comes back with a GET.Why
No new NuGet dependency and no migration.
Tests
Unit: form mapping (UTC days with both ends included, empty fields select the full log, a one-day window, an item's history, every problem reported at once, unreadable dates per field, half an item, the return query keeps values and lists each code once). Exporter: the export event is committed before the first byte in both formats; an item's export names the item; an invalid query or format records nothing and opens no unit of work. bUnit: Badge default and variants.
Integration through the running host: anonymous
GET /auditandPOST /audit/exportgo to sign-in and record nothing. A signed-in JSON download of an item's history and a CSV download of a date range return the selected events, each export's own event included, as attachments with the right content type, file name,no-storeand the CSP; each records oneaudit.exportedevent naming the user. A rejected export redirects to the page with both messages next to their fields, the values kept, and nothing recorded. A forged anti-forgery token gets 400 and records nothing. The history lookup shows an item's events oldest first with every field, encodes a source containing a script tag, leaves out other items' events, and handles an unknown and a malformed id. The layout links to the page. The integration tests from #2449 now also check that an item's export contains its own record and that an empty selection is still recorded.Checked by hand in the browser: the page in light, dark and RTL, a rejected export coming back with its errors, the history of the signed-in user, and no CSP violations.
Checklist
v2(ordevfor a v1 security fix)