Skip to content

v1.0.0

Choose a tag to compare

@github-actions github-actions released this 11 Jul 18:50
· 141 commits to main since this release

[1.0.0] - 2026-07-11

Added

  • MCP Skill free-form instructions. Custom Instructions now reach every
    model-driven incident stage. OpsMender parses and preserves per-tier
    Markdown, injects only the active tier's guidance, and keeps structured tool
    policy, deny lists, approval requirements, and backend guardrails
    authoritative.

  • Deterministic autonomy demonstrations. The demo seed now includes
    self-contained Tier 0 autonomous remediation, Tier 1 approval-gated, and
    Tier 2 advisory-only sessions with persisted workflow and audit events; the
    README screenshots show each policy producing a distinct outcome without a
    live model or network dependency.

  • Standalone SQLite evaluation. opsmender serve now initializes a local
    SQLite schema from ORM metadata, stamps it at the bundled Alembic head, and
    starts in development mode for a zero-PostgreSQL product trial. The
    PostgreSQL production migration path and migration-skip flag remain intact.

  • Release verification gate. Added reusable pull-request and release-tag
    verification for backend lint/tests, empty-Postgres migrations, frontend
    lint/typecheck/tests/build, and a container health smoke test. Backend tests
    now deselect live integration cases and enforce a 180-second per-test timeout.

  • Platform-neutral health probes. Added dependency-free /health/live and
    role-aware /health/ready endpoints with database and migration checks, while
    retaining /health as the backward-compatible liveness alias.

  • Simplification sprint — Phase 5 Voice & SMS calling. Added admin
    Settings for workspace Voice/SMS calling, with encrypted auth-token storage,
    DB-over-env resolution, availability for My Routing, and disabled
    Voice/SMS routing options when calling is not configured.

  • Noise and analytics sprint — Phase 0 tier clarity. Approval rows now show
    the requesting session tier, the demo seed separates Tier 1 approval-gated
    sessions from Tier 2 advisory blocking, and team-scoped maintenance windows
    mark status-page components as maintenance.

  • Noise and analytics sprint — Phase 1 alert-noise intelligence. Services
    now support inherited/on/off similar alert grouping, grouped Incident badges,
    flapping detection with P0 safety behavior, and workspace default controls.

  • Noise and analytics sprint — Phase 2 analytics. Added read-only Noise
    and Response analytics APIs with CSV export plus a dashboard Analytics page
    for alert volume, noise reduction, MTTA, and MTTR trends.

  • Noise and analytics sprint — Phase 3 API tokens. Added named,
    revocable, role-scoped REST API bearer tokens with Settings management,
    one-time secret display, audit attribution, and self-service denylist
    protections.

  • A+ closure sprint — final audit proof. Added
    scripts/live_update_proof.mjs, a Playwright two-context acceptance script
    that proves the dashboard's live incident refresh updates within the
    two-second budget with one notification WebSocket and no skeleton flash.
    Sprint-end re-grade remains GATE PASS 0/0/0.

Changed

  • Production configuration validation. Production startup now rejects
    placeholder secrets, non-PostgreSQL database URLs, weak configured bootstrap
    passwords, and invalid autonomy tiers, with warnings for wildcard CORS,
    missing public URLs, and enabled API documentation.

  • Simplification sprint — Phase 3 Models. Services now expose their
    service-specific model list as Models, backed by services.model_config_ids,
    and the session model picker is restricted to active service Models plus the
    workspace default model.

Breaking

  • Explicit Skill tier policies. Every executable Skill operation must now
    declare complete T0/T1/T2 behavior; underspecified runtime policies fail
    closed and deny entries remain unconditional. Existing stored and imported
    classification-only operations are deterministically upgraded to the prior
    conservative behavior and surfaced for operator review.

  • Simplification sprint — Phase 4 MCP servers. Service MCP server lists are
    now strict allowlists, backed by services.mcp_server_ids; empty service
    lists and sessions without a service get no MCP tools instead of falling back
    to any configured server.

