# Home-page KPIs of a client application / module (`INavigationStatsProvider`)

Every application and every module the client creates already gets a **presentation page** for free:
header, description, and one card per child, all derived from the navigation menu (which the API has
already filtered by permission, license and tenant catalog). Nothing to scaffold, nothing to
register — `DynamicRouter` emits it as the DEFAULT index of any node that has no page of its own.

What the menu cannot know is the module's **numbers**. That is this seam: one class, one DI line, and
the module's home page gains its KPI banner — with **ZERO client frontend code**.

```csharp
using SmartStack.Application.Common.Interfaces.Navigation;

public sealed class OrdersStatsProvider(IExtensionsDbContext context) : INavigationStatsProvider
{
    public string NodeKey => "ventes.commandes";                  // your module's componentKey
    public string? RequiredPermission => "ventes.commandes.read"; // the gate

    public async Task<IReadOnlyList<NavigationStat>> GetStatsAsync(CancellationToken ct) =>
    [
        new NavigationStat("pending", "Awaiting validation",
            await context.Orders.CountAsync(o => o.Status == OrderStatus.Pending, ct),
            Icon: "ClipboardCheck", Route: "/ventes/commandes/validation"),

        new NavigationStat("revenue", "Revenue",
            await context.Orders.SumAsync(o => o.Total, ct),
            NavigationStatFormat.Currency, "Coins", "this month"),
    ];
}
```

```csharp
// {Project}.Infrastructure/DependencyInjection.cs — the <<< NAVIGATION-STATS-DI >>> markers
services.AddSmartStackNavigationStatsProvider<OrdersStatsProvider>();
```

---

## Which page renders where

| Node | Children | What renders at its route |
|---|---|---|
| Application | ≥ 1 landable module | `ApplicationHomePage` — one card per module |
| Application | no landable module | AccessDenied (the filtered menu already implies it) |
| Module | ≥ 2 sections | `ModuleHomePage` — one card per section |
| Module | exactly 1 section | redirect into that section (a one-card page is a pointless click) |
| Module | 0 sections | redirect up to the application index |
| Any | a component registered on its `componentKey` | that component wins, always |

Two consequences for a generated app:

- A module whose root you scaffolded as a **hub page** (`SmartModuleHome` in the BA pagespecs) keeps
  that page — a registered component always wins. The KPI provider then has **no rendering surface**
  unless that page renders `<NavigationStatsBanner nodeKey="…" />` itself. Register a provider for a
  module whose root is generic, or render the banner explicitly on your hub page.
- A module with a **single** section never shows a home page, so a provider on it is dead weight.

## The contract

Mirror of `IMenuBadgeProvider` / `ITaskProvider` / `ISearchProvider`: the handler resolves
`IEnumerable<INavigationStatsProvider>` and runs the ones the caller is authorized for, so
registrations compose across the platform and every client package.

| Member | Meaning |
|---|---|
| `NodeKey` | componentKey of the application (`ventes`) or module (`ventes.commandes`) the KPIs belong to |
| `RequiredPermission` | permission the caller must hold (`PermissionMatcher`, wildcards included) |
| `GetStatsAsync(ct)` | the figures, honouring your DbContext's tenant / data-scope filters |

`NavigationStat(Key, Label, Value, Format, Icon, Subtitle, Route)`:

| Field | Meaning |
|---|---|
| `Key` | stable key within the provider — the SPA's i18n key (see below) |
| `Label` | English fallback, used when the SPA has no translation |
| `Value` | the raw figure; formatting is client-side (it knows the locale) |
| `Format` | `Number` (default), `Percent`, `Currency`, `Hours` |
| `Icon` | a lucide icon name |
| `Subtitle` | English fallback for the card's sub-label ("last 30 days") |
| `Route` | app-relative route the card links to; the SPA adds the tenant prefix |

The first card carries the accent stripe — order your stats headline-first.

## The three rules

**1. Permission-gate it.** A caller who may browse the module but not read its data gets **no
banner** — never an error, never a partial figure. `null` means "any authenticated caller" and is
reserved for **personal** nodes (`IsPersonal` applications, which bypass permission checks and have
no permission to gate on); a provider that returns null MUST scope its data to the caller itself.

**2. Let the query filters do the scoping.** Never call `IgnoreQueryFilters()`. When an entity has
**no** data-scope filter (a tenant-wide table read through a per-query resolver), gate the provider on
the widest read tier rather than publishing a figure the caller is not entitled to. For a table with
no tenant filter at all, narrow it explicitly through a filtered bridge table — and say so in the
provider's summary, so a reader knows which perimeter the number covers.

**3. Expect to be one of several.** Providers targeting the same node concatenate in DI registration
order. That is how an application banner adapts to the caller (register one small provider per
permission family rather than one all-or-nothing class), and how you **append** KPIs to a CORE module
— register on `administration.users` and your figures land next to the platform's.

A provider that throws is logged and skipped: one broken KPI never blanks a home page.

## Translating the labels

The SPA looks up `navigation:stats.{nodeKey}.{key}`, with the node key **flattened** (`.` → `_`), and
falls back to `Label`. The subtitle uses the same key suffixed with `Subtitle`.

```jsonc
// locales/fr/navigation.json  (and en / it / de)
{
  "stats": {
    "ventes_commandes": {
      "pending": "En attente de validation",
      "revenue": "Chiffre d'affaires",
      "revenueSubtitle": "ce mois-ci"
    }
  }
}
```

The flattening exists because i18next reads dots as nesting: keeping them would make the
APPLICATION-level stat `stats.ventes.commandes` collide with the MODULE namespace
`stats.ventes.commandes.*`.

## Enriching the page beyond KPIs

Quick actions and free-form widgets are frontend contributions, registered at module load (before
render), exactly like `PageTabRegistry`:

```ts
import { ModuleHomeRegistry, ApplicationHomeRegistry } from '@atlashub/smartstack';

ModuleHomeRegistry.register('ventes.commandes', {
  actions: OrdersQuickActions,   // rendered in the page header
  widgets: OrdersChart,          // rendered below the section cards
  stats: OrdersLiveBanner,       // OPTIONAL — wins over the API-driven banner
});

ApplicationHomeRegistry.register('ventes', { actions: SalesQuickActions });
```

`ModuleHomeRegistry.stats` is an **escape hatch**, not the normal path: use it only when your KPIs
cannot be expressed as plain figures (a chart, a live-updating widget). Everything else belongs on the
server, where it stays permission-gated. `ApplicationHomeRegistry` deliberately has no `stats` slot.

## Checklist

- [ ] `NodeKey` matches a real application/module `componentKey` (a typo yields a silent empty banner)
- [ ] the node actually renders a generic home page — otherwise the provider has no surface
- [ ] every `Route` points at a route your navigation seed declares (a card linking nowhere renders
      perfectly and only fails when a user clicks it)
- [ ] `RequiredPermission` set (or `null` only on a personal node, with caller-scoped data)
- [ ] no `IgnoreQueryFilters()`
- [ ] registered inside the `<<< NAVIGATION-STATS-DI >>>` markers
- [ ] i18n keys added to the four locales
- [ ] unit test on the provider (mock the context, assert the figures **and** the perimeter)
