独家

Alert Phase <mv-alert-phase>

一种严重程度随时间衰减的事件状态,而不是在红色与绿色之间来回切换。

文化出处

合金装备,小岛秀夫(Konami)(1998 年,游戏)。一旦守卫发现玩家,基地就进入 Alert 状态,随后依次衰减为 Evasion、Caution,最后恢复正常;每个阶段都有自己可见的倒计时,只有新的发现才能重置倒计时或直接退回 Alert。在 UI 中,事件信号会提升状态等级,状态再按各自的计时器逐级下降,因此严重程度会平缓消退,而不是从红色直接跳到绿色;任何新信号都会重新激活或升级它。

工作原理

一种严重程度随时间衰减的事件状态,而不是在红色与绿色之间来回切换。信号(signal(level, { reason })、点击任意 [data-alert-phase-signal] 触发器,或通过 listen 映射的 DOM 事件)会把状态提升到 Alert、Elevated 或 Caution;每个阶段都运行自己可见的倒计时(一个逐渐消耗的圆环,在 panel 中还有一条通往 Clear 的衰减路径),如果超时前没有新信号,就下降一级:Alert → Elevated → Caution → Clear。同级信号会重新激活当前阶段,更高级的信号会通过可取消的 mv-escalate 立即升级(适用于维护窗口、去重),更低级的信号只会被记录,而 mv-decay 让应用可以把某个阶段保持得更久一些。badge 变体是用于表格和标题栏的行内标签;panel 变体是一张值班卡片,包含倒计时、距 Clear 的时间、最近原因、信号计数、最近几分钟的带图案时间线(每个信号一个刻度)、状态转换日志,以及 Acknowledge / Hold / Resolve 操作。截止时间基于 Date.now() 链式计算,因此被限流的后台标签页能准确追上进度;状态可 JSON 序列化(state / restore() / persist),便于页面重载和服务端注水;每个阶段除颜色外还有自己的图标、标签和图案(solid、stripes、dots、flat)。

分类反馈
类型Web Component(<mv-alert-phase>)
状态稳定版
套件没有告警疲劳的监控
同时安装button
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

安装

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

通过 Marvelous UI MCP 服务器使用 AI 智能体:install_components({ slugs: ["alert-phase"], target_dir: "<absolute path>/src/marvelous", framework: "react" })。

复制的文件(含依赖):tokens/tokens.css, core/base.css, components/button/button.css, core/dom.js, core/element.js, components/alert-phase/alert-phase.js, components/alert-phase/alert-phase.css。

用法

快速开始,最小可运行标记:

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

标准标记,可在此基础上通过属性、data-* 和 CSS 变量进行定制:

<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", "Push gateway 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>

API

Attributes

Name类型DefaultDescription
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

Name类型Description
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… Durations: unitHour, unitMinute, unitSecond (spoken, "{n} minute|{n} minutes", plural form picked for locale) and shortHour, shortMinute, shortSecond (compact, "{n} min"). 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. Known limits: the countdown digits (0:17) are not localized; every phase lasts at least 1 second; the history keeps the last 200 transitions and 300 signal ticks; each instance listens for trigger clicks on document, so a page with hundreds of badges should prefer signal() calls over declarative triggers.

本页面由 AI 翻译。报告翻译问题