Skip to content

Add notification counts in the header and the security events view - #2590

Merged
pournasserian merged 1 commit into
v2from
2587-notifications-security
Oct 19, 2026
Merged

pournasserian merged 1 commit into
v2from
2587-notifications-security

Conversation

@pournasserian

Copy link
Copy Markdown
Member

Closes #2587

What

The fifth slice of F-DSK (#2422): in-app notification counts (DSK-05) and the security events view on the desk (SEC-08).

  • Core (FluentCMS.Core.Audit):
    • SecurityEvents.Operations: the operations SEC-08 names.
      • Agent keys issued and revoked.
      • The three cap refusals and an agent's refused move.
      • People added, disabled and enabled, password resets issued and used, and external sign-ins linked.
      • Routing rules created, revised, activated and deactivated. Those match nothing until routing rules (Add routing rules and their evaluation #2565) merge.
    • IAuditEventLists: a page of the log with any of the given operations, newest first, optionally since a time, with the total. CountAsync gives the total alone. A bad window fails with offset_invalid or limit_invalid, and no operations with audit_operations_missing. It is separate from IAuditReader, which streams the log oldest first for exports.
  • Core (FluentCMS.Core.Desk): IDeskQueues.CountAsync(queue) counts one queue, so the header does not count all six.
  • Infrastructure: AuditEventLists in its own file, SQL over AuditEvents as in AuditReader.
  • Admin:
    • NotificationLinks in the header replaces the plain Desk link.
      • Desk shows how many sets await the signed-in person (awaiting me).
      • A new Security link shows how many security events the last 24 hours hold.
      • Each count is a badge with a screen-reader suffix and appears only when it is above zero. Counts are read on each page load.
    • /security: the events newest first, in pages of 50. Each row has the time, a sentence for the operation, who acted (agent, person or system badge and name) and the subject, linked to its audit history. The rows rove like the desk's.
    • The desk links to /security.

Why

  • A list of operation names in Core. Each lane appends its own events, and the desk should not depend on all of them. An operation nobody appends yet costs nothing, so routing rules can be named now.
  • Counts per page load. M1 has in-app counts only (DSK-05). The layout is static, so a count is current on every navigation without a circuit or polling. Each count is one indexed query.
  • The last day for the security badge. A count of everything ever recorded would never fall back to zero. One day is enough for a daily check, and NotificationLinks.SecurityWithin is the one place to change it.
  • Not in this PR:
    • "Injection flagged" (SEC-08): the instructions check (SEC-03) reports a finding in a check result, not an audit event. Once check results are stored (Run the checks on every submitted change set #2575), a flagged result can append its own event and join the list.
    • Marking events as seen, and email (DSK-06, M2).

Tests

Unit:

  • DeskQueuesTests: one queue is counted on its own, stale facts included.
  • bUnit SecurityEventsViewTests:
    • Each row has its sentence, initiator, subject link and time, and the query names the security operations.
    • The page comes from the address and links to its neighbours, and an empty log says so.
    • The header shows both counts only when they are above zero, and counts security events since one day ago.

Integration:

  • On SQLite and PostgreSQL: a list holds only the named operations, newest first, with the total, a window and a time from any offset.
  • A bad window or no operation is refused, and a list appends nothing.
  • Host: after a key is issued and revoked, /security lists both newest first, links the credential's history and leaves out the change set events. The header counts one set awaiting the owner and two security events. An anonymous request goes to sign-in.
  • Browser: the axe WCAG 2.1 AA check now covers /security too, in both directions and themes.

dotnet build -warnaserror and the full suite with the browser tests pass locally against a postgres:17 container as well as SQLite. No new package and no new project reference.

Checklist

  • Linked to an issue a maintainer has confirmed
  • Targets v2
  • Tests or docs updated where behaviour changed
  • No tool-generated co-author trailers or footers

@pournasserian pournasserian added this to the M1 Governance core milestone Oct 19, 2026
@pournasserian pournasserian added type:feature New capability v2 FluentCMS v2 (v2 branch) area:desk Review desk (DSK) labels Oct 19, 2026
@pournasserian
pournasserian merged commit 3a40875 into v2 Oct 19, 2026
2 checks passed
@pournasserian
pournasserian deleted the 2587-notifications-security branch October 19, 2026 06:14
pournasserian added a commit that referenced this pull request Oct 19, 2026
…ostgreSQL (#2642)

Closes #2635

## What

Follow-up from the independent review of #2590 (F-DSK, #2422). On
PostgreSQL, signed-in pages failed most of the time with "A second
operation was started on this context instance".

- **`NotificationLinks`** reads the header counts through a service
scope of its own. The layout renders while the page loads its data, and
the two used to share the request's `DbContext`. The caller is still the
request's, because the initiator comes from the `HttpContext`.
- **An index on `AuditEvents (Operation, Timestamp)`**, with migrations
for SQLite and PostgreSQL, generated after v2's latest. The security
view lists and counts a few operations across the whole log, and no
index covered `Operation`.
- **`CONTRIBUTING.md`** states the rule: a component that loads data
next to the page reads through a scope of its own.

## Why

- **A scope, not a `DbContext` factory.** The directories and the unit
of work share one context per scope by design (ADR-0005). Only
components that run beside the page need their own, and today that is
the header counts. #2639 applies the same rule to the picker's searches.
- **SQLite hid it.** Its async calls finish synchronously, so no host
test ever overlapped two queries. The new page test runs on both
providers.

## Tests

- `SignedInPageTests`, on SQLite and PostgreSQL: loads every signed-in
page five times, with a set awaiting review, an entry, an asset and
their pages. Without the fix it fails on PostgreSQL.
- Every SEC-08 event the code appends today is appended by the code that
owns it and read back from `/security` with the kind and name of who did
it, so a renamed operation fails a test:
  - `SecurityEventTests`, on both providers:
    - an agent over its calls cap, over MCP;
    - an agent over its items-per-set and sets-per-day caps, over MCP;
    - an agent refused a move it can't make;
    - a key issued and revoked;
    - a user created, disabled and enabled;
    - a routing rule created, revised, activated and deactivated.
  - `PasswordResetTests`: a reset link issued and used.
  - `ExternalSignInTests`: a sign-in linked through the provider.
  - `SecurityRows` reads the view's rows for all three.
- The header counts follow a set into review and out of it, and follow
security events as they are appended.
- The axe check covers `/security` with more than a page of events, so
the table, badges, links and pager are checked.

`dotnet build -warnaserror`, the unit and bUnit suite, the browser
tests, the integration suite on SQLite and PostgreSQL and markdownlint
pass locally. No new package and no new project reference.

## Checklist

- [x] Linked to an issue a maintainer has confirmed
- [x] Targets `v2`
- [x] Tests or docs updated where behaviour changed
- [x] No tool-generated co-author trailers or footers
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:desk Review desk (DSK) type:feature New capability v2 FluentCMS v2 (v2 branch)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Notification counts in the header and the security events view

1 participant