ExklusivBeta

Alert Phase <mv-alert-phase>

Ein Incident-Status, dessen Schweregrad mit der Zeit abklingt, statt zwischen Rot und Grün zu springen. Signale (signal(level, { reason }), ein Klick auf einen beliebigen [data-alert-phase-signal]-Auslöser oder per listen zugeordnete DOM-Events) heben ihn auf Alert, Elevated oder Caution; jede Phase hat ihren eigenen sichtbaren Countdown (einen sich leerenden Ring und im Panel einen Abklingpfad bis Clear) und sinkt, wenn sie ohne neues Signal abläuft, um eine Stufe: Alert → Elevated → Caution → Clear. Ein Signal derselben Stufe setzt die Phase neu, ein höheres eskaliert sofort über das abbrechbare mv-escalate (Wartungsfenster, Deduplizierung), ein niedrigeres wird nur vermerkt, und mit mv-decay kann die App eine Phase etwas länger halten. Die Variante badge ist ein Inline-Chip für Tabellen und Kopfzeilen; die Variante panel ist eine Bereitschaftskarte mit Countdown, Zeit bis Clear, letztem Grund, Signalanzahl, einer gemusterten Zeitleiste der letzten Minuten mit einem Strich pro Signal, einem Übergangsprotokoll und den Aktionen Acknowledge / Hold / Resolve. Die Fristen werden von Date.now() aus verkettet, sodass ein gedrosselter Hintergrund-Tab exakt aufholt, der Zustand ist JSON-serialisierbar (state / restore() / persist) für Neuladen und serverseitige Hydration, und jede Phase hat zusätzlich zu ihrer Farbe ein eigenes Icon, Label und Muster (solid, stripes, dots, flat).

KategorieFeedback
TypWeb Component (<mv-alert-phase>)
StatusBeta
KitMonitoring ohne Alarmmüdigkeit
Installiert auchbutton
Keywordsexclusive, culture, status, incident, severity, alerting, decay, cooldown, monitoring, on-call, observability, countdown, timer, dashboard, security, fraud, ci, timeline, health-check, escalation

When to use

  • A monitoring or on-call dashboard should show recent trouble fading back to normal instead of flipping from red to green
  • Bursty signals (5xx errors, fraud flags, failed logins) must keep a status raised while they continue and relax once they stop
  • A security or fraud console needs a visible cool-down period after an incident before anyone declares all clear
  • A table of services, jobs or pipelines needs a compact severity chip that also tells how long the current level will last

Avoid when

  • The status is a fixed label that never changes on its own (paid, draft, archived) → use Badge instead
  • A one-off message must be shown to the user and dismissed → use Toast instead
  • Clearing must only ever follow a human decision, never elapsed time: use a plain status and an incident workflow

Installation

node scripts/add.mjs alert-phase --out ./src/marvelous

