# The Dashboard

BackWave ships a built-in operations Dashboard: a set of server-rendered screens for watching your jobs and acting on them, with no separate service to run. It mounts onto your host's own ASP.NET Core pipeline as middleware, reads exclusively through the [Monitor](/docs/dashboard-operations/monitor-api), and writes exclusively through the audited [Operator](#what-operators-can-do). It is read-only the moment you mount it: viewing is allowed, but every write and every sensitive-data view is denied until you opt in.

This page is the whole story for running the Dashboard: mount it, secure it, understand what the operator actions actually do, and enable Pro Workflows. It does not catalogue every screen and column. The screens are self-describing; what follows is what you cannot see by looking.

## Mounting the Dashboard

You mount it with one middleware call, `UseBackWaveDashboard`, placed after your authentication and authorization middleware so the permission callbacks run against an authenticated request.

```csharp title="Program.cs"
builder.Services.AddBackWave(bw => bw
    .UseStore(new PostgresJobStore(connectionString))
    .UseJobs(BackWaveJobs.Module)
    .AddWorkerGroup(new WorkerGroupOptions
    {
        Name = "default",
        Policy = new DispatchPolicy.Strict("emails"),
    }));

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.UseBackWaveDashboard(); // served at /backwave, read-only by default

app.Run();
```

The first argument is the path prefix, defaulting to `"/backwave"` (it must start with `'/'` and be non-empty, or the call throws). The second is an optional `BackWaveDashboardOptions`; omit it and you get the safe read-only defaults. There is no separate `AddBackWaveDashboard()`. The Dashboard only needs `AddBackWave` (which registers the Monitor and Operator it resolves at request time) and, once you grant any write permission, a call to `AddAntiforgery()`. Grant a write without it and the action throws an `InvalidOperationException` at request time. A purely read-only Dashboard never touches antiforgery.

Every screen reads through the Monitor, so what the Dashboard shows and what your tests assert against the Monitor agree by construction. Every change goes through the Operator as an audited, antiforgery-protected transition, never a raw row edit.

## Securing it: permissions as delegation

BackWave owns no users and no roles. Every permission is a callback you supply that takes the request's `HttpContext` and returns `ValueTask<bool>`; BackWave never inspects identities itself, it asks your host whether the current request may do a thing. That single idea, authorization as delegation rather than ownership, is the whole model.

Reads default to allowed so the Dashboard works the instant you mount it (a local-dev convenience). Every write and the sensitive-content gate default to denied. Because a control renders only when its permission passes, a denied user does not see a disabled button. They see no button at all.

```csharp title="Program.cs"
builder.Services.AddAntiforgery(); // required once you grant any write

app.UseAuthentication();
app.UseAuthorization();

app.UseBackWaveDashboard("/backwave", new BackWaveDashboardOptions
{
    // Read gate for the whole Dashboard (default: allow). Set this in production.
    AuthorizeView = ctx => ValueTask.FromResult(ctx.User.IsInRole("ops")),

    // Write actions (default: deny). Map each to your own policy or role.
    AuthorizeRequeue         = ctx => ValueTask.FromResult(ctx.User.IsInRole("ops-admin")),
    AuthorizeCancel          = ctx => ValueTask.FromResult(ctx.User.IsInRole("ops-admin")),
    AuthorizePauseQueue      = ctx => ValueTask.FromResult(ctx.User.IsInRole("ops-admin")),
    AuthorizeTriggerSchedule = ctx => ValueTask.FromResult(ctx.User.IsInRole("ops-admin")),

    // Raw-content read gate (default: deny).
    AuthorizeViewSensitiveData = ctx => ValueTask.FromResult(ctx.User.IsInRole("ops-admin")),

    // Stamped into every audited action (default: user name, else "dashboard").
    ResolveActor = ctx => ctx.User.Identity?.Name ?? "automation",
});
```

The role checks are illustrative. To delegate to a named ASP.NET Core policy instead, resolve `IAuthorizationService` from `ctx.RequestServices` inside the callback and return `result.Succeeded`. The callback shape is the only contract.

