---
title: "Operations Command Center"
slug: operations-command-center
section: Governance & Operations
status: stable
description: "The processor-facing landing page — a composed view of queue health, SLA risk, bottlenecks, and team workload, replacing Hanover exports and Excel pivot tables with real-time, role-scoped widgets."
last-updated: 2026-10-07
---

# Operations Command Center

The processor-facing landing page — a composed view of queue health, SLA risk, bottlenecks, and team workload, replacing Hanover exports and Excel pivot tables with real-time, role-scoped widgets.

> **SHIPPED — live at /command-center (verified 2026-07-04)**
>
> The Command Center is implemented in Globus (Tasks #1246–#1356): route /command-center (not /ops as the original shell ticket proposed), shell client/src/pages/command-center/index.tsx, feature-flagged via isCommandCenterEnabled(). Shipped tabs are a superset of the five widgets specced below: overview, attention, readiness, pipeline, track-stages, queues, team-workload, sla-risk, documents, compliance, eligibility. Live endpoints in server/routes/ops/index.ts: GET /api/ops/queue-health, /api/ops/kpi-cards, /api/ops/sla-risk?dimension=country|program|season|partner|processor (+ /sla-risk/applicants), /api/ops/top-bottlenecks, /api/ops/overview-summary, /api/ops/readiness-rollup (+ /applicants), /api/ops/sla-monitor (?format=csv), /api/ops/unowned-hosts — mostly gated dashboard.view. Scoping shipped exactly as specced: scopeApplicantsForCaller / resolveOpsCallerScope (server/services/queue/scopeBuilder.ts) enforced by the ESLint rule no-unscoped-ops-query. One RBAC correction to the tables below: there is no processor_manager role — the manager gate is the queues.manage permission (processor-dimension SLA risk 403s without it), and the team-view default gate is applicants.list.view, deny-by-default to self-assigned. The widget contracts and KPI definitions below remain the design reference; the P16 ticket appendix is retained as the historical build record, not future work. The Readiness tab now surfaces the global `ready_for_sevis` gate and program add-on gate outcomes (e.g. Teach USA and the Camp Counselor CAMP-001 / CAMP-002 / CAMP-003 gate) plus the sevis-readiness-blocked queue (Aug 11 2026 release capsule; see the SEVIS Readiness page). The visible tab strip is grouped into five operational areas (Task #2473): **Overview**; **Work** (Work, Browse Queues); **Pipeline**; **Reviews** (Documents, Compliance, Eligibility); **Management** (SLA Risk, Team Workload, Bottlenecks, Progress Milestones). The child tab keys above (overview, attention, queues, readiness, track-stages, …) are unchanged, so KPI sets, saved preferences and `?tab=` links keep working; legacy aliases such as `?tab=lifecycle`, `sla` or `bottlenecks` are redirected to the canonical child tab while preserving scope and filter parameters. A tab is hidden from staff who lack its permission: Team Workload needs `queues.manage` and Eligibility needs `compliance.console.view`.

## What This Page Specifies

Applicant-processing staff today land on the generic Queue Workspace and rebuild their view from filters every morning. The Applicant Processing Workspace epic asks for an “operational headquarters” instead — a role-aware landing page that answers two questions at a glance:

Question one

“What requires action now?”

Queue Health, KPI Cards, Top Bottlenecks, SLA Risk — all answer this.

Question two

“Find an applicant”

Already covered by the Queue Workspace and global search — not duplicated here.

## Composition, Not Duplication

> **Every widget reads from existing queue-system primitives**
>
> The Operations Command Center is a composition surface. Each widget reads from primitives already documented in /docs/queue-system and /docs/applicant-lifecycle — rules-engine outputs, SLA timer events (queue_sla_events), assignment state (applicants.assigned_staff_id), queue-reason rows (queue_reasons), and queue memberships. No widget introduces a new aggregation table, a new event vocabulary, or a new derivation rule. Every panel is a new projection of an event stream that already exists.

Cross-references for the underlying primitives:

- SLA timer infrastructure — /docs/queue-system#sla-data-model (queue_sla_events, warning / breach thresholds, On Track / Warning / At Risk / Overdue).
- Bottleneck detection & workload distribution — /docs/queue-system#bottleneck-detection and /docs/queue-system#workload-distribution .
- Queue Reason object — /docs/queue-system#queue-reason-object (the structured rows the Top Bottlenecks panel ranks).
- Lifecycle phases & assignment model — /docs/applicant-lifecycle (canonical phase set; applicants.assigned_staff_id).
## RBAC & Scoping

The Command Center is staff-only. Role visibility and the scope applied to every widget are summarised below. Scope semantics (country / program / season / partner ownership) follow the role-group rules defined in /docs/rbac .

| Role | Widgets visible | Scope applied to every widget |
| --- | --- | --- |
| processor | Queue Health, KPI Cards, Top Bottlenecks, SLA Risk | Applicants the processor is assigned to, plus unassigned applicants inside the programmes / seasons they have read access to. |
| processor_manager | All five (adds Team Workload) | Every applicant under the manager’s span (the processors they manage and the programmes / seasons those processors cover). |
| country_director | All five | Every applicant in the director’s country, across every programme and season. |
| compliance_officer | All five (read-only — no re-balance action on Team Workload) | Global, by definition. Compliance officers see every scope; widget queries skip the scope filter. |
| applicant_portal_user / partner / host | None — page returns 404 | Not applicable. The Command Center is not part of any external portal surface. |

> **Scope is applied at the query, not the renderer**
>
> Every Command Center endpoint runs through a single scopeApplicantsForCaller helper (delivered by the RBAC ticket below). The renderer is never trusted to filter — if a row reaches the widget, the caller is allowed to see it. `scopeApplicantsForCaller` and `scopeApplicationsForCaller` also build a condition from the caller context's `applicantFilters` using `buildOpsApplicantFilter`. `resolveOpsCallerScope` parses `applicantFilters` from the request query with `parseOpsApplicantFilters` and includes them in the returned caller scope when parsing yields filters.

## Page Performance & Freshness

The freshness contract names the target without prescribing a framework. Implementation teams pick the websocket / SSE / push transport that fits the Globus core stack.

- Initial load: every widget re-reads its source primitives on mount. No widget reads from a stale cache older than the request itself. p95 first-paint target: under 1.5 seconds at the typical processor scope (a few thousand applicants).
- Live updates: widgets poll their endpoints with react-query `refetchInterval` (30 s for most widgets, 60 s for the KPI strip and the unlinked-host widget). There is no websocket subscription in the Command Center.
- Refresh coupling: there is no per-event refresh. Queue Health, KPI Cards, SLA Risk, Top Bottlenecks and Team Workload each re-fetch on their own polling interval, and no `queue_memberships` table exists (queue membership is computed live).
- Mobile responsiveness: originally deferred to v2. As of Sep 14 2026 the shell contains its own overflow on narrow viewports (tab strip scrolls within its track with an overflow menu, controls wrap, no horizontal page scroll); the desktop layout remains the primary surface.

## Shell Controls — Freshness, Customize, Filters (Sep 2026)

Shipped Sep 7–14 2026. The page-level controls were consolidated into one shell row so every tab shares the same freshness, layout and filter behaviour instead of each widget carrying its own. This supersedes the earlier per-widget refresh buttons and the freshness label that lived inside the Applicant Summary briefing header.

- **Freshness bar (extracted control).** A single `ops-freshness-bar` shows *Not loaded yet* / *Updated just now* / *Updated Ns ago* / *Updated Nm ago* / *Updated Nh ago* for the whole Command Center and one **Refresh Command Center data** button that re-fetches every Command Center query at once (the icon spins while any query is fetching). The bar is a standalone component reused by the shell; no widget owns its own timestamp any more.
- **One Customize control.** A single **Customize** (layout customizer, "Customize Command Center") replaces separate per-area layout dialogs. It has two sections: **Overview sections** (show / hide / order the Overview widgets; the partner-facing section appears only for callers holding `partners.view`) and **"(Active tab) KPIs"** (per-tab KPI card selection). Preferences are saved per user; **Reset to default** restores the Overview layout and the active tab's KPI set. When a tab has no KPI definitions the dialog says so rather than showing an empty list.
- **Classification and Assignee filter chips.** Active filters render as removable chips beneath the controls. The classification filter now renders embedded in the toolbar's **More filters** popover, and the **Assignee** multi-select is its own toolbar field. Deep links built from the current scope include `sameCampReturn` in the URL when it is set. When a deep link's query string is read, the stage comes from `pipelineStage`, falling back to `stage`, and the readiness comes from `readinessState`, falling back to `readiness` when `readinessState` is absent.
- **Filter toolbar.** The shell renders `OpsFilterToolbar` for the active tab. The toolbar and the KPI strip are not rendered on the Pipeline tab when the `pipelineView` search parameter is `steps`. The toolbar holds a **Programme** selector, an **Operational cycle** selector, the **Assignee** filter and a **More filters** popover. On the Overview, Queues and Team Workload tabs it also shows a note that assignee and applicant filters narrow applicant data only, and that host and job-order queues use programme context while operational cycle and applicant filters do not narrow those entity lists.
- **Global assignee filter.** Narrows every Command Center KPI and Overview widget to one or more canonical staff owners. Query/URL key `assigneeIds` (comma-separated user UUIDs; the shareable URL carries it). Multiple owners combine with **OR**; the reserved value `unassigned` includes applicants with no canonical staff owner. Malformed values are ignored (never a query error). The result is always **ANDed** with the caller's program / season / organisation scope from `resolveOpsCallerScope` — the filter can only narrow, never widen, what the caller may see. The picker's options (`GET /api/ops/assignees`) are restricted server-side to active staff represented in the caller's already-visible applicant population. The picker's option query key spreads the scope and overrides `assigneeIds` with an empty string, so the option catalogue does not filter itself down to the selected owner. While the options load the picker shows a loading placeholder; if they cannot be loaded it shows an "Assignees could not be loaded." alert with a **Retry** button, and when no options match it shows "No matching assignees. Try another name." It is distinct from the Work tab's local assignee filter.

### Applicant Progress (Operational Position) Widget

Refined Sep 2026. The Overview's applicant-progress widget ("operational position") groups the pipeline into three lifecycle columns (**Move toward placement** → **Secure the placement** → **Prepare for departure**: "SEVIS cleared and getting ready to travel") rendered as a connected three-up strip on large screens and a stacked list on small ones, each row showing the count and a one-line description and linking to the filtered list. Exceptions (Self-Arranged Placement Pending and Needs review) render in a separate dashed-border block beneath the strip, and a **Beyond placement — for continued program tracking** section (In Program, Completing, Completed) covers post-placement stages. A **View full applicant progress** action opens the full applicant list with the same filters applied. Counts obey the global assignee and classification filters above.

### Needs Attention Today (Overview)

The Overview's "Needs Attention Today" panel lists up to 8 of the most pressing in-flight applications in the caller's scope — critical first (blocked, escalated or breached), then longest waiting — backed by `GET /api/ops/overview-summary` and excluding actively snoozed attention items. Each row shows the applicant and short code, the reason (SLA breached, Open escalation, Readiness blocked, Documents pending review, or the recorded next-action label) with a one-line explanation, the time in stage, a specific next action, and a screening worklist summary. Live signals outrank stale guidance: a persisted `on_track` next action is ignored, so a breached, blocked, escalated or pending-document application never shows an "on track" action. Clicking a row follows the application's recorded internal next-action deep link, falling back to the applicant profile (`/applicants/:id`); external or malformed links are rejected.

**Attention-tab triage state.** The Attention tab keeps per-application triage state in `attention_item_states` (one row per application). Snooze is shared: `POST /api/queues/attention/:applicationId/snooze` with a `duration` of `1d`, `2d` or `1w` records who snoozed and until when, hides the row from the default Attention view and from Needs Attention Today, and the row wakes early if the item escalates past the category rank or SLA state it had when snoozed; `POST .../unsnooze` clears it. Acknowledge does not hide the row: `POST .../acknowledge` (optional `reasonCode`) dims it, and the acknowledgement lapses by itself once the item's primary reason no longer matches the reason snapshotted at acknowledgement; `POST .../unacknowledge` clears it. `first_surfaced_at` is stamped once, the first time the application appears in the feed, and backs the per-viewer "New since last visit" badge together with `POST /api/queues/attention/visit`, which records the caller's visit time in `attention_tab_visits` and returns the previous visit. All five routes require `dashboard.view` (or an API key with the `dashboard.read` scope) and write an audit entry (`queue.attention.snoozed`, `.unsnoozed`, `.acknowledged`, `.unacknowledged`, `.visited`); the four per-application routes reject a non-UUID application id with 400 `VALIDATION_ERROR`.

### Eligibility Tab

The Eligibility tab (Reviews area) surfaces the dedicated eligibility engine (`eligibility_assessments` joined to their application), not the generic Rules Engine V2 records. `GET /api/admin/eligibility/pool` returns counts and rows for four buckets — Eligible, Needs Review, Blocked and Waitlisted (the waitlist is capacity-backed but each entry is matched to its latest application). Counts are computed over the full population and rows are capped by `rowLimit` (default 200, max 500). The tab and the pool endpoint are gated on `compliance.console.view`, the same permission the detail endpoint requires; determination mutations stay on the separate `eligibility.approve` gate. Session callers get the full operator scope (brand / program / season plus country and processor self-assignment) applied to both the assessment and waitlist queries. Every row and every drill-sheet entry deep-links to the exact application on the applicant Compliance tab (`…&section=compliance`), where the eligibility assessment card shows the qualifying-path and sponsor-determination detail. The `/api/eligibility/applications/:id` routes resolve the application and check caller scope in one query, so a missing application and an out-of-scope one are indistinguishable.

### My Alerts

The `ops_dashboard_alerts` table holds alerts written by the SLA escalation dashboard-alert action (`alert_type` `sla_breach`) and by the queue escalate action (`queue_escalation`). Its columns are `alert_type`, `message`, `severity` (default `warning`), the polymorphic `related_entity_type` and `related_entity_id`, `sla_event_id`, `application_id`, a global `is_dismissed` flag with `dismissed_at` and `dismissed_by_user_id`, and `created_at`. `GET /api/my-alerts` (permission `dashboard.view`) returns the caller's alerts that are not globally dismissed, not personally dismissed, on applications whose applicant is assigned to the caller and inside the caller's program and season scope, newest first, together with the caller's open escalation tickets, `activeCount` and `overdueThresholdDays`. `POST /api/my-alerts/:id/dismiss` records a personal dismissal in `ops_alert_dismissals` (idempotent, 404 for an unknown alert) that hides the alert only for that user, and `DELETE /api/my-alerts/:id/dismiss` restores it.

### Pipeline Matrix — Column Sorting and Applicant Last Activity

The pipeline matrix column headings are sort controls. Selecting a heading sorts by that column ascending; selecting a column that is already sorted ascending switches it to descending, and every sort change returns the matrix to page 1. The sortable columns are Applicant / application, Applicant last activity, Applicant completion, Placement source, Screening, Notes, each workflow step column and, for a collapsed phase, its **Completed** column. The default sort is `applicant` ascending, and the chosen sort is sent with the matrix request as `sortBy` and `sortDirection`.

A new **Applicant last activity** column shows each row's `lastApplicantStepActivity`.

The matrix header has an **Expand view** button that changes to **Exit expanded view** while the workspace is expanded. The column display mode can be set to All steps, Selected phase or Selected steps, a **Screening** checkbox sets the `showScreening` view option, and the matrix view state is written to `sessionStorage`.

The matrix's note detail dialog shows full note bodies rather than table previews. It shows the pinned note (labelled **Pinned latest note** when it is the same note as the latest, otherwise **Pinned note**) and the **Latest note** (omitted when it is the pinned note). When the row has a last-finance-note value, it adds a **Last finance note** section that reads "No finance note" when the value is `null` and "Finance note unavailable" when it is present without content.

The search field's placeholder reads "Legal/preferred name or application reference", and each applicant name cell also renders `preferredNameLabel(row.firstName, row.preferredName)`. The "No matching applications" empty state also renders an `ApplicantSearchRecovery` block.

When the matrix request fails with a 400, the guidance names Completion, workflow step, phase and Screening sorts (and step/status filtering) as requiring 500 or fewer candidates, whereas Applicant, activity, placement and Notes sorts alone have no candidate limit. If a sort other than `applicant` or a status filter is active, the error panel also offers **Sort by applicant instead** (or **Clear status filter and sort by applicant** when a status filter is set), which resets the sort to `applicant` ascending on page 1 and, when a status filter is set, also clears the status and step filters.

## Historical Build Record — P16 Tickets (implemented)

These tickets have been implemented (Globus Tasks #1246–#1356; see the shipped callout at the top for the live routes). They are retained as the historical build record of the widget contracts — do not re-file them. Where the shipped code diverges (route /command-center instead of /ops; permission gates instead of the notional processor_manager role), the shipped behaviour wins.