KI-Agent mit dem MCP-Server von Marvelous UI: install_components({ slugs: ["alert-phase"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Kopierte Dateien (inklusive Abhängigkeiten): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, components/alert-phase/alert-phase.js, components/alert-phase/alert-phase.css, components/button/button.css.

Verwendung

Schnellstart mit dem kleinsten funktionierenden Markup:

<mv-alert-phase label="Checkout API" phase="alert" listen="checkout-5xx:alert"></mv-alert-phase>

Referenz-Markup zum Starten und Anpassen mit Attributen, data-* und CSS-Variablen:

<div id="ap-demo" style="width:min(100%,68rem);margin-inline:auto">
  <style>
    #ap-demo { display:grid; gap:1rem }
    #ap-demo .ap-top { display:flex; align-items:flex-end; justify-content:space-between; gap:.5rem 1rem; flex-wrap:wrap }
    #ap-demo .ap-top h3 { margin:0; font-size:1.125rem; letter-spacing:-.01em }
    #ap-demo .ap-top p { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
    #ap-demo .ap-layout { display:grid; grid-template-columns:minmax(0,1fr) 21rem; gap:1rem; align-items:start }
    #ap-demo .ap-main { display:grid; gap:.75rem; min-width:0 }
    #ap-demo .ap-controls { display:grid; gap:.75rem; padding:.875rem 1rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-bg-subtle) }
    #ap-demo .ap-row { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap }
    #ap-demo .ap-k { color:var(--mv-fg-muted); font-size:.6875rem; font-weight:600; letter-spacing:.04em; text-transform:uppercase; margin-inline-end:.25rem }
    #ap-demo .ap-row .mv-choice { font-size:.8125rem }
    #ap-demo .ap-log { margin:0; min-height:1.1rem; color:var(--mv-fg-subtle); font:.75rem/1.4 var(--mv-font-mono); overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
    #ap-demo .ap-services { border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm); overflow:hidden }
    #ap-demo .ap-services header { display:flex; align-items:center; justify-content:space-between; gap:.5rem; padding:.875rem 1rem; border-bottom:1px solid var(--mv-border) }
    #ap-demo .ap-services h4 { margin:0; font-size:.875rem }
    #ap-demo .ap-list { list-style:none; margin:0; padding:.25rem 0 }
    #ap-demo .ap-list li { display:flex; align-items:center; justify-content:space-between; gap:.75rem; padding:.625rem 1rem }
    #ap-demo .ap-list li + li { border-top:1px solid var(--mv-border) }
    #ap-demo .ap-svc { display:grid; gap:.125rem; min-width:0 }
    #ap-demo .ap-svc b { font-size:.8125rem; font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
    #ap-demo .ap-svc span { color:var(--mv-fg-muted); font-size:.75rem }
    #ap-demo .ap-foot { display:grid; gap:.625rem; padding:.875rem 1rem; border-top:1px solid var(--mv-border); background:var(--mv-bg-subtle) }
    #ap-demo .ap-foot p { margin:0; color:var(--mv-fg-muted); font-size:.75rem; line-height:1.5 }
    @media (max-width:56rem) { #ap-demo .ap-layout { grid-template-columns:minmax(0,1fr) } }
  </style>

  <div class="ap-top">
    <div>
      <h3>Incident console</h3>
      <p>Acme Commerce · production · accelerated timers for the demo (Alert 20 s → Elevated 25 s → Caution 30 s)</p>
    </div>
    <span class="mv-badge" data-variant="outline" data-shape="pill">On call: Priya Natarajan</span>
  </div>

  <div class="ap-layout">
    <div class="ap-main">
      <mv-alert-phase id="ap-checkout" variant="panel" label="Checkout API · us-east-1"
        alert-duration="20s" elevated-duration="25s" caution-duration="30s" history-window="3m">
        <a slot="actions" class="mv-button" data-variant="link" data-size="sm" href="#runbook">Open runbook</a>
      </mv-alert-phase>

      <div class="ap-controls">
        <div class="ap-row">
          <span class="ap-k">Send a signal</span>
          <button type="button" class="mv-button" data-variant="outline" data-size="sm"
            data-alert-phase-signal="alert" data-alert-phase-target="ap-checkout"
            data-alert-phase-reason="5xx rate 6.1% on /v2/charges (threshold 2%)">Error spike · Alert</button>
          <button type="button" class="mv-button" data-variant="outline" data-size="sm"
            data-alert-phase-signal="elevated" data-alert-phase-target="ap-checkout"
            data-alert-phase-reason="Retry rate 18% from mobile clients">Retry storm · Elevated</button>
          <button type="button" class="mv-button" data-variant="outline" data-size="sm"
            data-alert-phase-signal="caution" data-alert-phase-target="ap-checkout"
            data-alert-phase-reason="p95 latency 910 ms (target 600 ms)">Slow responses · Caution</button>
        </div>
        <div class="ap-row">
          <label class="mv-choice">
            <input type="checkbox" role="switch" class="mv-switch" data-size="sm" id="ap-maint">
            <span class="mv-choice-text"><span class="mv-choice-title">Maintenance window: veto escalations</span></span>
          </label>
        </div>
        <p class="ap-log" id="ap-log" aria-hidden="true">Waiting for the next transition…</p>
      </div>
    </div>

    <aside class="ap-services" aria-label="Service status">
      <header>
        <h4>Services</h4>
        <label class="mv-choice" data-control="end" style="font-size:.75rem">
          <input type="checkbox" role="switch" class="mv-switch" data-size="sm" id="ap-sim" checked>
          <span class="mv-choice-text"><span class="mv-choice-title">Live signals</span></span>
        </label>
      </header>
      <ul class="ap-list">
        <li><span class="ap-svc"><b>Payments gateway</b><span>eu-west-1</span></span>
          <mv-alert-phase id="ap-s1" label="Payments gateway" announce="escalations" alert-duration="30s" elevated-duration="30s" caution-duration="40s"></mv-alert-phase></li>
        <li><span class="ap-svc"><b>Identity (SSO)</b><span>global</span></span>
          <mv-alert-phase id="ap-s2" label="Identity" announce="escalations" alert-duration="30s" elevated-duration="30s" caution-duration="40s"></mv-alert-phase></li>
        <li><span class="ap-svc"><b>Search</b><span>us-west-2</span></span>
          <mv-alert-phase id="ap-s3" label="Search" announce="escalations" alert-duration="30s" elevated-duration="30s" caution-duration="40s"></mv-alert-phase></li>
        <li><span class="ap-svc"><b>Media CDN</b><span>global edge</span></span>
          <mv-alert-phase id="ap-s4" label="Media CDN" announce="escalations" alert-duration="30s" elevated-duration="30s" caution-duration="40s"></mv-alert-phase></li>
        <li><span class="ap-svc"><b>Notifications</b><span>ap-southeast-1</span></span>
          <mv-alert-phase id="ap-s5" label="Notifications" announce="escalations" alert-duration="30s" elevated-duration="30s" caution-duration="40s"></mv-alert-phase></li>
        <li><span class="ap-svc"><b>Ledger sync</b><span>eu-central-1</span></span>
          <mv-alert-phase id="ap-s6" label="Ledger sync" announce="escalations" alert-duration="30s" elevated-duration="30s" caution-duration="40s"></mv-alert-phase></li>
      </ul>
      <div class="ap-foot">
        <p>Each status decays one level when its timer runs out without a new signal. A new signal of the same level re-arms it; a higher one escalates at once.</p>
      </div>
    </aside>
  </div>

  <script type="module">
    await customElements.whenDefined("mv-alert-phase");
    const root = document.getElementById("ap-demo");
    const $ = (id) => document.getElementById(id);
    const s = 1000;
    const t = Date.now();

    // Hydrate from a server snapshot: the incident started two minutes ago.
    $("ap-checkout").restore({
      phase: "alert",
      remaining: 17 * s,
      since: t - 9 * s,
      episode: { start: t - 118 * s, signals: 9 },
      last: { level: "alert", reason: "Card authorization timeouts: 312 in the last minute", at: t - 3 * s },
      history: [
        { type: "change", from: "clear", to: "caution", at: t - 118 * s, cause: "signal", reason: "p95 latency 842 ms (target 600 ms)" },
        { type: "change", from: "caution", to: "alert", at: t - 97 * s, cause: "signal", reason: "5xx rate 4.2% on /v2/charges" },
        { type: "change", from: "alert", to: "elevated", at: t - 71 * s, cause: "timeout", idle: 20 * s },
        { type: "change", from: "elevated", to: "caution", at: t - 46 * s, cause: "timeout", idle: 25 * s },
        { type: "change", from: "caution", to: "alert", at: t - 9 * s, cause: "signal", reason: "Card authorization timeouts: 188 in the last minute" },
      ],
      signals: [118, 111, 97, 94, 91, 50, 9, 6, 3].map((ago, i) => ({ at: t - ago * s, level: i < 2 ? 1 : i === 5 ? 2 : 3 })),
    });

    const seed = {
      "ap-s1": { phase: "elevated", remaining: 12 * s, last: { level: 3, reason: "Issuer declines up 3x", at: t - 30 * s } },
      "ap-s2": { phase: "alert", held: true, remaining: 16 * s, acknowledged: { by: "Priya N." }, last: { level: 3, reason: "SAML assertion failures", at: t - 40 * s } },
      "ap-s3": { phase: "caution", remaining: 19 * s },
      "ap-s4": { phase: "caution", remaining: 6 * s },
      "ap-s5": { phase: "clear" },
      "ap-s6": { phase: "clear" },
    };
    for (const [id, state] of Object.entries(seed)) $(id).restore(state);

    // Maintenance window: the app vetoes escalations (the cancelable hook).
    const maint = $("ap-maint");
    $("ap-checkout").addEventListener("mv-escalate", (e) => { if (maint.checked) e.preventDefault(); });

    const log = $("ap-log");
    root.addEventListener("mv-change", (e) => {
      const { from, to, cause } = e.detail;
      const name = e.target.label ?? e.target.id;
      log.textContent = `mv-change · ${name} · ${from} → ${to} (${cause})`;
    });
    root.addEventListener("mv-signal", (e) => {
      if (e.detail.outcome === "vetoed") log.textContent = `mv-escalate vetoed · ${e.target.label} · ${e.detail.level}`;
    });
    root.addEventListener("click", (e) => { if (e.target.closest("a[href='#runbook']")) e.preventDefault(); });

    // Live signals on the service list (your WebSocket or polling handler would do this).
    const feed = [
      ["ap-s1", "elevated", "Issuer declines up 3x"], ["ap-s1", "alert", "Acquirer timeouts above 5%"],
      ["ap-s3", "caution", "Index lag 42 s"], ["ap-s3", "elevated", "Query errors 2.4%"],
      ["ap-s4", "caution", "Origin fetch p95 1.2 s"], ["ap-s5", "caution", "Push queue depth 18k"],
      ["ap-s6", "elevated", "Reconciliation drift $1,240"], ["ap-s5", "alert", "APNs rejects 9%"],
      ["ap-s4", "elevated", "Cache hit ratio 71%"],
    ];
    let i = 0;
    const sim = $("ap-sim");
    const timer = setInterval(() => {
      if (!root.isConnected) return clearInterval(timer);
      if (!sim.checked || document.hidden) return;
      const [id, level, reason] = feed[i++ % feed.length];
      $(id).signal(level, { reason, source: "metrics" });
    }, 5000);
  </script>
</div>

Kulturelle Referenz

Metal Gear Solid, Hideo Kojima (Konami) (1998, Spiel). Sobald Wachen den Spieler entdecken, geht die Basis in den Alert-Modus und klingt dann über Evasion und Caution in den Normalzustand ab, jede Phase mit ihrem eigenen sichtbaren Countdown, den nur eine neue Sichtung zurücksetzen oder direkt wieder auf Alert schicken kann. In der UI heben Incident-Signale einen Status an, der mit eigenen Timern Stufe für Stufe sinkt, sodass der Schweregrad sanft abklingt, statt von Rot auf Grün zu springen, und jedes neue Signal setzt ihn neu oder eskaliert ihn.

API

Attributes

NameTypDefaultDescription
variantbadge | panelbadgebadge: inline chip (icon with countdown ring, phase, time left). panel: card with big countdown, decay path, last signal, history timeline, log and actions.
labelstringStatusWhat the status is about ("Checkout API"). Panel title, prefix of the badge's accessible name and of every announcement.
phasealert | elevated | caution | clearInitial phase at first connection (full duration). Changing it later forces that phase (authoritative: not cancelable). The live phase is reflected in data-phase, not here.
alert-durationtime ("2m", "90s", "1h30m", ms number)2mHow long Alert lasts without a new alert-level signal before stepping down to Elevated.
elevated-durationtime5mHow long Elevated lasts before stepping down to Caution.
caution-durationtime10mHow long Caution lasts before Clear. Duration changes apply from the next phase.
heldbooleanFreezes the countdown (the Hold action sets it): no step-down until removed, signals still re-arm and escalate. Removed automatically when the status reaches Clear.
history-windowtime15mSpan of the panel's timeline strip (a block per non-clear stretch, a tick per signal).
log-sizenumber4Transitions listed under the panel's timeline, newest first (0 hides the log).
announceall | escalations | noneallWhat is spoken: escalations assertively, step-downs, Clear, acknowledgments and holds politely. Use escalations on long lists of badges so only rising severity interrupts.
listenstringSpace-separated event:level pairs listened on document, e.g. "checkout-5xx:alert checkout-slow:caution". The event detail may carry { level, reason (or message), source }; without a level in the pair, detail.level or alert is used.
persiststringlocalStorage key: the state is saved on every change and restored (with elapsed time caught up) after a reload. Storage failures are ignored.
actions"false""false" hides the built-in Acknowledge / Hold / Resolve buttons of the panel (slot="actions" content stays).
localeBCP 47 tagen-USLocale of the times in the panel (Intl.DateTimeFormat).
data-alert-phase-signalalert | elevated | cautionOn any clickable element: a click sends that signal to the element whose id is in data-alert-phase-target (space-separated ids) or, without it, to the enclosing mv-alert-phase. data-alert-phase-reason gives the reason.
data-alert-phase-actionacknowledge | hold | resolveOn any clickable element: runs that action on the target (same targeting rules; hold toggles). data-alert-phase-by names who acknowledged.
data-phase / data-level / data-held / data-acknowledgedset by the componentLive state on the host for styling (data-level 0..3).

Properties

NameTypDescription
phase"alert" | "elevated" | "caution" | "clear"Current phase. Setting it forces that phase (not cancelable, announced).
level0..3Current level: 0 clear, 1 caution, 2 elevated, 3 alert (read-only).
remainingnumber (ms)Time left in the running phase, recomputed on every read; frozen while held, 0 when clear (read-only).
remainingToClearnumber (ms)Time before Clear if no new signal arrives: remaining plus the lower phases' durations (read-only).
deadlinenumber (epoch ms) | nullWhen the running phase steps down; null when clear or held (read-only).
heldbooleanMirrors the held attribute.
acknowledged{ by, at } | nullAcknowledgment of the current episode; reset by any escalation and by Clear (read-only).
lastSignal{ level, phase, reason, source, at, outcome } | nullMost recent signal, whatever its outcome (escalated, rearmed, noted, vetoed), read-only. The panel marks a vetoed one.
historyArray<{ type, at, from?, to?, cause?, reason?, idle?, by?, phase? }>Up to 200 entries, oldest first. type: change (cause: signal | timeout | resolve | api | initial; idle = ms without signal for timeouts), ack, hold, resume, veto (read-only copy).
stateobjectJSON-safe snapshot { phase, level, deadline, remaining, held, acknowledged, since, quietSince, episode, last, history, signals }: send it to your server or storage and give it back to restore().
stringsPartial<Record<string, string>>Overrides for every visible text and announcement: phase names (alert, elevated, caution, clear), next, nextClear, heldText, quiet, lastSignal, signalsValue ({count}), acknowledge, hold, resume, resolve, timer ({time}), logTimeout, announceEscalate ({label}, {phase}), announceStep, announceClear… English defaults.

Methods

NameDescription
signal(level = "alert", { reason?, source? })Reports a signal. Same level as the current phase → re-armed to its full duration; higher → escalation (mv-escalate, cancelable); lower → only noted (counted, shown as last signal). Returns "escalated" | "rearmed" | "noted" | "vetoed" | "ignored".
acknowledge({ by? })Marks the current episode as seen: the Alert beacon stops, the panel shows who acknowledged. Returns false when clear or already acknowledged.
hold() / release()Freezes / resumes the countdown (sets / removes held). Emits mv-hold.
resolve({ reason? })Back to Clear now. Returns false when already clear.
restore(state)Rebuilds the status from a state snapshot (or a partial one: { phase, remaining } is enough), then catches up the time elapsed since, step by step. Emits the resulting mv-change events, without announcing them.
clearHistory()Forgets the history and the signal ticks; the current phase is kept.

Events

NameDescription
mv-escalateCancelable, before a signal raises the phase. detail: { from, to, reason, source }. preventDefault() vetoes it (maintenance window, known flapping check): the signal is logged as vetoed and the phase is unchanged.
mv-decayCancelable, before a timeout steps the phase down. detail: { from, to, at }. preventDefault() keeps the phase and re-arms it for its full duration (e.g. while a deploy is still rolling out).
mv-changeAfter every transition. detail: { from, to, level, cause: "signal" | "timeout" | "resolve" | "api", reason, source, at, deadline }. at is the exact time of the step, also when a background tab catches up.
mv-signalAfter every signal. detail: { level, reason, source, outcome, phase } (outcome as returned by signal(), phase = resulting phase).
mv-acknowledgeThe episode was acknowledged. detail: { phase, by, at }.
mv-holdThe countdown was frozen or resumed. detail: { held, remaining, phase }.

Content structure

NameDescription
actionsChildren with slot="actions" ("Open runbook", "Page on-call"…) are placed at the end of the panel's actions row; hidden in the badge variant.

CSS classes

NameDescription
mv-alert-phase-badgeBadge root (data-phase, data-flash on escalation): .mv-alert-phase-icon (countdown ring), -name, -time (-time-held pause glyph).
mv-alert-phase-panelPanel root (role=group, data-phase): -head, -title, -flag (ack / held), -now (icon, -name, -next, -clock), -path (-track, -seg, -seg-fill, -end, -steps, -step), -facts, -history (-strip, -block, -tick, -axis, -log), -actions.
mv-alert-phase-swatchAny element painted with a phase's tone and pattern (set data-phase on it): segment fills, timeline blocks, log dots. Reusable for a legend.

CSS variables

NameDefaultDescription
--mv-alert-phase-alertvar(--mv-danger)Alert tone (solid pattern).
--mv-alert-phase-elevatedmix of --mv-danger and --mv-warningElevated tone (diagonal stripes).
--mv-alert-phase-cautionvar(--mv-warning)Caution tone (dots).
--mv-alert-phase-clearvar(--mv-success)Clear tone (flat tint).

Accessibility

Severity is never carried by color alone: each phase has its own icon (octagon, magnifier, eye, check), its name in text and its own pattern (solid, stripes, dots, flat), which also keeps the phases apart in forced colors where every tone becomes CanvasText. Changes are announced through two visually hidden live regions: an escalation assertively with its reason (“Checkout API: Alert. 5xx rate 6.1%…”), step-downs, Clear, acknowledgments and holds politely; re-armed phases and the ticking countdown are never announced, and announce="escalations" or "none" keeps long lists of badges quiet. The remaining time is exposed in a role="timer" element right after the phase name (“Checkout API: Alert, steps down in 17 seconds”), whose implicit aria-live is off, so it is read on demand without chatter. The panel is a labelled group; the timeline strip and decay path are aria-hidden and duplicated by text (next step, time to Clear, last signal, signal count, a real <ol> log with <time datetime>). Acknowledge, Hold (aria-pressed toggle) and Resolve are native buttons; when they don't apply they become aria-disabled, so focus is never lost. Declarative triggers work with any native button, keyboard included. Reduced motion (OS or data-motion="reduce"): the ring and bars jump once per second instead of sliding, the Alert beacon becomes a static ring and the escalation flash is skipped. Performance: one timeout per second at most, aligned on the displayed second; badges sleep entirely while clear or held.

Diese Seite wurde mit KI übersetzt. Übersetzungsfehler melden