Removed

  • Workspace Status Page. Removed the public/private Status Page feature
    entirely — the /status page, the Settings section, the per-incident status
    update publishing, the status_page_* tables and Organization columns, and
    the related API. Stakeholder communication is served by scheduled reports; the
    separate Atlassian Statuspage integration connector is unaffected. Migration
    n1o2p3q4r5s6 drops the tables and columns (downgrade recreates them).

  • Simplification sprint — Phase 1 Agent Teams. Removed Agent Teams from
    the API, database model, Settings, session start flow, and AI workflow
    runner; sessions now use the single-agent path by design.

  • Simplification sprint — Phase 2 Session workflow. Removed the Session
    Profiles page and per-session profile picker; admins now manage one
    workspace-wide Session workflow from Settings, and every new AI session stamps
    that default workflow configuration.

Fixed

  • Session notification task lifecycle. Scheduled session chat
    notifications are tracked in the application background-task registry so
    shutdown can drain them cleanly and session-state transitions cannot race an
    unmanaged SQLite read transaction.

  • Role-aware dashboard loading. Viewer and operator dashboard routes no
    longer issue requests their roles cannot make, while workspace branding is
    readable by every member and protected mutations remain admin-only.

  • Session audit history and approval layout. Completed and paused sessions
    hydrate their event streams from persisted audit entries, the desktop work
    area retains usable height as context grows, and approval actions fit within
    the table at supported desktop widths.

  • Integration and notification-channel brand icons. Restored the ServiceNow
    mark in the integration catalog; added recognizable Microsoft Teams,
    Lark/Feishu, DingTalk, and WeCom marks to the notification-platform picker;
    and replaced the monochrome Slack glyph with its official four-color mark.

  • A+ closure sprint — A-8 README screenshot set. The screenshot capture
    helper now uses the design-audit credential pattern with environment
    overrides and .env bootstrap fallback, captures the four launch README
    surfaces into site/public/screenshots/, refuses to capture an empty
    approval inbox, and the README now embeds the current screenshots with
    product captions. Skill parsing now fails closed on non-mapping YAML roots so
    malformed skill records cannot 500 session-tier resolution.

  • A+ closure sprint — A-9 motion micro-polish. Toasts now ease in with a
    short fade/rise and fade out before removal, shared modals open with a
    conservative backdrop fade plus panel fade/scale, and globals.css now has
    a global prefers-reduced-motion: reduce kill-switch for existing and new
    animations/transitions.

  • A+ closure sprint — A-6 command-palette incident search. Cmd/Ctrl+K now
    debounces incident search through listIncidents({ q, limit: 5 }), renders
    an Incidents section with severity badges and relative created times, drops
    stale responses, fails silently, and lets keyboard selection traverse static
    nav/actions plus incident hits as one list.

  • A+ closure sprint — A-5 memory severity tags. Incident memory severity
    tags now canonicalize to severity-<level> across recall derivation, LLM
    writeback, manual memory create/update, and postmortem memory extraction;
    an Alembic data migration normalizes existing incident_memories.tags rows
    while preserving order and deduping duplicates.

  • A+ closure sprint — A-4 Activity grouping. Activity now defaults to
    collapsible session groups with short session IDs, View session links, time
    spans, entry counts, blocked-count chips, grouped Search/Type/Tier/Status
    filters, and a toggle that restores the previous flat DataTable with its
    date range, column picker, expansion, and CSV behavior intact.

  • A+ closure sprint — A-3 mobile approvals. Pending approvals now use
    thumb-sized, full-width Approve, Reject, and Redirect actions below sm on
    the session-detail card and Approvals page; action JSON collapses behind an
    accessible disclosure on mobile while reason and expiry stay visible, and
    the Approvals page renders stacked mobile cards instead of the desktop
    table.

  • A+ closure sprint — A-2 dashboard onboarding. The first-run setup
    checklist now lives only on the Dashboard, has a slim progress bar and
    explicit "N of 5 steps" status, refreshes on focus/live events, silently
    hides when unavailable or complete, and no longer duplicates itself on the
    Incidents page.

  • A+ closure sprint — A-1 live updates. Dashboard, Approvals,
    Orchestration, Incidents, the notification Inbox bell, and the sidebar
    Approvals badge now share one live notification WebSocket with
    category-filtered, debounced subscribers; manual and fire-test incident
    creation emit incident.created in-app events so existing silent refreshers
    update without loading flashes.

  • UI/UX Polish sprint (design-audit remediation) — Phase C: Reliability
    maintenance polish (P-15).
    Maintenance windows now cross-link to Paging
    maintenance windows, explicitly note that they suppress paging too, and
    populate the Targets column with "All Targets" or scoped target badges.

  • UI/UX Polish sprint (design-audit remediation) — Phase C:
    Settings/Profile (P-20).
    Settings now uses a compact pointer row for
    Models, MCP Servers, and Notification Channels; Organization save is
    bottom-right in its card, and the profile page is titled "My profile."

  • UI/UX Polish sprint (design-audit remediation) — Phase D:
    Inbox/preferences (P-18).
    Inbox rows now coalesce duplicate notifications
    with ×N chips and grouped read/delete actions, linked subjects render as real
    dashboard deep links, preference switches use the shared tracked switch
    styling, and quiet-hours timezone uses the offset-labelled IANA select.

  • UI/UX Polish sprint (design-audit remediation) — Phase D: heading effect
    policy (P-22).
    Dashboard headings are now covered by a regression test that
    forbids glitch/chromatic/text-shadow treatment in in-app pages and the shared
    PageHeader, preserving the auth-only visual decision.

  • UI/UX Polish sprint (design-audit remediation) — Phase D: Approvals
    zero-state copy (P-21).
    The default Pending empty state now explains that
    no approvals are waiting, All statuses keeps the onboarding/test-incident
    branch, and other status filters use filter-specific empty copy.

  • UI/UX Polish sprint (design-audit remediation) — Phase D: People detail
    (P-19).
    People detail display-name headers and Joined metadata are now
    covered by focused regression tests; the People list Joined / Sent column is
    covered as well.

  • UI/UX Polish sprint (design-audit remediation) — Phase D: Reports empty
    state (P-16).
    Scheduled reports now show the shared empty-state treatment
    when no schedules exist, with report-specific copy and a compact action that
    returns focus to the schedule form.

  • UI/UX Polish sprint (design-audit remediation) — Phase C: Sidebar IA
    (P-17).
    The global sidebar Paging group now exposes only On Call Schedule
    and the Paging hub. Detailed Paging setup remains in the hub tabs, and old
    nested Paging routes stay protected for admin/operator access.

  • UI/UX Polish sprint (design-audit remediation) — Phase C: Integrations
    layout (P-14).
    The Integrations page now shows configured connectors first,
    then a brand-icon kind gallery. The connector form stays collapsed until a
    kind is selected or an existing connector is edited, while preserving the
    existing field/API behavior.

  • UI/UX Polish sprint (design-audit remediation) — Phase C:
    Models/MCP/Skills (P-12).
    MCP runtime Error chips now explain themselves
    on hover/focus with last successful call, last error detail, and a "Run Test
    for details" hint. Earlier Phase C work already covered the shared header,
    action, provider-casing, duplicated Skills action, and labelled delete
    cleanup for these surfaces.

  • UI/UX Polish sprint (design-audit remediation) — Phase C: incidents list
    (P-7).
    The incidents list now shows one unclipped Last activity value with
    an absolute timestamp tooltip, and "Fire Test Incident" stays prominent only
    for empty/synthetic-only lists before moving into a More menu once real
    incident history exists.

  • UI/UX Polish sprint (design-audit remediation) — Phase C: keyboard entry
    (G-10).
    Dashboard pages now expose a first-focus "Skip to content" link
    that targets the main content region. Session activity streams use
    keyboard-focusable polite role="log" regions with accessible labels, while
    co-pilot chat panes remain labelled regions.

  • UI/UX Polish sprint (design-audit remediation) — Phase C: native
    form-control skinning (G-8).
    Shared date/time inputs and selects now carry
    theme-aware native chrome, including calendar indicators and select chevrons.
    DataTable rows-per-page uses the shared Select instead of a one-off raw
    native control, and Reports aligns its on-demand from/to range with matching
    7d/30d quick range buttons.

  • UI/UX Polish sprint (design-audit remediation) — Phase C:
    destructive-action pattern (G-7).
    Row/default destructive actions now use
    quiet ghost styling with icon labels and confirmations instead of filled-red
    buttons across config, paging, integrations, and reports. Busy action rows
    have stable minimum widths so they do not stack vertically. Filled danger
    styling remains reserved for confirmation-style flows.

  • UI/UX Polish sprint (design-audit remediation) — Phase C: button
    taxonomy (G-6).
    Creation controls now use "New ..." labels on the affected
    config and Reliability surfaces, with "New profile" replacing the wrapped
    "Add Session Profile" label. MCP Skills no longer repeats the same action
    row inside the empty state, model setup no longer duplicates the create
    action, and Reliability keeps one primary action by demoting maintenance
    window creation to secondary.

  • UI/UX Polish sprint (design-audit remediation) — Phase B:
    investigation cleanup (I-1..I-4).
    Services now resolve the "On call now"
    user from the service's linked escalation-chain calendar before falling back
    to a team roster, so service-level paging configuration is reflected in the
    list. Dashboard active AI sessions now flag stale long-running rows and share
    the same stale threshold as Orchestration. The Settings optional-config and
    Reliability stale-id paths remain verified clean in the full design-audit
    run.

  • UI/UX Polish sprint (design-audit remediation) — Phase B: scoped live
    data refresh (G-9).
    Approvals and Orchestration now distinguish initial
    loading from background refreshes, so 30-second/focus/visibility refetches
    keep existing rows on screen instead of flashing loading UI. Incidents now
    silently refetches AI-session badges on window focus in addition to its
    visible-tab interval and visibility refresh. Dashboard already used the
    same background-refresh pattern.

  • UI/UX Polish sprint (design-audit remediation) — Phase B: no machine
    strings (G-3).
    Added lib/displayNames.ts as the shared source for
    audit-entry labels, workflow-node labels, provider brand casing, generic
    identifier labels, and dashboard session primary labels. Session Profile
    phase chips now show operator-facing labels such as "Tier gate" instead of
    raw node ids, session-detail workflow events use the same labels, provider
    names preserve brand casing such as OpenAI/OpenAI-compatible/AWS Bedrock,
    and dashboard session cards no longer fall back to a short UUID as the
    primary title.

  • UI/UX Polish sprint (design-audit remediation) — Phase B: one date/time
    system (G-4).
    Every user-facing timestamp now routes through
    lib/formatDate.ts instead of ad-hoc toLocale* calls (there were three
    visibly different formats before). Added calendar-label helpers
    (formatMonthYear, formatDayLong, formatWeekdayDate, formatWeekday)
    so month pickers and weekday labels share the same source. Swept ~50 call
    sites across incidents, sessions, activity, people, skills, memories,
    postmortem, reports, integrations, reliability (incl. chart hover
    tooltips), paging, config, and roster calendar; consolidated three
    duplicated local relative-time helpers onto the shared formatRelative
    (which switches to an absolute date past 7 days — no more "215h ago").
    Seconds are dropped from timestamps. Only number formatters (row_count)
    and chart axis-tick formatters keep locale APIs, per the audit spec.

  • UI/UX Polish sprint (design-audit remediation) — Phase A.

    • Session detail no longer contradicts itself on ended sessions. A
      terminal session (cancelled/stopped/failed/timed-out/completed) previously
      showed a green "Live" pill, a running "Tier 0 time left 0:00" countdown,
      an "Initializing…" workflow state, and "Waiting for events…" all at once,
      with an open co-pilot input. Now every header/stream signal derives from
      the session status: terminal sessions show a neutral "Ended · {relative}"
      chip (no Live/countdown), the workflow strip freezes reached stages and
      shows a terminal label ("Cancelled — {summary}"), the empty event stream
      reads "No events were recorded for this session.", and the co-pilot input
      is disabled read-only (a message to an ended session was saved but never
      answered). Rollback is hidden for sessions that never executed
      (still-queued or cancelled-out-of-queue).
    • Co-pilot markdown renderer no longer leaks literal * markers from
      malformed LLM bold/italic (e.g. "*Severity: Medium"): bold matching is
      now resilient and orphan emphasis markers are stripped. Markdown list
      lines (* item / - item) render with a real bullet glyph and hanging
      indent instead of a raw star.
    • Incidents "Critical" tile is no longer alarm-red at zero — the number
      uses the critical color only when the count is > 0, matching the
      dashboard's value-based stat coloring.
    • Mobile page headers no longer overlap their actions. PageHeader
      now stacks the title over its action buttons below sm (they previously
      sat in a non-wrapping shrink-0 row that collided with the title at
      390px, e.g. Incidents' "Refresh" rendering under the heading). Incident
      stat tiles render as a 2×2 grid with tighter padding on phones instead
      of four full-height stacked cards.
    • Session detail no longer overlaps its panels on mobile. The event
      stream and co-pilot columns collapsed into a fixed-height grid at 390px,
      so the co-pilot header overlapped the event-stream summary and the chat
      input floated over the memories panel. On phones the two panels now stack
      as full-width blocks with a bounded height and their own internal scroll,
      in normal page flow; the fill-remaining-height behavior is kept at lg+.
    • Added lib/formatDate.ts (formatDate / formatDateTime / formatTime /
      formatRelative) as the single date/time formatting system; used here for
      the "Ended · {relative}" chip (call-site sweep continues under G-4).
    • On-Call Schedule: readable in light theme, on phones, and to screen
      readers.
      Calendar person-chips hardcoded dark-only classes
      (text-purple-100 on a translucent tint), so in light theme they became
      near-white text on pale tints — unreadable. lib/calendarColor.ts now
      carries a light: override per hue (dark text on a light tint), keyed
      off the app's [data-theme] via a new light: Tailwind custom-variant.
      Day cells no longer declare role="gridcell" without a row parent
      (cleared 42 aria-required-parent violations). On phones the 7-column
      month (unreadable at 390px) is replaced by an agenda list — one row
      per covered day with the date tappable to open details and the same
      clickable level chips — and the "Show times in" timezone picker no longer
      collapses to a zero-width box (given a min width; controls stack on
      mobile).
    • Muted-text contrast (WCAG AA). Raised --color-fg-muted from
      #6B7280 → #8A94A3 (dark) and #64748b → #55617a (light) in
      globals.css. The old values failed WCAG AA on panels, hover rows, and
      tinted light surfaces — the single largest source of accessibility
      violations app-wide (~1,100 nodes). Both new values clear ≥5:1 on every
      surface in their theme. No component-level gray text utilities exist
      (the token is used consistently), so this is the complete fix.
    • Accessible names for controls (G-2). The shared table controls now
      label date-range inputs and rows-per-page selectors; modal close buttons,
      quiet-hour toggles, session chat/send controls, Reliability detail's back
      button/custom range fields, People detail's role picker, profile password
      fields, and Skills row delete actions all expose explicit accessible
      names. Model status dots no longer put aria-label on non-interactive
      spans. The design audit now reports 0 axe critical nodes (0
      select-name, label, button-name, and aria-prohibited-attr nodes).

Added

  • On Call Schedule — full-depth calendar with per-person, timezone-aware
    shifts and direct on-call replacement.
    The team calendar got a major
    usability pass:

    • Cells show the real coverage picture with shift times. Every
      escalation chain renders as a caption with its L1/L2/L3 chips; each
      chip shows the level, the on-call person (color-coded), and that
      level's shift window in the roster's own time zone (e.g.
      L1 · on-call · 09:00–17:00 EDT). Overflow collapses to "+N more
      levels/chains"; tooltips carry the full detail.
    • Click a person to replace who's on call. Each roster-backed level
      chip is individually clickable (admin): it opens a "Replace who's on
      call"
      modal showing the chain/level, the roster, and the current
      person, with a Cover with picker (the current person excluded) and a
      Through date so one action can cover a single day or a whole span —
      the "X is out this week, Y covers" flow. Creates a roster override for
      the range. Direct user-target levels (not roster-backed) are shown but
      not reassignable.
    • Maintenance stays day-level. Drag across day backgrounds (or
      Ctrl/Cmd-click) to select days → Maintenance window… in the floating
      bar; non-contiguous selections create one window per contiguous run. The
      day detail modal also has an inline maintenance action.
    • Calendar edits are admin-only (previously admin/operator); operators
      and viewers get a read-only schedule.
    • Sidebar move: On Call Schedule now lives under Paging & On-call
      (still visible to everyone, read-only) instead of Incident Management.
    • Display time zone picker. A "Show times in" dropdown (left of the
      month nav) re-expresses every shift in a zone of the viewer's choosing —
      rosters store their own zone (usually UTC) and the calendar converts on
      the fly (e.g. a 09:00–17:00 UTC shift reads 04:00–12:00 CDT), with a
      ⁺¹/⁻¹ marker when a converted time crosses midnight. Defaults to UTC.
    • Backend: the team on-call-calendar payload's levels now include
      coverage_time_zone alongside coverage_start/coverage_end.
  • UTC offsets in every time-zone picker. IANA time-zone dropdowns now label
    each option with its current offset — UTC (+00:00),
    America/Chicago (-05:00), Asia/Kolkata (+05:30) — across the roster form,
    quiet-hours settings (upgraded from a free-text field to a proper picker),
    and the new calendar display-zone selector. Roster tables and the roster
    calendar modal show the offset next to the zone too. (New lib/timezones
    helpers: tzOffsetLabel, timeZoneOptionsWithOffset, convertWallTime.)

Fixed

  • Feishu/Lark outbound messages crashed with NameError. The Feishu
    connector's send_message called json.dumps(...) but json was only
    imported locally inside the inbound-parse method, so every outbound
    Feishu/Lark send raised NameError: name 'json' is not defined. The import
    is now module-scoped. (Found by the 2026-07-03 security/quality audit.)

Security

  • Full SAST / dependency / complexity audit (bandit, ruff F,B,S, radon,
    pip-audit) against the production Docker image. Remediations:
    • XXE hardening (CWE-611). Inbound WeCom/Weixin webhook XML (three
      attacker-reachable parse sites in bot_webhooks.py, wecom.py,
      weixin.py) now parses via defusedxml instead of stdlib
      xml.etree.ElementTree, guarding against entity-expansion and
      external-entity attacks. defusedxml added as a declared runtime
      dependency.
    • SHA-1 signature checks annotated. WeCom/Weixin callback signatures
      are SHA-1 by Tencent's protocol spec; marked
      usedforsecurity=False with explanatory comments (no behavior change).
    • pydantic-settings 2.14.1 → 2.14.2 (GHSA-4xgf-cpjx-pc3j; transitive
      via mcp, not exploitable here — bumped as hygiene).
    • Container image CVE scan (trivy). apt-get upgrade -y added to the
      Dockerfile runtime stage pulls the base-image security patches (fixes the
      3 libssh2 HIGH CVEs; re-scan shows 0 fixable CRITICAL/HIGH). Remaining
      OS findings are Debian fix_deferred (no upstream patch yet) and clear on
      base-image refresh.
    • Ollama URL scheme allowlist. OllamaProvider now rejects any
      OLLAMA_BASE_URL that isn't http/https, so a misconfigured value
      can't turn the client's urllib calls into file:/ftp: reads.
    • API docs no longer exposed in production. /docs, /redoc, and
      /openapi.json were served unauthenticated in production mode,
      disclosing the full API surface. They are now disabled when
      OPSMENDER_ENVIRONMENT resolves to production (development keeps them;
      OPSMENDER_ENABLE_API_DOCS=true opts back in).
    • Security response headers. Every response now carries
      X-Content-Type-Options: nosniff, X-Frame-Options: DENY, and
      Referrer-Policy: strict-origin-when-cross-origin (CSP/HSTS left to the
      operator's TLS-terminating proxy). Covered by new tests in
      tests/test_security_headers_and_docs.py.
    • Fixed an undefined AsyncSession annotation name in
      backend/api/session_runner.py (added a TYPE_CHECKING import).

Added

  • On Call Schedule (team calendar). A new viewer-visible month-grid calendar
    (Incident Management → On Call Schedule) shows who is on call for a selected
    team across all of its escalation chains, by level, color-coded per person,
    navigable up to ~12 months out. Clicking a day opens a per-chain / per-level
    breakdown. Global + team-scoped maintenance windows blank that day's coverage
    and show a maintenance marker. Backed by a new
    GET /paging/teams/{team_id}/on-call-calendar?start=&days= endpoint
    (readable by any authenticated user, including viewers). Admins/operators can
    click a day to create a coverage override (pick roster + covering person)
    or a team maintenance window (defaults to the full day); viewers get a
    read-only calendar.

Changed

  • Roster time zone is now a dropdown. The roster schedule form's "Time zone
    (IANA)" free-text field is replaced with a select of all IANA zones
    (Intl.supportedValuesOf("timeZone"), with a fallback list); a legacy/free-form
    saved value is preserved as the selected option.
  • People show real names, not usernames, across paging. Team member, roster
    rotation/override, and escalation "User" pickers (and the resolved on-call /
    escalation-target labels) now render the user's display name ("First Last",
    falling back to username when no name is set) via a shared displayName()
    helper.
  • Paging nav reordered to the build flow. Both the in-page tabs and the
    "Paging & On-call" sidebar now read Teams → Rosters → Escalation Chains →
    Services → Maintenance Windows → Notifications
    (rosters before chains, chains
    before services) so the order matches how you actually set things up. No
    behavior change; deep-link routes/ids unchanged.
  • Standardized on Node.js 24 (Active LTS). Previously the toolchain was split
    — Docker ran Node 22 while CI, @types/node, and the docs said Node 20 (which
    is now EOL). Everything is now Node 24: docker/Dockerfile (frontend-builder +
    node-runtime → node:24-*), .github/workflows/release.yml +
    deploy-site.yml (node-version: 24), frontend/package.json
    (@types/node ^24 + engines.node >=24), a new frontend/.nvmrc, and the
    README badge / CONTRIBUTING requirement. Verified: frontend build + 203 vitest
    pass; clean Docker build boots healthy with bundled node v24.18.0.
  • Raised the Python floor to 3.12. requires-python is now >=3.12 (was
    >=3.11); the 3.11 classifier is dropped and the mypy / ruff targets move to
    3.12 / py312 (Docker + CI already ran 3.12). uv.lock regenerated; README
    badge + CONTRIBUTING updated. Backend suite green (2020 passed, 2 skipped).