| Option | Default | What it controls |
|---|---|---|
| `AuthorizeView` | allow | Whether the request sees the Dashboard at all. Checked first; a denial returns a 403 and zero bytes. |
| `AuthorizeViewSensitiveData` | deny | Whether raw content (payload, Job Output, Failure Detail) is shown. |
| `AuthorizeRequeue` | deny | The Requeue action. |
| `AuthorizeCancel` | deny | The Cancel action (and Pro's Cancel Workflow). |
| `AuthorizePauseQueue` | deny | Governs both pausing and resuming a Queue. There is no separate resume permission. |
| `AuthorizeTriggerSchedule` | deny | Triggering a Recurring Schedule now. |
| `ResolveActor` | user name, else `"dashboard"` | The actor string recorded in every audit record. Not a gate. |

There are four write permissions but five write actions, because `AuthorizePauseQueue` covers both pause and resume. `AuthorizeView` is the single entry door: it gates GET pages, POST actions, and the live stream alike.

### The sensitive-data gate

Raw content that may carry secrets or PII (job payload, Job Output, and inline Failure Detail) sits behind its own read gate, separate from `AuthorizeView`. Two conditions must both be true before a single byte is read: the per-request `AuthorizeViewSensitiveData` permission, and the host-wide `ExposeSensitiveData` flag (default `true`). Either one turns content off.

For an ops-side override with no redeploy, set the environment variable `BACKWAVE_DASHBOARD_DISABLE_SENSITIVE_DATA` to a truthy value (`1`, `true`, `yes`, or `on`); it forces exposure off regardless of the code flag. This gate governs what an operator *views*; it does not restrict your handler code from reading an ancestor's output in-process.

Every write action is stamped with the resolved actor and recorded in an append-only audit trail, readable programmatically through `BackWaveOperator.ListAuditRecordsAsync` for one target (a job id, Queue name, or schedule id).

## What operators can do

Reading the Dashboard tells you what the system is doing; an operator action changes it. Each is a defined state-machine transition, never a raw row edit, stamped with the acting identity and written to the audit trail. The Dashboard buttons are one way to invoke them; the same actions are a public API on `BackWaveOperator` (registered by `AddBackWave`, namespace `BackWave.Operations`), so a runbook can do exactly what an operator does by hand.

The behavior below is the part you cannot infer from the buttons:

| Action | What it actually does |
|---|---|
| **Requeue** | Valid only when the job is Dead-Lettered or Quarantined. Returns it to `Scheduled`, resets the attempt count to zero, and sets Due Time to now. The whole attempt budget resets, so the job starts over rather than getting one more try. Any other state is rejected unchanged. |
| **Cancel** | A not-yet-started job (`Scheduled`, `AwaitingParent`) is cancelled immediately. A running (`Leased`) job cannot be stopped by force. Cancel is cooperative: it sets a flag, and the handler's `CancellationToken` fires on the next heartbeat. Threads are never killed; the job reaches `Cancelled` when the handler winds down. |
| **Pause / Resume Queue** | Pausing stops every Worker in the cluster from claiming new work from that Queue; jobs already running keep running. Resume reverses it. Cluster-wide, no precondition; a no-op still writes an audit record. |
| **Trigger Schedule now** | Mints one instance right now, due immediately. The schedule's cursor and recorded ticks are untouched, so its normal cadence fires exactly as it would have, a one-off alongside the schedule rather than a shift of it. |

```csharp title="IncidentResponder.cs"
using BackWave.Operations;

public sealed class IncidentResponder(BackWaveOperator backwave)
{
    public async Task RecoverAsync(Guid deadJobId, string queue)
    {
        await backwave.RequeueAsync(deadJobId, actor: "alice@corp");   // dead -> Scheduled, attempts reset
        await backwave.CancelJobAsync(deadJobId, actor: "alice@corp"); // cooperative if running
        await backwave.PauseQueueAsync(queue, actor: "alice@corp");    // stop future claims, cluster-wide
        await backwave.ResumeQueueAsync(queue, actor: "alice@corp");
    }
}
```

Each write runs a fixed sequence: the permission is checked (403 on deny), the antiforgery token is validated (400 if missing or invalid), the audited action runs, and the browser is sent a 303 redirect back so a refresh never re-submits. Editing a payload is deliberately not an operator action.

## Live refresh

The at-a-glance screens refresh in place over a Server-Sent Events connection, so figures stay current without reloads. The server re-reads the Monitor on a fixed cadence and pushes an update only when the rendered screen actually changed. `LiveRefreshInterval` controls that cadence (default 4 seconds); raise it to ease load, lower it for a snappier feel. Live refresh is progressive enhancement. Without it, the server-rendered page still stands on its own.

```csharp title="Program.cs"
app.UseBackWaveDashboard("/backwave", new BackWaveDashboardOptions
{
    LiveRefreshInterval = TimeSpan.FromSeconds(10), // default is 4 seconds
});
```

## Pro: Workflows on the Dashboard

The free Dashboard ships with no Workflow UI. The BackWave Pro dashboard package adds a Workflows list and an interactive graph view of a single [Workflow](/docs/core-concepts/workflows)'s members, contributed through the Dashboard's extension seam without the free Dashboard depending on it. Enable it with one registration, `AddBackWaveProDashboard()`, called after `AddBackWave(...).AddBackWavePro(...)`:

```csharp title="Program.cs"
builder.Services
    .AddBackWave(/* ... */)
    .AddBackWavePro(builder.Configuration["BackWave:ProLicense"]);

builder.Services.AddBackWaveProDashboard();
```

There is no Pro-specific mounting step; the free Dashboard is still mounted the one way it always is. The Workflows nav entry slots in after Failures, the Cancel Workflow action reuses the existing Cancel permission (no new permission), and member payload and output reuse the same sensitive-data gate as everywhere else.

The Workflows surface appears because the package is registered, not because the license is valid. The license drives exactly one thing: a soft amber banner when the license is missing, malformed, or out of term. Every Pro feature runs identically regardless, because the software cannot and does not detect revenue. Pro is free for organizations under $1M annual revenue on the honor system; the banner is a reminder, not a restriction.

## Where to go next

- [The Monitor API](/docs/dashboard-operations/monitor-api): the read surface every screen renders, for building your own views, alerts, and tests.
- [Health, Fail-Stop & Draining](/docs/dashboard-operations/health-and-fail-stop): what happens at the runtime level when a node dies or shuts down.
- [Scaling & Throughput Tuning](/docs/dashboard-operations/scaling-and-throughput-tuning): `PoolSize`, `Pumps`, and when to add nodes.
- [Retention & History Purge](/docs/dashboard-operations/retention-and-purge): how long jobs and transitions are kept before purge.
- [Workflows](/docs/core-concepts/workflows): the Pro grouping the Workflows surface visualizes.
