Exclusivobeta
Alert Phase <mv-alert-phase>
Un estado de incidente cuya gravedad decae con el tiempo en lugar de alternar entre rojo y verde. Las señales (signal(level, { reason }), un clic en cualquier disparador [data-alert-phase-signal] o eventos del DOM mapeados con listen) lo elevan a Alert, Elevated o Caution; cada fase lleva su propia cuenta regresiva visible (un anillo que se vacía y, en el panel, un camino de decaimiento hasta Clear) y, cuando expira sin una nueva señal, baja un nivel: Alert → Elevated → Caution → Clear. Una señal del mismo nivel rearma la fase, una superior escala de inmediato a través del evento cancelable mv-escalate (ventanas de mantenimiento, deduplicación), una inferior solo se anota, y mv-decay permite a la app mantener una fase un poco más. La variante badge es un chip en línea para tablas y cabeceras; la variante panel es una tarjeta de guardia con la cuenta regresiva, el tiempo hasta Clear, el último motivo, el número de señales, una línea de tiempo con patrones de los últimos minutos con una marca por señal, un registro de transiciones y las acciones Acknowledge / Hold / Resolve. Los plazos se encadenan desde Date.now(), así que una pestaña en segundo plano ralentizada se pone al día con exactitud, el estado es serializable en JSON (state / restore() / persist) para recargas e hidratación desde el servidor, y cada fase tiene su propio icono, etiqueta y patrón (solid, stripes, dots, flat) además de su color.
| Categoría | Feedback |
|---|---|
| Tipo | Web Component (<mv-alert-phase>) |
| Estado | beta |
| Kit | Supervisión sin fatiga de alertas |
| También instala | button |
| Keywords | exclusive, 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
Instalación
node scripts/add.mjs alert-phase --out ./src/marvelousAgente de IA con el servidor MCP de Marvelous UI: install_components({ slugs: ["alert-phase"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Archivos copiados (dependencias incluidas): 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.
Uso
Inicio rápido, el marcado mínimo que funciona:
<mv-alert-phase label="Checkout API" phase="alert" listen="checkout-5xx:alert"></mv-alert-phase>Marcado de referencia: parte de él y personalízalo con atributos, data-* y variables 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", "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>Referencia cultural
Metal Gear Solid, Hideo Kojima (Konami) (1998, videojuego). En cuanto los guardias detectan al jugador, la base entra en Alert y luego decae pasando por Evasion y Caution hasta volver a la normalidad, cada fase con su propia cuenta regresiva visible que solo un nuevo avistamiento puede reiniciar o devolver directamente a Alert. En la interfaz, las señales de incidente elevan un estado que baja de nivel en nivel con sus propios temporizadores, de modo que la gravedad se desvanece con suavidad en lugar de pasar de rojo a verde, y cualquier nueva señal lo rearma o lo escala.
API
Attributes
| Name | Tipo | Default | Description |
|---|---|---|---|
variant | badge | panel | badge | badge: inline chip (icon with countdown ring, phase, time left). panel: card with big countdown, decay path, last signal, history timeline, log and actions. |
label | string | Status | What the status is about ("Checkout API"). Panel title, prefix of the badge's accessible name and of every announcement. |
phase | alert | elevated | caution | clear | Initial 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-duration | time ("2m", "90s", "1h30m", ms number) | 2m | How long Alert lasts without a new alert-level signal before stepping down to Elevated. |
elevated-duration | time | 5m | How long Elevated lasts before stepping down to Caution. |
caution-duration | time | 10m | How long Caution lasts before Clear. Duration changes apply from the next phase. |
held | boolean | Freezes 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-window | time | 15m | Span of the panel's timeline strip (a block per non-clear stretch, a tick per signal). |
log-size | number | 4 | Transitions listed under the panel's timeline, newest first (0 hides the log). |
announce | all | escalations | none | all | What is spoken: escalations assertively, step-downs, Clear, acknowledgments and holds politely. Use escalations on long lists of badges so only rising severity interrupts. |
listen | string | Space-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. | |
persist | string | localStorage 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). | |
locale | BCP 47 tag | en-US | Locale of the times in the panel (Intl.DateTimeFormat). |
data-alert-phase-signal | alert | elevated | caution | On 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-action | acknowledge | hold | resolve | On 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-acknowledged | set by the component | Live state on the host for styling (data-level 0..3). |
Properties
| Name | Tipo | Description |
|---|---|---|
phase | "alert" | "elevated" | "caution" | "clear" | Current phase. Setting it forces that phase (not cancelable, announced). |
level | 0..3 | Current level: 0 clear, 1 caution, 2 elevated, 3 alert (read-only). |
remaining | number (ms) | Time left in the running phase, recomputed on every read; frozen while held, 0 when clear (read-only). |
remainingToClear | number (ms) | Time before Clear if no new signal arrives: remaining plus the lower phases' durations (read-only). |
deadline | number (epoch ms) | null | When the running phase steps down; null when clear or held (read-only). |
held | boolean | Mirrors the held attribute. |
acknowledged | { by, at } | null | Acknowledgment of the current episode; reset by any escalation and by Clear (read-only). |
lastSignal | { level, phase, reason, source, at, outcome } | null | Most recent signal, whatever its outcome (escalated, rearmed, noted, vetoed), read-only. The panel marks a vetoed one. |
history | Array<{ 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). |
state | object | JSON-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(). |
strings | Partial<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
| Name | Description |
|---|---|
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
| Name | Description |
|---|---|
mv-escalate | Cancelable, 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-decay | Cancelable, 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-change | After 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-signal | After every signal. detail: { level, reason, source, outcome, phase } (outcome as returned by signal(), phase = resulting phase). |
mv-acknowledge | The episode was acknowledged. detail: { phase, by, at }. |
mv-hold | The countdown was frozen or resumed. detail: { held, remaining, phase }. |
Content structure
| Name | Description |
|---|---|
actions | Children 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
| Name | Description |
|---|---|
mv-alert-phase-badge | Badge root (data-phase, data-flash on escalation): .mv-alert-phase-icon (countdown ring), -name, -time (-time-held pause glyph). |
mv-alert-phase-panel | Panel 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-swatch | Any 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
| Name | Default | Description |
|---|---|---|
--mv-alert-phase-alert | var(--mv-danger) | Alert tone (solid pattern). |
--mv-alert-phase-elevated | mix of --mv-danger and --mv-warning | Elevated tone (diagonal stripes). |
--mv-alert-phase-caution | var(--mv-warning) | Caution tone (dots). |
--mv-alert-phase-clear | var(--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.