Services
The Services view lists every Wolverine service that has registered with CritterWatch. From here you drill into a single service's detail page, which is the operational hub for one logical Wolverine application — its nodes, its agents, its message handlers, its endpoints, its tenants, its DDL, and the rest.
Services List
The list page renders both as a card grid (default) and as a sortable table. Use the toggle in the toolbar to switch.
Each card / row shows:
| Field | Meaning |
|---|---|
| Name | Wolverine ServiceName — unique within the cluster |
| Label | Human-readable display name; defaults to the service name and is editable on the service's Overview tab |
| Version | Wolverine version reported on the most recent VersionDetected snapshot |
| Status | Aggregate health roll-up — Healthy / Warning / Critical / Unknown |
| Nodes | Active process instances heartbeating in the cluster |
| Msgs/hr | Recent throughput across all handlers and endpoints |
| DLQ | Unresolved dead letters in this service's stores |
| Alerts | Open alerts (raised + elevated) attributed to this service |
Clicking a service opens its detail page. The detail page binds the active tab to the URL via ?tab=<name> so deep links and back-button navigation always land on the right pane.
Service Detail
The detail page is organised into seven always-on tabs and two conditional tabs. The conditional tabs (Tenants, HTTP) are only rendered when the service surfaces the relevant capability.
Overview · Storage · Durability · Cluster · Messaging · Metrics · Endpoints · [Tenants] · [HTTP]URL backward compatibility
Two reorganisations changed tab names; both keep their old URLs working so saved bookmarks don't 404:
Old ?tab= | Lands on |
|---|---|
?tab=messages | Messaging tab (renamed in #112) |
?tab=nodes | Cluster tab, Nodes view |
?tab=agents | Cluster tab, Agents view |
If a service has any critical alerts and no explicit ?tab= is set, the detail page opens on the Alerts tab so the operator sees the active incident first.
Overview Tab
The Overview pane is the at-a-glance identity card for the service. It is the only tab that always opens, and it is the home for editable service-level configuration.
Mini metrics dashboard
Five compact cards across the top:
| Card | Source |
|---|---|
| Msgs/hr | Total messages handled per hour (sent + received) |
| Avg Exec Time | Average handler execution time, milliseconds (excludes middleware) |
| Avg Effective Time | Average end-to-end processing time including middleware, serialization, and transport |
| DLQ/hr | Messages moved to the dead-letter queue per hour |
| Exceptions/hr | Handler exceptions per hour (includes retried + dead-lettered) |
Identity
A two-column descriptions table with:
Service ID — Wolverine
ServiceNameLabel — editable inline via the pencil-affordance
EditableLabelCell(#101). Click the label, type a new value, press Return or click away to commit. The change emitsRenameServiceLabeland is reflected immediately across every page that shows this service.Version — Wolverine version
Communication URI — how CritterWatch talks back to the service (typically
wolverine.signalr://for the BFF transport)Tenancy —
None,Single,ConjoinedTenancy,DefaultTenant, orExtendedDynamic. The Tenants tab is shown for any tenancy mode beyondSingle/None; the per-row actions (Add / Disable / Enable / Remove / Hard delete) are only enabled when at least one event-store isDynamicMultiple(#103 Pass 2a).Tenant ID Style (#103 Pass 2c) — surfaces Marten's
StoreOptions.TenantIdStylewhen the service has any tenancy. One of:CaseSensitive— tenant IDs stored exactly as supplied; case must match at session-open time.ForceLowerCase— tenant IDs silently lowercased on every reference.ForceUpperCase— tenant IDs silently uppercased on every reference.
When multiple document stores disagree the tag shows the first value with a
(mixed)suffix and a warning tooltip. Hidden when the service has no tenancy.
Health checks
A card on the Overview tab confirms whether this service is wired up to ASP.NET Core health checks (#73). Two states:
- Registered (green) — the service is exposing its Wolverine state to anything that asks via the standard
/healthzendpoint, with the registered check names and tags listed as chips. This is what Kubernetes liveness and readiness probes consume. - Not registered (amber) — the service has no Wolverine-aware health check registered. Without one, k8s and load balancers can't tell whether the runtime is up or jammed.
The card is hidden entirely when the monitored service is running an older Wolverine.CritterWatch build that doesn't ship the registration descriptor.
For the why and how, see Health Checks. The "Operator docs" link in the card goes there directly.
Trace provider binding
A card on the Overview tab lets the operator pick which configured OTel store CritterWatch queries when looking up traces for this service's saga instances and handler runs. The dropdown lists every provider configured globally (Settings → Trace providers); leaving it empty falls back to the default. The "Manage providers" link opens Settings.
Storage Tab
Wave 2 (#112) consolidated everything storage-shaped into one tab so operators investigating a "where is this document / event / message persisted?" question stop scrolling past four stacked sections at the bottom of Overview.
Message Stores
Per-store row with live persistence counts:
| Column | Meaning |
|---|---|
| Icon | Postgres / SqlServer / RabbitMQ / etc. |
| URI | Database / store URI |
| Role | Main (durability tables) or Ancillary (DLQ-only or per-tenant overflow) |
| Incoming | Persisted incoming envelopes (inbox) |
| Outgoing | Persisted outgoing envelopes (outbox) |
| Scheduled | Persisted scheduled-send envelopes awaiting their delivery time |
| Handled | Successfully handled envelopes still retained for idempotency (KeepAfterMessageHandling) |
| Dead Letter | DLQ count for this store. Click a non-zero count to drill into the Dead Letters page pre-filtered to this store + service. |
Wolverine Configuration snapshot
Reference data captured on each VersionDetected (i.e. once per deploy). Three sub-tables:
- Service Settings — top-level
WolverineOptions(default timeouts, listener kill switch, metrics policy, remote-invocation behaviour) - Durability — Operational — operational identity controls
- Durability — Polling & Cadence — reassign / health / scheduled-job intervals
- Durability — Retention — handled-message + DLQ + node-history retention windows
Each field has a tooltip pulled from XML doc comments via the auto-generated tooltip lookup; field descriptions stay in sync with the upstream Wolverine sources.
Document Stores & Event Stores
Per-store cards listing document mappings, registered projections, and DDL. The first Document Store (or first Event Store, if no document stores are present) is expanded by default — biased toward the most recently shipped surface. See DdlBlock for the syntax-highlighted DDL panel.
DbContexts
The third storage subsection — one card per EF Core DbContext registered in the service. Picked up automatically: contexts wired through AddDbContextWithWolverineIntegration get the full Wolverine-integrated picture, and any plain AddDbContext<>() registration is detected by the implicit-discovery hook in UseEntityFrameworkCoreTransactions, so untracked contexts still show up — just with the integration badges off.
Each card's title row carries a strip of badges. Read them in order:
- Wolverine — green when the model has been mapped through
MapWolverineEnvelopeStorage. This is the single most important badge to triage missing-publish issues. When green, your domain events publish atomically withSaveChangesAsyncbecause the envelope tables share the context's connection. When grey ("Plain"), Wolverine isn't driving this context's transaction at all — any messaging happens on a separate connection, which is correct but won't give you transactional outbox guarantees. - Tenancy —
Singlefor a single-database context (info color),ConnectionStringorDbDataSourcefor multi-tenant (warning color). Multi-tenant cards expand to show the per-tenant database list (server + database + tenantId only — never the raw connection string). - Tx —
Eager(green) when transactional middleware opens an explicit database transaction up front;Lightweight(amber) when atomicity is delegated toSaveChangesAsync. Absent when Wolverine isn't wrapping this handler in transactional middleware. - Domain events —
OutgoingDomainEvents(the per-handlerOutgoingDomainEventscollection is the source) orPerEntityType(one or more entity-level domain-event scrapers are registered). When PerEntityType, expand the card to see exactly which entity publishes which event type. - Outbox —
Mapped(green-path) means the envelope tables share this context's connection.ExternalConnectionmeans Wolverine's outbox factory is registered but the envelope tables live on a different connection — correct, but slower under load. Absent for plain contexts. - Wolverine-managed — present when the application is using
UseEntityFrameworkCoreWolverineManagedMigrations. Wolverine's resource-startup pipeline will create or migrate this context's database on its own. Without the badge, schema management is your responsibility (dotnet ef database updateor equivalent).
Below the badges, the Pending migrations field starts at "—" until you click Check pending migrations. The action is on-demand — clicking it runs IMigrator.GetPendingMigrationsAsync on the target service, opening a connection to the database and reading the __EFMigrationsHistory table. That's a synchronous round-trip per click, which is why we don't bake it into the regular capabilities snapshot. Inline error text appears under the field if the probe couldn't reach the database (transient connection failure, missing permissions, or a provider that doesn't support migrations).
Cards expand to a per-entity table — table name, primary-key columns, index count, foreign-key count, plus chips for sagas, owned types, and view-mapped entities. Saga rows cross-link to the Sagas tab so you can jump straight from "what does this context manage" to "what does this saga handle". Drill into an entity row for its full index and foreign-key detail.
Brokers
Configured messaging brokers (RabbitMQ, Kafka, Azure Service Bus, etc.) with each broker's declared queue / topic surface area.
Broker health columns (#70)
Each broker row carries a small health strip that surfaces the connection's actual condition rather than the configuration alone.
- Status — a traffic-light dot. Green when the connection is steady, orange when it has recently flapped (lost-and-recovered inside the last few minutes), red when it is currently down. Hover the dot for the probe's last description — typically the close-reason for an unhealthy connection or the recovery timestamp for a degraded one.
- Last successful — relative time since the most recent good probe (
2m ago,1h ago). Drift on this column is the leading indicator that something started failing in the background; if the row says "Healthy" butLast successfulis climbing past the probe cadence (60 s by default), the broker is flapping faster than the probe can resolve it. - Reconnects — running counter of times this transport has lost and re-established the broker connection since the host started. A single bump is harmless transient weather; sustained climbing is the signal something is actually broken — typically a misbehaving network path, an undersized broker, or expired credentials retrying. The counter renders bold-amber once it leaves zero.
- Cert expiry — the TLS certificate's expiry date when TLS is configured. The chip switches to amber within 14 days of expiry; the action is to rotate the cert before the broker starts refusing new connections.
A status of Unknown (grey dot, "—" for the timestamp) means the transport hasn't connected yet. That is normal during cold starts — the first probe pass runs ~60 s after the host comes up. If a row stays Unknown past the warm-up window, the transport is misconfigured or disabled; check the host's startup logs.
The same data is surfaced on the per-broker detail page (click the broker name) with extra room for the probe description and the reporting node number — useful when multiple nodes probe the same broker and you need to spot which one is seeing the issue.
Durability Tab
Live operator surface for the inbox/outbox tables backing this service.
The Durability tab table mirrors the Storage tab's Message Stores table but adds a clickable row — the URI cell drills into a dedicated Per-message-store detail page (#105) at /service/:id/message-store/:storeUri. The detail page is the per-store equivalent of this tab — recent inbox/outbox samples, DLQ counts, scheduled-job queue, and the DDL for that store's durability tables.
Use this tab when:
- A persistence count looks wrong and you need a per-store drill-in
- You want to see the actual DDL Wolverine has installed
- You need the per-store DLQ count without leaving the service page
The row backgrounds shade gold when an ?focus=<storeUri> query parameter is present — used by deep links from the alerts feed when a DLQ-rate alert fires.
Cluster Tab
#106 collapsed the standalone Nodes and Agents tabs into a single Cluster tab with a segmented view-mode toggle. Both views are full operational surfaces — switching used to lose scroll, filter, and selection state, so they now sit side-by-side under one parent.
Cluster view toggle
A radio group at the top of the tab switches between:
- Nodes — process-instance lens
- Agents — agent-assignment lens
The active view is URL-bound via ?view=nodes|agents so deep links and operator bookmarks survive remounts.
Nodes view
Cluster-wide actions
| Action | Effect |
|---|---|
| Trigger Election | Force a new leader election. Disabled while OperationsEnabled is false. Confirmation popconfirm. |
| Clear Node History (overflow menu) | Trim the node-history table; retains the 10 most recent records |
The destructive Trigger Election is the primary action; Clear Node History is demoted to an overflow kebab so an operator can't fat-finger it next to the election button.
Node view-mode toggle (#107)
A second toggle inside the Nodes view switches between:
- Cards — one el-card per node with a Leader pill, alert badge, last health check, version, agents-assigned count, and Eject button
- List — dense table with the same surface area for big clusters
Heuristic auto-flips to List at 6+ nodes; an explicit operator override is URL-bound via ?nodeView=cards|list for share-and-restore.
Per-node columns / fields
| Field | Meaning |
|---|---|
| Heartbeat dot | Liveness pill — green / amber / red / grey. See Heartbeat dot below |
| Node # | Wolverine node number; clickable — drills into the per-node detail page (#108) |
| Role | Leader pill if this node holds the leadership lease |
| Alerts | Per-node active-alert badge (#106 rollup) — click to focus the alerts list filtered to that node |
| Version | Wolverine version on this node |
| Uptime | Time since started; tooltip shows the wall-clock start time |
| Last health check | Relative-time short-form; tooltip shows the absolute timestamp |
| Agents | Count of agents currently assigned to this node |
| Actions → Eject | EjectNode(nodeNumber) — danger popconfirm, redistributes assigned agents |
Heartbeat dot
Each running service sends a quick liveness ping to CritterWatch every 30 seconds. The dot beside every node number turns the dot through four states based on the gap since the last ping:
| Colour | State | Meaning | What to do |
|---|---|---|---|
| Green | Live | Heartbeat received within the last 60 s. | Nothing — the node is healthy. |
| Amber | Stale | At least one heartbeat missed (60–150 s old). | Watch for a few more seconds — a single missed beat usually means a slow GC or a brief network blip. |
| Red | Silent | Five or more heartbeats missed (older than 150 s, or never received). | Open the per-node detail page. The node has likely hung, lost its broker connection, or been killed without graceful shutdown. Check that the service process is up, the broker is reachable, and the network between them isn't partitioned. |
| Grey | Unknown | The node has only just started and no heartbeat has arrived yet, or the service hasn't been integrated with heartbeats. | Usually fine — older deployments that haven't been rebuilt against a heartbeat-aware Wolverine still show grey. If the node has been around for several minutes and stays grey, the service may need to be redeployed against the current Wolverine.CritterWatch package. |
Hovering the dot reveals the exact Last heartbeat: <timestamp> line plus a one-sentence description of the colour, so operators don't have to remember the rules.
The thresholds are sized against the default 30 s cadence (two missed beats turns the dot amber; five missed beats turns it red). Services that ship a slower or faster cadence — supplied via the heartbeatInterval parameter on AddCritterWatchMonitoring — still use the same wall-clock thresholds today; we'll revisit per-service scaling when a real deployment asks for it.
Agents view
Agent-assignment lens — every registered agent (projection shards, subscriptions, leadership, durability, scheduled-message agents, etc.) with its current node and status:
| Column | Meaning |
|---|---|
| Agent URI | Unique identifier (e.g. wolverine://leader/, marten://projection/TripSummary:All) |
| Status | Healthy / Degraded / Offline |
| Assigned Node | Which node currently owns this agent |
| Last Seen | Time of most recent health report |
Per-row actions:
| Action | Effect |
|---|---|
| Pin | PinAgentToNode — keep this agent on a chosen node across rebalances |
| Unpin | UnpinAgent — drop the assignment override |
If a deep link supplies ?scheme=<scheme> (e.g. from the Topology page), the agent table is pre-filtered to that scheme regardless of which view the URL landed on.
Messaging Tab
#112 renamed this tab from "Messages" to "Messaging" — the previous name collided with the global Inbox / Outbox concept and operators kept landing here looking for envelopes. The tab actually catalogs handler / message-type configuration — what messages the service knows how to handle, and where it routes published messages.
Filters
A namespace dropdown at the top trims the table to a single C# namespace. Useful for huge handler graphs.
Columns
| Column | Meaning |
|---|---|
| Message Type | Short type name; tooltip shows the full namespace; clickable — drills into the per-message-type detail page |
| Handler or Destination | Three row kinds: • handler — single handler type• separated-handler — handler routed to specific endpoint(s)• destination — published-only, with the target endpoint(s) |
| Msgs/hr | Per-message-type throughput (sent or received depending on row kind) |
| Avg Exec | Handler execution time |
| Avg Eff | Effective time including middleware |
| DLQ/hr | DLQ rate for this message type |
| Exc/hr | Exception rate for this message type |
| Actions | DLQ drill-in + Replay All / Discard All overflow |
The Messaging tab is the standard entry point into the HandlerChainDetailPage — clicking the message type or handler type opens the per-handler page with the full source-code-view, middleware chain, and per-endpoint sticky routing.
Metrics Tab
Per-message-type metrics table, sortable. Same fields as the Messaging tab but without the routing column — pure throughput, exec, effective, DLQ, and exception counters. Useful when you don't care about which handler runs the message and you just want the hot-path list.
Endpoints Tab
The full listener + sender table for this service, with per-row lifecycle controls.
Filters
| Filter | Meaning |
|---|---|
| Search | Free text against URI, name, scheme |
| Endpoint type | Scheme dropdown — local, rabbitmq, kafka, sqlserver, etc. |
| Mode | BufferedInMemory / Inline / Durable |
A column picker (popover) lets the operator hide / show columns. The row-identifier (Endpoint URI) and the Actions column are locked — they can't be hidden.
Per-endpoint columns
| Column | Meaning |
|---|---|
| URI | Endpoint address (clickable; opens EndpointDetailPage) |
| Mode | Listener / Sender / Both |
| Status | Accepting / Stopped / TooBusy / Latched / Paused / Draining |
| Transport | RabbitMQ / Kafka / InMemory / SqlServer / etc. |
| Circuit Breaker | Active / Paused — failure-rate gating state |
| Back Pressure | None / Triggered |
Per-endpoint actions
| Action | Effect |
|---|---|
| Pause | PauseListener(uri) — hard-stops processing on the endpoint immediately. Existing in-flight work is abandoned. |
| Drain (#67) | DrainListener(uri) — graceful-shutdown. Stops accepting new messages, lets queued + in-flight messages finish, then halts. The right call before a deploy. |
| Restart | RestartListener(uri) — resumes a paused or drained endpoint |
| Edit Buffering Limits | UpdateEndpointBufferingLimits — adjust MaximumMessagesToReceive and related buffer thresholds. Drawer form. |
| Edit Circuit Breaker | UpdateEndpointCircuitBreaker — adjust failure-rate, sampling window, and pause duration |
Batch actions
When one or more endpoint rows are selected, the toolbar exposes Pause Selected and Restart Selected for batch lifecycle control.
The Drain action is intentionally per-row only — draining 30 endpoints at once is almost never the right move; if you really want that, the cluster-wide PauseAllListeners / RestartAllListeners commands exist for the all-or-nothing case (issued from the parent operations menu).
Pause / Drain / Restart — when to use which
| Scenario | Use |
|---|---|
| Hot-fix needs a stop-the-world | Pause |
| Pre-deploy graceful shutdown | Drain |
| Resume after either Pause or Drain | Restart |
| Circuit-broken upstream, want to throttle without stopping | Edit Circuit Breaker |
Tenants Tab (conditional)
Visible whenever the service has any tenancy mode beyond Single / None (#103 Pass 2a). The tab renders one of two layouts depending on cardinality:
Dynamic case (DynamicMultiple)
Editable list — operators add / disable / enable / remove / hard-delete tenants at runtime via the relay commands. Top toolbar:
- Refresh — re-issues
RequestTenantListto the service - Add Tenant — opens the tenant-creation drawer (
AddTenantcommand)
Per-tenant columns:
| Column | Meaning |
|---|---|
| Tenant ID | Tenant identifier as the application sees it |
| Database URI | Per-tenant database connection string |
| Executions (1h) | How many handler executions this tenant generated in the last hour. Quick read on traffic volume per tenant. |
| Failures (1h) | How many handler failures (transient exceptions) this tenant hit in the last hour. Renders red when non-zero. |
| DLQ depth | How deep this tenant's dead-letter queue is right now. Renders red when non-zero. |
| Status | Active / Disabled |
| Actions | Disable / Enable (mutually exclusive) · Remove · Hard delete |
The three traffic columns only appear once at least one tenant is publishing per-tenant metrics. If you don't see them, your monitored service is either single-tenant or its Prometheus scrape isn't emitting tenant_id labels yet (turn that on in the service's Wolverine + OpenTelemetry config). The columns disappear again the moment no tenant has data — they don't sit there showing dashes.
When to drill in
Click any tenant row to expand it. The expand panel shows two cards:
- Top message types — the top five message types this tenant is processing, ranked by execution count, with per-type failure and DLQ counts. Use this to answer "which handler is the noisy tenant hammering?" before paging the team that owns it.
- Durable counters — the full breakdown of this tenant's durable-store depth: incoming, scheduled, outgoing, handled, and dead-letter counts. Reach for this when the service-level Durability tab numbers look high and you want to know which tenant is parked in the inbox.
What "normal" looks like:
- Failures (1h) > 0 — single failures from a transient outage are normal; sustained non-zero across refreshes means the tenant is hitting a real bug or running with bad config.
- DLQ depth > 0 — anything above zero means messages are stuck. A tenant whose DLQ depth is climbing while the rest are flat is a classic noisy-neighbour signal.
- Executions (1h) far above the rest — usually fine on its own, but combine it with rising failures and you have a tenant that's about to start affecting the rest of the system.
Lifecycle
The full lifecycle a tenant goes through, from creation through permanent destruction:
- Register — the monitored service starts up with
MultiTenantedDatabasesWithMasterDatabaseTable(Marten) configured. CritterWatch sees theDynamicMultiplecardinality and renders the Tenants tab. - Add — operator clicks Add Tenant, supplies a tenant id + connection string. CritterWatch dispatches
AddTenant; the service writes a master-table row and (best-effort) creates the per-tenant database if it doesn't already exist. - Use — application code opens sessions against the new tenant id and reads/writes per-tenant data.
- Disable / Enable — soft toggles via
DisableTenantandEnableTenant. Disabled tenants stay in the master table but session-open against them throwsUnknownTenantIdExceptionuntil re-enabled. Useful for incident-time isolation without data loss. - Remove (
RemoveTenant) — drops only the master-table row. The per-tenant database is left intact for forensic recovery / backup. Reversible by re-runningAddTenantwith the same id + connection string. - Hard delete (
HardDeleteTenant) — drops the per-tenant database and removes the master row. Permanent; not reversible.
The end-to-end lifecycle is pinned by the multi_tenancy_lifecycle_end_to_end integration test (#103 Pass 1) so any Pass-2 UX change is forced to keep the underlying behaviour stable.
Tenant-id normalization (TenantIdStyle)
When the host sets StoreOptions.TenantIdStyle to ForceLowerCase or ForceUpperCase, both AddTenant and session-open run the supplied tenant id through MaybeCorrectTenantId before doing anything with it. Concrete consequence:
| Configured | AddTenant("Acme-Corp", …) writes | martenStore.LightweightSession("Acme-Corp") resolves to |
|---|---|---|
CaseSensitive (default) | Acme-Corp | the Acme-Corp tenant DB (or fails if you only added acme-corp) |
ForceLowerCase | acme-corp | the acme-corp tenant DB |
ForceUpperCase | ACME-CORP | the ACME-CORP tenant DB |
The normalization is silent — the operator's typed id is rewritten without a warning. The Pass-2c UI work uses this contract to inline-warn before the command leaves the browser. Under ForceLowerCase / ForceUpperCase, you cannot have two tenants whose ids differ only in case (the second AddTenant collides with the first).
Actions
| Action | Effect |
|---|---|
| Disable | DisableTenant(tenantId) — soft-disables; messages routed to the tenant queue without dropping data |
| Enable | EnableTenant(tenantId) — re-enables a disabled tenant |
| Remove | RemoveTenant(tenantId) — drops the master-table record only; the per-tenant database itself is not deleted |
| Hard delete (#68) | HardDeleteTenant(tenantId) — drops the tenant database and removes the master record. Permanent. |
See Wolverine.CritterWatch / Inbound commands for the full command reference.
Static / sharded case (StaticMultiple) — read-only (#103 Pass 2a)
Tenant lists configured at startup show as a read-only table. The Add Tenant button is absent and per-row actions render as a "Read-only" hint with the tooltip "This service uses static tenancy; tenants are configured at startup, not at runtime." Source: eventStores[*].database.databases[*].tenantIds, flattened and de-duplicated by tenant id; each row carries the DatabaseDescriptor.identifier (or databaseName) so operators see which DB the tenant routes to.
Add Tenant dialog — TenantIdStyle validation (#103 Pass 2c)
The Tenant ID input runs through the resolved TenantIdStyle policy client-side (mirroring JasperFx.MultiTenancy.TenantIdStyleExtensions.MaybeCorrectTenantId):
- Empty / whitespace-only input is rejected (the Add button stays disabled).
- When the input would be silently rewritten under
ForceLowerCase/ForceUpperCase(e.g.Acme-Corp→acme-corpunderForceLowerCase), an inline warning appears under the input. The Add button stays enabled — the operator can knowingly proceed; the warning surfaces the policy rather than enforcing a different one. - Under
CaseSensitiveno warning is shown; the id is stored exactly as supplied.
Hard-delete typed-id confirmation (#68)
Hard delete is gated behind a typed-tenant-id confirmation modal:
The standard "are you sure?" double-confirm is not enough friction for a database-drop. The operator must type the exact tenant id; only then does the dialog's primary action button enable. Audit log records every hard delete with the operator's identity (see Audit Log).
HTTP Tab (conditional)
Only rendered when the service surfaces any HTTP endpoint — either a Wolverine HTTP graph (Wolverine.HTTP is referenced), or non-Wolverine ASP.NET Core endpoints when the host opts in via services.AddCritterWatchHttp().
The tab is one merged surface — Wolverine HTTP chains and non-Wolverine endpoints share the same table with a Source chip per row. A filter strip above the table lets you narrow by source.
All Endpoints
| Column | Meaning |
|---|---|
| Source | Wolverine / MinimalApi / Mvc / RazorPages / SignalR / Other — color-coded chip |
| Method | GET / POST / PUT / DELETE / PATCH / OPTIONS — color-coded tag |
| Route | Route pattern. Multi-version Wolverine chains show one row with a chip per declared version (v1 / v2 / 2024-01-01) |
| Handler | Generated handler endpoint or display name (monospace) |
Click any row to drill into the per-endpoint detail page. Wolverine chains route to the HTTP chain detail page; non-Wolverine endpoints route to the ASP.NET endpoint detail page.
Above the table, source chips work as a multi-select filter; toggle the ones you care about. The free-text input on the right matches anywhere in the route, method set, or handler name.
A configuration block follows with the Wolverine HTTP graph's settings — global route prefix, antiforgery defaults, applied policies, middleware types, tenant-detection strategies. Tooltip per field, sourced from XML doc comments.
Coming soon: when a route fires a downstream message, you'll see chips on the row pointing at the Messaging-tab page for that message type. The Wolverine HTTP graph surfaces cascading messages on the chain detail page today; the row-level chip is the next step.
Detail-page drill-ins
Several tabs feed into dedicated detail pages with their own routes. Each is breadcrumb-anchored back to the parent service.
Per-node detail page (#108)
Route: /service/:id/node/:nodeNumber
Drilled into from the Cluster tab → Nodes view (clicking the node number cell in either Cards or List view). Surfaces:
- Node identity: number, version, started time, last health-check timestamp
- Heartbeat history graph
- Agents currently assigned to this node, with un-assign / pin controls
- Per-node alert list filtered to this node
- Eject control
Per-message-store detail page (#105)
Route: /service/:id/message-store/:storeUri
Drilled into from the Durability tab → message-store row click. Surfaces:
- Persistence counts (incoming, outgoing, scheduled, handled, DLQ)
- Recent inbox / outbox samples
- The actual installed DDL for that store's durability tables (rendered through
DdlBlock) - DLQ drill-in pre-filtered to this store
Endpoint detail page
Route: /service/:id/endpoint/:endpointUri
Drilled into from the Endpoints tab. Surfaces:
- Endpoint identity (URI, mode, transport)
- Live metrics (sent / received / failed / DLQ)
- Circuit-breaker + buffering settings (read-only — edit happens on the parent tab)
- Recent message samples
- The handler source code if the endpoint terminates in a handler
Handler chain detail page
Route: /service/:id/handler/:messageType
Drilled into from the Messaging tab. Surfaces:
- Message type identity (full type name, namespace)
- Middleware chain (in order)
- Generated handler source code with syntax highlighting
- Per-endpoint sticky-routing metadata
- Recent metric trend
Saga detail page
Route: /service/:id/saga/:stateType
For sagas declared in the service. Surfaces saga state shape, configured timeouts, retry policy, and a link to Saga Instances for active state rows.
Saga instances page
Route: /service/:id/saga/:stateType/instances
Live list of in-flight saga instances. Per-row: instance id, state, last update, message log.
Subscription / Projection / Broker detail pages
Subscription detail: /service/:id/subscription/:name Projection detail: /service/:id/projection/:shardName Broker detail: /service/:id/broker/:broker
Each surfaces the same shape — identity, config snapshot, recent activity, lifecycle controls (rewind, rebuild, restart). See the linked tabs for full coverage:
- Projections — Projections
- Subscriptions — Cluster tab → Agents view
- Brokers — Storage tab
DDL viewer
A few tabs render PostgreSQL DDL through the shared DdlBlock component. It uses highlight.js with the SQL grammar, light + dark themes, and a copy-to-clipboard affordance. Used on the Storage tab (Document Stores, Event Stores) and the per-message-store detail page.
Operations gating
Every action on the detail page that mutates state — Pause / Drain / Restart, Pin / Unpin, Eject Node, Add / Remove / Hard-delete Tenant, edits — is gated by the global "Operations enabled" flag. When disabled, the buttons render disabled with a hover tooltip explaining why. This is set in Settings → Connection settings, and is the recommended way to switch CritterWatch into a read-only mode for production.
