@arcfusionz/arc-analytic-ui 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/components/AnomalyCard/AnomalyCard.chart.d.ts +64 -0
- package/dist/components/AnomalyCard/AnomalyCard.chart.js +341 -0
- package/dist/components/AnomalyCard/AnomalyCard.d.ts +169 -0
- package/dist/components/AnomalyCard/AnomalyCard.js +1019 -0
- package/dist/components/AnomalyCard/AnomalyCard.model.d.ts +508 -0
- package/dist/components/AnomalyCard/AnomalyCard.model.js +553 -0
- package/dist/components/AnomalyCard/index.d.ts +4 -0
- package/dist/components/AnomalyCard/index.js +4 -0
- package/dist/components/ChartCard/ChartCard.chart.d.ts +72 -0
- package/dist/components/ChartCard/ChartCard.chart.js +1333 -0
- package/dist/components/ChartCard/ChartCard.d.ts +206 -0
- package/dist/components/ChartCard/ChartCard.js +718 -0
- package/dist/components/ChartCard/ChartCard.model.d.ts +452 -0
- package/dist/components/ChartCard/ChartCard.model.js +1223 -0
- package/dist/components/ChartCard/index.d.ts +4 -0
- package/dist/components/ChartCard/index.js +4 -0
- package/dist/components/ComparisonCard/ComparisonCard.chart.d.ts +80 -0
- package/dist/components/ComparisonCard/ComparisonCard.chart.js +337 -0
- package/dist/components/ComparisonCard/ComparisonCard.d.ts +200 -0
- package/dist/components/ComparisonCard/ComparisonCard.js +1325 -0
- package/dist/components/ComparisonCard/ComparisonCard.model.d.ts +309 -0
- package/dist/components/ComparisonCard/ComparisonCard.model.js +263 -0
- package/dist/components/ComparisonCard/index.d.ts +4 -0
- package/dist/components/ComparisonCard/index.js +4 -0
- package/dist/components/ForecastCard/ForecastCard.chart.d.ts +82 -0
- package/dist/components/ForecastCard/ForecastCard.chart.js +416 -0
- package/dist/components/ForecastCard/ForecastCard.d.ts +148 -0
- package/dist/components/ForecastCard/ForecastCard.js +1029 -0
- package/dist/components/ForecastCard/ForecastCard.model.d.ts +497 -0
- package/dist/components/ForecastCard/ForecastCard.model.js +554 -0
- package/dist/components/ForecastCard/index.d.ts +4 -0
- package/dist/components/ForecastCard/index.js +4 -0
- package/dist/components/HighlightCard/HighlightCard.chart.d.ts +122 -0
- package/dist/components/HighlightCard/HighlightCard.chart.js +660 -0
- package/dist/components/HighlightCard/HighlightCard.d.ts +202 -0
- package/dist/components/HighlightCard/HighlightCard.js +1286 -0
- package/dist/components/HighlightCard/HighlightCard.model.d.ts +692 -0
- package/dist/components/HighlightCard/HighlightCard.model.js +875 -0
- package/dist/components/HighlightCard/index.d.ts +4 -0
- package/dist/components/HighlightCard/index.js +4 -0
- package/dist/components/InsightCard/InsightCard.chart.d.ts +91 -0
- package/dist/components/InsightCard/InsightCard.chart.js +471 -0
- package/dist/components/InsightCard/InsightCard.d.ts +161 -0
- package/dist/components/InsightCard/InsightCard.js +961 -0
- package/dist/components/InsightCard/InsightCard.model.d.ts +405 -0
- package/dist/components/InsightCard/InsightCard.model.js +421 -0
- package/dist/components/InsightCard/index.d.ts +4 -0
- package/dist/components/InsightCard/index.js +4 -0
- package/dist/components/MetricCard/MetricCard.chart.d.ts +79 -0
- package/dist/components/MetricCard/MetricCard.chart.js +435 -0
- package/dist/components/MetricCard/MetricCard.d.ts +227 -0
- package/dist/components/MetricCard/MetricCard.js +1293 -0
- package/dist/components/MetricCard/MetricCard.model.d.ts +521 -0
- package/dist/components/MetricCard/MetricCard.model.js +627 -0
- package/dist/components/MetricCard/index.d.ts +4 -0
- package/dist/components/MetricCard/index.js +4 -0
- package/dist/components/ProfileCard/ProfileCard.chart.d.ts +64 -0
- package/dist/components/ProfileCard/ProfileCard.chart.js +427 -0
- package/dist/components/ProfileCard/ProfileCard.d.ts +140 -0
- package/dist/components/ProfileCard/ProfileCard.js +735 -0
- package/dist/components/ProfileCard/ProfileCard.model.d.ts +279 -0
- package/dist/components/ProfileCard/ProfileCard.model.js +244 -0
- package/dist/components/ProfileCard/index.d.ts +4 -0
- package/dist/components/ProfileCard/index.js +4 -0
- package/dist/components/ProgressCard/ProgressCard.chart.d.ts +73 -0
- package/dist/components/ProgressCard/ProgressCard.chart.js +373 -0
- package/dist/components/ProgressCard/ProgressCard.d.ts +137 -0
- package/dist/components/ProgressCard/ProgressCard.js +908 -0
- package/dist/components/ProgressCard/ProgressCard.model.d.ts +385 -0
- package/dist/components/ProgressCard/ProgressCard.model.js +389 -0
- package/dist/components/ProgressCard/index.d.ts +4 -0
- package/dist/components/ProgressCard/index.js +4 -0
- package/dist/components/ReportCard/ReportCard.chart.d.ts +45 -0
- package/dist/components/ReportCard/ReportCard.chart.js +231 -0
- package/dist/components/ReportCard/ReportCard.d.ts +153 -0
- package/dist/components/ReportCard/ReportCard.js +1118 -0
- package/dist/components/ReportCard/ReportCard.model.d.ts +392 -0
- package/dist/components/ReportCard/ReportCard.model.js +399 -0
- package/dist/components/ReportCard/index.d.ts +4 -0
- package/dist/components/ReportCard/index.js +4 -0
- package/dist/components/SankeyCard/SankeyCard.chart.d.ts +75 -0
- package/dist/components/SankeyCard/SankeyCard.chart.js +249 -0
- package/dist/components/SankeyCard/SankeyCard.d.ts +185 -0
- package/dist/components/SankeyCard/SankeyCard.js +788 -0
- package/dist/components/SankeyCard/SankeyCard.model.d.ts +501 -0
- package/dist/components/SankeyCard/SankeyCard.model.js +789 -0
- package/dist/components/SankeyCard/index.d.ts +4 -0
- package/dist/components/SankeyCard/index.js +4 -0
- package/dist/components/StatusCard/StatusCard.chart.d.ts +60 -0
- package/dist/components/StatusCard/StatusCard.chart.js +188 -0
- package/dist/components/StatusCard/StatusCard.d.ts +176 -0
- package/dist/components/StatusCard/StatusCard.js +989 -0
- package/dist/components/StatusCard/StatusCard.model.d.ts +555 -0
- package/dist/components/StatusCard/StatusCard.model.js +776 -0
- package/dist/components/StatusCard/index.d.ts +4 -0
- package/dist/components/StatusCard/index.js +4 -0
- package/dist/components/TableCard/TableCard.chart.d.ts +53 -0
- package/dist/components/TableCard/TableCard.chart.js +192 -0
- package/dist/components/TableCard/TableCard.d.ts +140 -0
- package/dist/components/TableCard/TableCard.js +811 -0
- package/dist/components/TableCard/TableCard.model.d.ts +381 -0
- package/dist/components/TableCard/TableCard.model.js +475 -0
- package/dist/components/TableCard/index.d.ts +4 -0
- package/dist/components/TableCard/index.js +4 -0
- package/dist/components/TargetCard/TargetCard.chart.d.ts +52 -0
- package/dist/components/TargetCard/TargetCard.chart.js +273 -0
- package/dist/components/TargetCard/TargetCard.d.ts +143 -0
- package/dist/components/TargetCard/TargetCard.js +910 -0
- package/dist/components/TargetCard/TargetCard.model.d.ts +468 -0
- package/dist/components/TargetCard/TargetCard.model.js +581 -0
- package/dist/components/TargetCard/index.d.ts +4 -0
- package/dist/components/TargetCard/index.js +4 -0
- package/dist/components/TreeCard/TreeCard.chart.d.ts +80 -0
- package/dist/components/TreeCard/TreeCard.chart.js +178 -0
- package/dist/components/TreeCard/TreeCard.d.ts +182 -0
- package/dist/components/TreeCard/TreeCard.js +1187 -0
- package/dist/components/TreeCard/TreeCard.model.d.ts +330 -0
- package/dist/components/TreeCard/TreeCard.model.js +401 -0
- package/dist/components/TreeCard/index.d.ts +4 -0
- package/dist/components/TreeCard/index.js +4 -0
- package/dist/components/TrendCard/TrendCard.chart.d.ts +75 -0
- package/dist/components/TrendCard/TrendCard.chart.js +393 -0
- package/dist/components/TrendCard/TrendCard.d.ts +224 -0
- package/dist/components/TrendCard/TrendCard.js +1053 -0
- package/dist/components/TrendCard/TrendCard.model.d.ts +545 -0
- package/dist/components/TrendCard/TrendCard.model.js +623 -0
- package/dist/components/TrendCard/index.d.ts +4 -0
- package/dist/components/TrendCard/index.js +4 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +57 -0
- package/dist/lib/vega/ArcVegaChart.d.ts +24 -0
- package/dist/lib/vega/ArcVegaChart.js +42 -0
- package/dist/lib/vega/ArcVegaSurface.d.ts +13 -0
- package/dist/lib/vega/ArcVegaSurface.js +38 -0
- package/dist/lib/vega/axisTicks.js +156 -0
- package/dist/lib/vega/config.d.ts +29 -0
- package/dist/lib/vega/config.js +113 -0
- package/dist/lib/vega/embed.d.ts +29 -0
- package/dist/lib/vega/embed.js +29 -0
- package/dist/lib/vega/engine.d.ts +10 -0
- package/dist/lib/vega/engine.js +6 -0
- package/dist/lib/vega/formatters.d.ts +125 -0
- package/dist/lib/vega/formatters.js +277 -0
- package/dist/lib/vega/index.d.ts +11 -0
- package/dist/lib/vega/load.d.ts +13 -0
- package/dist/lib/vega/load.js +21 -0
- package/dist/lib/vega/time.d.ts +45 -0
- package/dist/lib/vega/time.js +105 -0
- package/dist/lib/vega/tokens.d.ts +73 -0
- package/dist/lib/vega/tokens.js +137 -0
- package/dist/lib/vega/tooltip.d.ts +45 -0
- package/dist/lib/vega/tooltip.js +124 -0
- package/dist/lib/vega/useArcVega.d.ts +43 -0
- package/dist/lib/vega/useArcVega.js +121 -0
- package/package.json +60 -0
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
import { formatDate, formatDateRange, formatNumber, parseArcNumberFormat } from "../../lib/vega/formatters.js";
|
|
2
|
+
import { resolveInstant, zonedDayStart } from "../../lib/vega/time.js";
|
|
3
|
+
//#region src/components/StatusCard/StatusCard.model.ts
|
|
4
|
+
const SECOND_MS = 1e3;
|
|
5
|
+
const MINUTE_MS = 6e4;
|
|
6
|
+
const HOUR_MS = 36e5;
|
|
7
|
+
const DAY_MS = 864e5;
|
|
8
|
+
const WEEK_MS = 7 * DAY_MS;
|
|
9
|
+
const DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
10
|
+
/** Every level in ladder order — the order the key row and the table use. */
|
|
11
|
+
const STATUS_LEVELS = [
|
|
12
|
+
"operational",
|
|
13
|
+
"degraded",
|
|
14
|
+
"down",
|
|
15
|
+
"maintenance",
|
|
16
|
+
"unknown"
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* How bad each level is. A strip cell takes the WORST level it overlaps, so
|
|
20
|
+
* an outage can never be averaged away by the operational time around it; a
|
|
21
|
+
* gap in monitoring outranks planned work, because "we don't know" is worse
|
|
22
|
+
* news than "we took it down on purpose".
|
|
23
|
+
*/
|
|
24
|
+
const STATUS_SEVERITY = {
|
|
25
|
+
operational: 0,
|
|
26
|
+
maintenance: 1,
|
|
27
|
+
unknown: 2,
|
|
28
|
+
degraded: 3,
|
|
29
|
+
down: 4
|
|
30
|
+
};
|
|
31
|
+
/** Levels that count as an incident: something was wrong and nobody planned it. */
|
|
32
|
+
const INCIDENT_LEVELS = ["degraded", "down"];
|
|
33
|
+
/**
|
|
34
|
+
* The state machine. Error wins; loading over existing data is a refresh (the
|
|
35
|
+
* previous render stays under the veil); a missing level is `no-match` when
|
|
36
|
+
* the consumer's filter caused it and `empty` otherwise; a level with no run
|
|
37
|
+
* behind it is `insufficient` — the card reports the state and says it has no
|
|
38
|
+
* history rather than drawing a single cell as if it were one.
|
|
39
|
+
*/
|
|
40
|
+
function deriveStatusState({ status, hasLevel, hasHistory, hasData, filtered }) {
|
|
41
|
+
if (status === "error") return "error";
|
|
42
|
+
if (status === "loading") return hasData ? "refreshing" : "loading";
|
|
43
|
+
if (hasLevel) return hasHistory ? "ready" : "insufficient";
|
|
44
|
+
return filtered ? "no-match" : "empty";
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The strip's domain. An explicit `window` wins; otherwise it spans the
|
|
48
|
+
* history — from the first reported start to the last reported end, extended
|
|
49
|
+
* to `asOf` when the card knows when it was last true, so a still-running
|
|
50
|
+
* state reaches the right-hand edge. `null` when there is nothing to span or
|
|
51
|
+
* the bounds have no length.
|
|
52
|
+
*/
|
|
53
|
+
function resolveStatusWindow(window, history, asOf, timeZone) {
|
|
54
|
+
const bounded = (start, end) => Number.isFinite(start) && Number.isFinite(end) && end > start ? {
|
|
55
|
+
start,
|
|
56
|
+
end,
|
|
57
|
+
span: end - start
|
|
58
|
+
} : null;
|
|
59
|
+
if (window) {
|
|
60
|
+
const declared = typeof window.end === "string" && DATE_ONLY.test(window.end) ? resolveInstant(window.end, timeZone) + DAY_MS : resolveInstant(window.end, timeZone);
|
|
61
|
+
const start = resolveInstant(window.start, timeZone);
|
|
62
|
+
return bounded(start, asOf != null && asOf > start && asOf < declared ? asOf : declared);
|
|
63
|
+
}
|
|
64
|
+
if (!history?.length) return null;
|
|
65
|
+
let start = Infinity;
|
|
66
|
+
let end = -Infinity;
|
|
67
|
+
for (const period of history) {
|
|
68
|
+
const from = resolveInstant(period.start, timeZone);
|
|
69
|
+
if (!Number.isFinite(from)) continue;
|
|
70
|
+
start = Math.min(start, from);
|
|
71
|
+
end = Math.max(end, from);
|
|
72
|
+
if (period.end != null) {
|
|
73
|
+
const to = resolveInstant(period.end, timeZone);
|
|
74
|
+
if (Number.isFinite(to)) end = Math.max(end, to);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (asOf != null) end = Math.max(end, asOf);
|
|
78
|
+
return bounded(start, end);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The reported states as one contiguous run across the window: resolved to
|
|
82
|
+
* instants, sorted, clipped to the window, implicit ends closed by the next
|
|
83
|
+
* start, overlaps resolved in favour of the later report (a correction
|
|
84
|
+
* supersedes what it corrects), holes filled with explicit `unknown` segments
|
|
85
|
+
* — a gap in monitoring is a fact, not a blank — and neighbours at the same
|
|
86
|
+
* level merged so the run reads as states rather than as reports.
|
|
87
|
+
*/
|
|
88
|
+
function normalizeHistory(history, window, timeZone) {
|
|
89
|
+
const reported = (history ?? []).map((period) => ({
|
|
90
|
+
level: period.level,
|
|
91
|
+
start: resolveInstant(period.start, timeZone),
|
|
92
|
+
end: period.end == null ? null : resolveInstant(period.end, timeZone),
|
|
93
|
+
label: period.label
|
|
94
|
+
})).filter((period) => Number.isFinite(period.start) && (period.end == null || Number.isFinite(period.end)) && STATUS_SEVERITY[period.level] != null).sort((a, b) => a.start - b.start);
|
|
95
|
+
const placed = [];
|
|
96
|
+
for (let index = 0; index < reported.length; index += 1) {
|
|
97
|
+
const period = reported[index];
|
|
98
|
+
const next = reported[index + 1];
|
|
99
|
+
const closed = period.end ?? next?.start ?? window.end;
|
|
100
|
+
const start = Math.max(window.start, period.start);
|
|
101
|
+
const end = Math.min(window.end, closed, next ? Math.max(next.start, start) : closed);
|
|
102
|
+
if (!(end > start)) continue;
|
|
103
|
+
placed.push({
|
|
104
|
+
level: period.level,
|
|
105
|
+
start,
|
|
106
|
+
end,
|
|
107
|
+
duration: end - start,
|
|
108
|
+
...period.label ? { label: period.label } : {}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const filled = [];
|
|
112
|
+
let cursor = window.start;
|
|
113
|
+
const gap = (start, end) => ({
|
|
114
|
+
level: "unknown",
|
|
115
|
+
start,
|
|
116
|
+
end,
|
|
117
|
+
duration: end - start,
|
|
118
|
+
gap: true
|
|
119
|
+
});
|
|
120
|
+
for (const segment of placed) {
|
|
121
|
+
if (segment.start > cursor) filled.push(gap(cursor, segment.start));
|
|
122
|
+
filled.push(segment);
|
|
123
|
+
cursor = segment.end;
|
|
124
|
+
}
|
|
125
|
+
if (cursor < window.end) filled.push(gap(cursor, window.end));
|
|
126
|
+
const merged = [];
|
|
127
|
+
for (const segment of filled) {
|
|
128
|
+
const previous = merged.at(-1);
|
|
129
|
+
if (previous && previous.level === segment.level && previous.gap === segment.gap) {
|
|
130
|
+
previous.end = segment.end;
|
|
131
|
+
previous.duration = previous.end - previous.start;
|
|
132
|
+
if (!previous.label && segment.label) previous.label = segment.label;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
merged.push({ ...segment });
|
|
136
|
+
}
|
|
137
|
+
return merged;
|
|
138
|
+
}
|
|
139
|
+
function worstOf(levels) {
|
|
140
|
+
return levels.reduce((worst, level) => STATUS_SEVERITY[level] > STATUS_SEVERITY[worst] ? level : worst, "operational");
|
|
141
|
+
}
|
|
142
|
+
/** The `degraded` / `down` runs inside a normalised run. */
|
|
143
|
+
function deriveIncidents(segments) {
|
|
144
|
+
const incidents = [];
|
|
145
|
+
let current = null;
|
|
146
|
+
for (const segment of segments) {
|
|
147
|
+
if (!INCIDENT_LEVELS.includes(segment.level)) {
|
|
148
|
+
current = null;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const level = segment.level;
|
|
152
|
+
const downtime = level === "down" ? segment.duration : 0;
|
|
153
|
+
if (current) {
|
|
154
|
+
current.end = segment.end;
|
|
155
|
+
current.duration = current.end - current.start;
|
|
156
|
+
current.downtime += downtime;
|
|
157
|
+
if (STATUS_SEVERITY[level] > STATUS_SEVERITY[current.level]) current.level = level;
|
|
158
|
+
if (!current.label && segment.label) current.label = segment.label;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
current = {
|
|
162
|
+
start: segment.start,
|
|
163
|
+
end: segment.end,
|
|
164
|
+
duration: segment.duration,
|
|
165
|
+
level,
|
|
166
|
+
downtime,
|
|
167
|
+
...segment.label ? { label: segment.label } : {}
|
|
168
|
+
};
|
|
169
|
+
incidents.push(current);
|
|
170
|
+
}
|
|
171
|
+
return incidents;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Everything the card reports, measured on the exact segments — never on the
|
|
175
|
+
* strip's cells, so the figures stay true however coarsely the strip has to
|
|
176
|
+
* draw. `null` when there is no window to measure over.
|
|
177
|
+
*
|
|
178
|
+
* A reported `level` is the current state, appended to the history as a
|
|
179
|
+
* still-running period: from `since` when the consumer knows when it began,
|
|
180
|
+
* otherwise from wherever the history stops — so the strip's right-hand edge
|
|
181
|
+
* always agrees with the word in the headline instead of contradicting it.
|
|
182
|
+
* With no `level` at all, the current state is read off the end of the run.
|
|
183
|
+
*/
|
|
184
|
+
function deriveStatusRun({ level, since, history, window, asOf = null, uptimeTarget, mode = "monitored", timeZone }) {
|
|
185
|
+
const explicitSince = since == null ? null : resolveInstant(since, timeZone);
|
|
186
|
+
const sinceMsGiven = explicitSince != null && Number.isFinite(explicitSince) ? explicitSince : null;
|
|
187
|
+
const currentStart = sinceMsGiven ?? lastReportedInstant(history, timeZone);
|
|
188
|
+
const bounds = resolveStatusWindow(window, level != null && currentStart != null ? [...history ?? [], {
|
|
189
|
+
level,
|
|
190
|
+
start: currentStart
|
|
191
|
+
}] : history, asOf, timeZone);
|
|
192
|
+
if (!bounds) return null;
|
|
193
|
+
const segments = normalizeHistory([...(history ?? []).flatMap((period) => {
|
|
194
|
+
if (sinceMsGiven == null) return [period];
|
|
195
|
+
const start = resolveInstant(period.start, timeZone);
|
|
196
|
+
if (!Number.isFinite(start) || start >= sinceMsGiven) return [];
|
|
197
|
+
const end = period.end == null ? sinceMsGiven : Math.min(resolveInstant(period.end, timeZone), sinceMsGiven);
|
|
198
|
+
return [{
|
|
199
|
+
...period,
|
|
200
|
+
end
|
|
201
|
+
}];
|
|
202
|
+
}), ...level != null ? [{
|
|
203
|
+
level,
|
|
204
|
+
start: currentStart ?? bounds.start
|
|
205
|
+
}] : []], bounds, timeZone);
|
|
206
|
+
if (segments.length === 0) return null;
|
|
207
|
+
const durations = {
|
|
208
|
+
operational: 0,
|
|
209
|
+
degraded: 0,
|
|
210
|
+
down: 0,
|
|
211
|
+
maintenance: 0,
|
|
212
|
+
unknown: 0
|
|
213
|
+
};
|
|
214
|
+
for (const segment of segments) durations[segment.level] += segment.duration;
|
|
215
|
+
const last = segments.at(-1);
|
|
216
|
+
const currentLevel = level ?? last.level;
|
|
217
|
+
let sinceMs = last.start;
|
|
218
|
+
for (let index = segments.length - 1; index >= 0; index -= 1) {
|
|
219
|
+
if (segments[index].level !== currentLevel) break;
|
|
220
|
+
sinceMs = segments[index].start;
|
|
221
|
+
}
|
|
222
|
+
const incidents = deriveIncidents(segments);
|
|
223
|
+
const longestIncident = incidents.reduce((longest, incident) => longest == null || incident.duration > longest.duration ? incident : longest, null);
|
|
224
|
+
const excluded = durations.maintenance + durations.unknown;
|
|
225
|
+
const monitored = mode === "window" ? bounds.span : bounds.span - excluded;
|
|
226
|
+
const uptime = monitored > 0 ? durations.operational / monitored : null;
|
|
227
|
+
const target = typeof uptimeTarget === "number" && Number.isFinite(uptimeTarget) && uptimeTarget > 0 && uptimeTarget <= 1 ? uptimeTarget : null;
|
|
228
|
+
const sla = target != null && uptime != null ? (() => {
|
|
229
|
+
const allowed = Math.round(monitored * (1 - target));
|
|
230
|
+
const used = Math.round(monitored - durations.operational);
|
|
231
|
+
return {
|
|
232
|
+
target,
|
|
233
|
+
verdict: uptime >= target ? "met" : "missed",
|
|
234
|
+
allowed,
|
|
235
|
+
used,
|
|
236
|
+
remaining: Math.max(0, allowed - used),
|
|
237
|
+
consumed: allowed > 0 ? used / allowed : used > 0 ? Infinity : 0
|
|
238
|
+
};
|
|
239
|
+
})() : null;
|
|
240
|
+
return {
|
|
241
|
+
window: bounds,
|
|
242
|
+
segments,
|
|
243
|
+
durations,
|
|
244
|
+
level: currentLevel,
|
|
245
|
+
since: sinceMs,
|
|
246
|
+
sinceDuration: bounds.end - sinceMs,
|
|
247
|
+
sinceClipped: sinceMs <= bounds.start,
|
|
248
|
+
worst: worstOf(segments.map((segment) => segment.level)),
|
|
249
|
+
incidents,
|
|
250
|
+
longestIncident,
|
|
251
|
+
downtime: durations.down,
|
|
252
|
+
excluded,
|
|
253
|
+
monitored,
|
|
254
|
+
uptime,
|
|
255
|
+
mode,
|
|
256
|
+
sla,
|
|
257
|
+
polarity: sla ? sla.verdict === "met" ? "positive" : "negative" : "neutral"
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/** The last instant the reported history reaches, or `null` when it reports nothing. */
|
|
261
|
+
function lastReportedInstant(history, timeZone) {
|
|
262
|
+
let latest = null;
|
|
263
|
+
for (const period of history ?? []) for (const bound of [period.start, period.end]) {
|
|
264
|
+
if (bound == null) continue;
|
|
265
|
+
const instant = resolveInstant(bound, timeZone);
|
|
266
|
+
if (Number.isFinite(instant) && (latest == null || instant > latest)) latest = instant;
|
|
267
|
+
}
|
|
268
|
+
return latest;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* The current state's age. From the run when there is one; from `since`
|
|
272
|
+
* against `asOf` when the card has a level but no window to draw (the
|
|
273
|
+
* `insufficient` state still owes the reader "how long"). `null` when nothing
|
|
274
|
+
* pins the moment it began.
|
|
275
|
+
*/
|
|
276
|
+
function resolveStatusHold({ run, since, asOf, timeZone }) {
|
|
277
|
+
if (run) return {
|
|
278
|
+
since: run.since,
|
|
279
|
+
duration: run.sinceDuration,
|
|
280
|
+
atLeast: run.sinceClipped
|
|
281
|
+
};
|
|
282
|
+
if (since == null || asOf == null) return null;
|
|
283
|
+
const from = resolveInstant(since, timeZone);
|
|
284
|
+
if (!Number.isFinite(from) || asOf < from) return null;
|
|
285
|
+
return {
|
|
286
|
+
since: from,
|
|
287
|
+
duration: asOf - from,
|
|
288
|
+
atLeast: false
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const STEP_LADDER = [
|
|
292
|
+
MINUTE_MS,
|
|
293
|
+
5 * MINUTE_MS,
|
|
294
|
+
15 * MINUTE_MS,
|
|
295
|
+
HOUR_MS,
|
|
296
|
+
6 * HOUR_MS,
|
|
297
|
+
12 * HOUR_MS,
|
|
298
|
+
DAY_MS,
|
|
299
|
+
2 * DAY_MS,
|
|
300
|
+
3 * DAY_MS,
|
|
301
|
+
WEEK_MS
|
|
302
|
+
];
|
|
303
|
+
const NAMED_STEPS = {
|
|
304
|
+
minute: MINUTE_MS,
|
|
305
|
+
hour: HOUR_MS,
|
|
306
|
+
day: DAY_MS,
|
|
307
|
+
week: WEEK_MS
|
|
308
|
+
};
|
|
309
|
+
/** The hue a level paints with, from the theme's status ladder. */
|
|
310
|
+
function statusHue(tokens, level) {
|
|
311
|
+
return tokens.status[level];
|
|
312
|
+
}
|
|
313
|
+
const STEP_KEYS = [
|
|
314
|
+
[MINUTE_MS, "minute"],
|
|
315
|
+
[5 * MINUTE_MS, "5-minute"],
|
|
316
|
+
[15 * MINUTE_MS, "15-minute"],
|
|
317
|
+
[HOUR_MS, "hour"],
|
|
318
|
+
[6 * HOUR_MS, "6-hour"],
|
|
319
|
+
[12 * HOUR_MS, "12-hour"],
|
|
320
|
+
[DAY_MS, "day"],
|
|
321
|
+
[2 * DAY_MS, "2-day"],
|
|
322
|
+
[3 * DAY_MS, "3-day"],
|
|
323
|
+
[WEEK_MS, "week"]
|
|
324
|
+
];
|
|
325
|
+
/** The resolved granularity as a word — the value of `data-resolution`. */
|
|
326
|
+
function statusStepKey(step) {
|
|
327
|
+
if (step == null) return "exact";
|
|
328
|
+
return STEP_KEYS.find(([size]) => size === step)?.[1] ?? `${step}ms`;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Buckets ≥ 1 hour align to the zone's calendar so a "day" cell is a day a
|
|
332
|
+
* reader recognises rather than a 24-hour block starting whenever monitoring
|
|
333
|
+
* did; the first cell is clipped back to the window.
|
|
334
|
+
*/
|
|
335
|
+
function alignStepStart(instant, step, timeZone) {
|
|
336
|
+
if (step < HOUR_MS) return Math.floor(instant / step) * step;
|
|
337
|
+
if (step < DAY_MS) {
|
|
338
|
+
const dayStart = zonedDayStart(instant, timeZone);
|
|
339
|
+
return dayStart + Math.floor((instant - dayStart) / step) * step;
|
|
340
|
+
}
|
|
341
|
+
const dayStart = zonedDayStart(instant, timeZone);
|
|
342
|
+
if (step === DAY_MS) return dayStart;
|
|
343
|
+
return dayStart;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* The rows the strip draws. `exact` emits the true segments; every other
|
|
347
|
+
* resolution divides the window into equal cells, each painted the worst
|
|
348
|
+
* level it overlaps — the Statuspage rule, and the only way a 40-second
|
|
349
|
+
* outage in a 90-day window is visible at all. `auto` takes the finest step
|
|
350
|
+
* from the ladder whose cells still clear `minCellPx` at the measured width,
|
|
351
|
+
* so a narrow tile degrades to coarser cells instead of to sub-pixel slivers.
|
|
352
|
+
*
|
|
353
|
+
* Cells are inset by a share of their own pitch so touching fills stay apart
|
|
354
|
+
* without a stroke eating a narrow cell, and each row keeps its true bounds
|
|
355
|
+
* and its true per-level durations for the tooltip — the drawing rounds, the
|
|
356
|
+
* numbers never do.
|
|
357
|
+
*/
|
|
358
|
+
function buildStatusStrip({ run, resolution, tokens, plotWidth, minCellPx, gapRatio, gapMinPx, gapMaxPx, timeZone }) {
|
|
359
|
+
const { window, segments } = run;
|
|
360
|
+
const msPerPx = plotWidth > 0 ? window.span / plotWidth : 0;
|
|
361
|
+
const emptyDurations = () => ({
|
|
362
|
+
operational: 0,
|
|
363
|
+
degraded: 0,
|
|
364
|
+
down: 0,
|
|
365
|
+
maintenance: 0,
|
|
366
|
+
unknown: 0
|
|
367
|
+
});
|
|
368
|
+
const step = resolveStep(resolution, window.span, plotWidth, minCellPx);
|
|
369
|
+
const granularity = step == null || step < DAY_MS ? "datetime" : "day";
|
|
370
|
+
const axisGranularity = window.span > 2 * DAY_MS ? "day-short" : window.span > 12 * HOUR_MS ? "day-time" : "time";
|
|
371
|
+
const cells = [];
|
|
372
|
+
if (step == null) for (const segment of segments) cells.push({
|
|
373
|
+
from: segment.start,
|
|
374
|
+
to: segment.end
|
|
375
|
+
});
|
|
376
|
+
else {
|
|
377
|
+
let cursor = alignStepStart(window.start, step, timeZone);
|
|
378
|
+
while (cursor < window.end) {
|
|
379
|
+
const from = Math.max(window.start, cursor);
|
|
380
|
+
const to = Math.min(window.end, cursor + step);
|
|
381
|
+
if (to > from) cells.push({
|
|
382
|
+
from,
|
|
383
|
+
to
|
|
384
|
+
});
|
|
385
|
+
cursor += step;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
let cursorIndex = 0;
|
|
389
|
+
return {
|
|
390
|
+
rows: cells.map(({ from, to }) => {
|
|
391
|
+
const durations = emptyDurations();
|
|
392
|
+
let label;
|
|
393
|
+
while (cursorIndex > 0 && segments[cursorIndex - 1].end > from) cursorIndex -= 1;
|
|
394
|
+
for (let index = cursorIndex; index < segments.length; index += 1) {
|
|
395
|
+
const segment = segments[index];
|
|
396
|
+
if (segment.start >= to) break;
|
|
397
|
+
if (segment.end <= from) {
|
|
398
|
+
cursorIndex = index + 1;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
durations[segment.level] += Math.min(segment.end, to) - Math.max(segment.start, from);
|
|
402
|
+
if (!label && segment.label) label = segment.label;
|
|
403
|
+
}
|
|
404
|
+
const level = worstOf(Object.keys(durations).filter((candidate) => durations[candidate] > 0));
|
|
405
|
+
const pitchPx = msPerPx > 0 ? (to - from) / msPerPx : 0;
|
|
406
|
+
const gapPx = Math.min(gapMaxPx, Math.max(gapMinPx, pitchPx * gapRatio));
|
|
407
|
+
const inset = Math.min(gapPx * msPerPx, (to - from) / 2);
|
|
408
|
+
return {
|
|
409
|
+
start: from + inset / 2,
|
|
410
|
+
end: to - inset / 2,
|
|
411
|
+
from,
|
|
412
|
+
to,
|
|
413
|
+
level,
|
|
414
|
+
c: statusHue(tokens, level),
|
|
415
|
+
durations,
|
|
416
|
+
...label ? { label } : {}
|
|
417
|
+
};
|
|
418
|
+
}),
|
|
419
|
+
step,
|
|
420
|
+
granularity,
|
|
421
|
+
axisGranularity
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
/** The bucket width, or `null` for exact segments. Exported for the unit tests. */
|
|
425
|
+
function resolveStep(resolution, span, plotWidth, minCellPx) {
|
|
426
|
+
if (resolution === "exact") return null;
|
|
427
|
+
if (resolution !== "auto") return NAMED_STEPS[resolution];
|
|
428
|
+
const budget = plotWidth > 0 ? Math.max(1, Math.floor(plotWidth / minCellPx)) : 1;
|
|
429
|
+
for (const candidate of STEP_LADDER) if (Math.ceil(span / candidate) <= budget) return candidate;
|
|
430
|
+
return Math.max(MINUTE_MS, Math.ceil(span / budget / MINUTE_MS) * MINUTE_MS);
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* A duration in the two most significant units it has — `6d 4h`, `2h 14m`,
|
|
434
|
+
* `41m`, `18s`. Precision to the decision (Few's pitfall #3): nobody pages
|
|
435
|
+
* anyone over the seconds of a six-day uptime. Units come from `labels` so a
|
|
436
|
+
* Thai product reads in Thai; `Intl.DurationFormat` is deliberately not used
|
|
437
|
+
* — it is too new to rely on across the runtimes this package supports.
|
|
438
|
+
*/
|
|
439
|
+
function formatDuration(ms, { units, locale, maxUnits = 2, zero }) {
|
|
440
|
+
if (ms == null || !Number.isFinite(ms)) return "—";
|
|
441
|
+
const total = Math.max(0, Math.round(ms));
|
|
442
|
+
if (total === 0) return zero;
|
|
443
|
+
const ladder = [
|
|
444
|
+
[DAY_MS, units.day],
|
|
445
|
+
[HOUR_MS, units.hour],
|
|
446
|
+
[MINUTE_MS, units.minute],
|
|
447
|
+
[SECOND_MS, units.second]
|
|
448
|
+
];
|
|
449
|
+
const parts = [];
|
|
450
|
+
let rest = total;
|
|
451
|
+
for (const [size, unit] of ladder) {
|
|
452
|
+
if (parts.length >= maxUnits) break;
|
|
453
|
+
const count = Math.floor(rest / size);
|
|
454
|
+
if (count === 0 && parts.length === 0) continue;
|
|
455
|
+
if (count > 0) parts.push(`${formatNumber(count, "integer", locale)}${unit}`);
|
|
456
|
+
rest -= count * size;
|
|
457
|
+
}
|
|
458
|
+
return parts.length > 0 ? parts.join(" ") : `${formatNumber(1, "integer", locale)}${units.second}`;
|
|
459
|
+
}
|
|
460
|
+
/** One grammar for every surface, so the same duration or share never reads two ways. */
|
|
461
|
+
function createStatusFormatters({ uptimeFormat, formatUptimeValue, units, zeroDuration, locale, timeZone }) {
|
|
462
|
+
const uptimeDigits = parseArcNumberFormat(uptimeFormat).digits;
|
|
463
|
+
const uptimeExtra = uptimeDigits == null ? void 0 : { minimumFractionDigits: uptimeDigits };
|
|
464
|
+
return {
|
|
465
|
+
formatUptime: (value) => value == null || !Number.isFinite(value) ? "—" : formatUptimeValue ? formatUptimeValue(value) : formatNumber(value, uptimeFormat, locale, uptimeExtra),
|
|
466
|
+
formatDuration: (ms) => formatDuration(ms, {
|
|
467
|
+
units,
|
|
468
|
+
locale,
|
|
469
|
+
zero: zeroDuration
|
|
470
|
+
}),
|
|
471
|
+
formatInstant: (instant, granularity = "day") => instant == null ? "—" : formatDate(instant, {
|
|
472
|
+
granularity,
|
|
473
|
+
locale,
|
|
474
|
+
timeZone
|
|
475
|
+
}),
|
|
476
|
+
formatSpan: (from, to, granularity = "day") => formatDateRange(from, to, {
|
|
477
|
+
granularity,
|
|
478
|
+
locale,
|
|
479
|
+
timeZone
|
|
480
|
+
})
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
const DEFAULT_STATUS_LABELS = {
|
|
484
|
+
levelNames: {
|
|
485
|
+
operational: "Operational",
|
|
486
|
+
degraded: "Degraded",
|
|
487
|
+
down: "Down",
|
|
488
|
+
maintenance: "Maintenance",
|
|
489
|
+
unknown: "No data"
|
|
490
|
+
},
|
|
491
|
+
heldFor: (duration) => `for ${duration}`,
|
|
492
|
+
heldAtLeast: (duration) => `for over ${duration}`,
|
|
493
|
+
uptimeOver: (span) => `uptime · ${span}`,
|
|
494
|
+
vsTarget: (target, met) => `${target} target ${met ? "met" : "missed"}`,
|
|
495
|
+
uptime: "Uptime",
|
|
496
|
+
monitored: "Monitored",
|
|
497
|
+
window: "Window",
|
|
498
|
+
incidents: (count, n) => `${count} ${n === 1 ? "incident" : "incidents"}`,
|
|
499
|
+
downFor: (duration) => `${duration} down`,
|
|
500
|
+
budgetLeft: (duration) => `${duration} budget left`,
|
|
501
|
+
budgetOver: (duration) => `${duration} over budget`,
|
|
502
|
+
currentRow: "Current",
|
|
503
|
+
sinceRow: "Since",
|
|
504
|
+
durationRow: "Duration",
|
|
505
|
+
worstRow: "Worst in window",
|
|
506
|
+
incidentsRow: "Incidents",
|
|
507
|
+
longestIncidentRow: "Longest incident",
|
|
508
|
+
downtimeRow: "Downtime",
|
|
509
|
+
excludedRow: "Excluded",
|
|
510
|
+
targetRow: "Target",
|
|
511
|
+
budgetRow: "Error budget",
|
|
512
|
+
stripRow: "Strip",
|
|
513
|
+
resolutionNote: (step) => `worst status per ${step}`,
|
|
514
|
+
exactNote: "exact state durations",
|
|
515
|
+
verdictNames: {
|
|
516
|
+
met: "Target met",
|
|
517
|
+
missed: "Target missed"
|
|
518
|
+
},
|
|
519
|
+
tableItem: "Item",
|
|
520
|
+
tableValue: "Value",
|
|
521
|
+
incidentTableWhen: "When",
|
|
522
|
+
incidentTableLevel: "State",
|
|
523
|
+
incidentTableFor: "For",
|
|
524
|
+
incidentTableCaption: (name) => `Incidents in ${name}`,
|
|
525
|
+
noIncidents: "No incidents",
|
|
526
|
+
durationUnits: {
|
|
527
|
+
day: "d",
|
|
528
|
+
hour: "h",
|
|
529
|
+
minute: "m",
|
|
530
|
+
second: "s"
|
|
531
|
+
},
|
|
532
|
+
zeroDuration: "none",
|
|
533
|
+
noData: "No data",
|
|
534
|
+
noMatch: "No matching data",
|
|
535
|
+
clearFilters: "Clear filters",
|
|
536
|
+
error: "Couldn't load this status",
|
|
537
|
+
retry: "Try again",
|
|
538
|
+
noHistory: "No history yet",
|
|
539
|
+
stale: "Stale",
|
|
540
|
+
asOf: "As of",
|
|
541
|
+
showTable: (name) => `Show the details behind ${name}`,
|
|
542
|
+
hideTable: (name) => `Hide the details behind ${name}`,
|
|
543
|
+
about: (name) => `About ${name}`,
|
|
544
|
+
loading: (name) => `Loading ${name}`,
|
|
545
|
+
tableCaption: (name) => `Availability details of ${name}`,
|
|
546
|
+
uptimeGroup: (name) => `${name} availability`,
|
|
547
|
+
levelGroup: (name) => `${name} status`,
|
|
548
|
+
insightMark: "Insight",
|
|
549
|
+
explanation: ({ uptimeText, spanText, monitoredText, mode, excludedText, downtimeText, incidentsText, sla }) => {
|
|
550
|
+
const rows = [[DEFAULT_STATUS_LABELS.uptime, uptimeText], [DEFAULT_STATUS_LABELS.window, spanText]];
|
|
551
|
+
if (mode === "monitored") rows.push([DEFAULT_STATUS_LABELS.monitored, monitoredText]);
|
|
552
|
+
if (excludedText) rows.push([DEFAULT_STATUS_LABELS.excludedRow, excludedText]);
|
|
553
|
+
rows.push([DEFAULT_STATUS_LABELS.downtimeRow, downtimeText]);
|
|
554
|
+
rows.push([DEFAULT_STATUS_LABELS.incidentsRow, incidentsText]);
|
|
555
|
+
if (sla) {
|
|
556
|
+
rows.push([DEFAULT_STATUS_LABELS.targetRow, `${sla.targetText} (${sla.verdictText})`]);
|
|
557
|
+
rows.push([DEFAULT_STATUS_LABELS.budgetRow, sla.over ? DEFAULT_STATUS_LABELS.budgetOver(sla.remainingText) : DEFAULT_STATUS_LABELS.budgetLeft(sla.remainingText)]);
|
|
558
|
+
}
|
|
559
|
+
return rows;
|
|
560
|
+
},
|
|
561
|
+
levelSentence: ({ name, level, duration, atLeast }) => `${name}: ${level} ${atLeast ? DEFAULT_STATUS_LABELS.heldAtLeast(duration) : DEFAULT_STATUS_LABELS.heldFor(duration)}.`,
|
|
562
|
+
uptimeSentence: ({ uptime, span, monitored, mode }) => mode === "monitored" && monitored !== span ? `${uptime} uptime over ${span}, ${monitored} monitored.` : `${uptime} uptime over ${span}.`,
|
|
563
|
+
incidentSentence: ({ count, countText, downtime, longest }) => count === 0 ? "No incidents." : `${countText} ${count === 1 ? "incident" : "incidents"}, ${downtime} down${longest ? `, longest ${longest}` : ""}.`,
|
|
564
|
+
slaSentence: ({ target, met, remaining, over }) => `${target} target ${met ? "met" : "missed"}: ${over ? `${remaining} over budget` : `${remaining} of error budget left`}.`,
|
|
565
|
+
windowSentence: ({ range }) => `Window ${range}.`,
|
|
566
|
+
chartDescription: ({ name, rangeText, resolutionText, levels, incidents }) => `Status timeline of ${name} over ${rangeText}, ${resolutionText}: ${levels.map(([level, duration]) => `${level.toLowerCase()} ${duration}`).join(", ")}. ${incidents}`
|
|
567
|
+
};
|
|
568
|
+
function resolveStatusLabels(overrides) {
|
|
569
|
+
return overrides ? {
|
|
570
|
+
...DEFAULT_STATUS_LABELS,
|
|
571
|
+
...overrides
|
|
572
|
+
} : DEFAULT_STATUS_LABELS;
|
|
573
|
+
}
|
|
574
|
+
/** The levels present in the window, in ladder order, with their durations — the key row, the table and the descriptions all read from this. */
|
|
575
|
+
function statusLevelBreakdown(run, formatters, labels) {
|
|
576
|
+
return STATUS_LEVELS.filter((level) => run.durations[level] > 0).map((level) => ({
|
|
577
|
+
level,
|
|
578
|
+
name: labels.levelNames[level],
|
|
579
|
+
duration: run.durations[level],
|
|
580
|
+
durationText: formatters.formatDuration(run.durations[level])
|
|
581
|
+
}));
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* A strip cell's span as the tooltip states it. A bucket's `end` is EXCLUSIVE,
|
|
585
|
+
* so at day granularity a single-day cell would read "Sep 2 – 3, 2026" — two
|
|
586
|
+
* days for one cell. Closing the range on the last instant the cell actually
|
|
587
|
+
* covers collapses it to "Sep 2, 2026" and turns a three-day cell into
|
|
588
|
+
* "Jun 12 – 14, 2026". Sub-day cells keep the exclusive bound, where
|
|
589
|
+
* "12:00 AM – 12:00 PM" reads better than "12:00 AM – 11:59 AM".
|
|
590
|
+
*/
|
|
591
|
+
function formatStripCell(row, strip, formatters) {
|
|
592
|
+
const inclusive = strip.step != null && strip.granularity === "day";
|
|
593
|
+
return formatters.formatSpan(row.from, inclusive ? row.to - 1 : row.to, strip.granularity);
|
|
594
|
+
}
|
|
595
|
+
/** How the strip's granularity is stated, in words. */
|
|
596
|
+
function describeResolution(strip, formatters, labels) {
|
|
597
|
+
return strip.step == null ? labels.exactNote : labels.resolutionNote(formatters.formatDuration(strip.step));
|
|
598
|
+
}
|
|
599
|
+
/** The rows behind the availability badge — hover, and `aria-describedby`. */
|
|
600
|
+
function buildStatusExplanation({ run, formatters, labels }) {
|
|
601
|
+
return labels.explanation({
|
|
602
|
+
uptimeText: formatters.formatUptime(run.uptime),
|
|
603
|
+
spanText: formatters.formatDuration(run.window.span),
|
|
604
|
+
monitoredText: formatters.formatDuration(run.monitored),
|
|
605
|
+
mode: run.mode,
|
|
606
|
+
excludedText: run.excluded > 0 ? formatters.formatDuration(run.excluded) : null,
|
|
607
|
+
downtimeText: formatters.formatDuration(run.downtime),
|
|
608
|
+
incidentsText: formatNumber(run.incidents.length, "integer"),
|
|
609
|
+
sla: run.sla ? {
|
|
610
|
+
targetText: formatters.formatUptime(run.sla.target),
|
|
611
|
+
verdictText: labels.verdictNames[run.sla.verdict],
|
|
612
|
+
remainingText: formatters.formatDuration(run.sla.remaining > 0 ? run.sla.remaining : run.sla.used - run.sla.allowed),
|
|
613
|
+
over: run.sla.remaining <= 0 && run.sla.used > run.sla.allowed
|
|
614
|
+
} : null
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
/** The summary table twin: every figure the card face and its tooltips show. */
|
|
618
|
+
function buildStatusDetailRows({ run, strip, formatters, labels }) {
|
|
619
|
+
const rows = [
|
|
620
|
+
{
|
|
621
|
+
key: "current",
|
|
622
|
+
label: labels.currentRow,
|
|
623
|
+
value: labels.levelNames[run.level]
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
key: "since",
|
|
627
|
+
label: labels.sinceRow,
|
|
628
|
+
value: formatters.formatInstant(run.since, "datetime")
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
key: "duration",
|
|
632
|
+
label: labels.durationRow,
|
|
633
|
+
value: formatters.formatDuration(run.sinceDuration)
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
key: "uptime",
|
|
637
|
+
label: labels.uptime,
|
|
638
|
+
value: formatters.formatUptime(run.uptime),
|
|
639
|
+
...run.sla ? { tone: run.polarity } : {}
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
key: "window",
|
|
643
|
+
label: labels.window,
|
|
644
|
+
value: formatters.formatDuration(run.window.span)
|
|
645
|
+
}
|
|
646
|
+
];
|
|
647
|
+
if (run.mode === "monitored" && run.excluded > 0) {
|
|
648
|
+
rows.push({
|
|
649
|
+
key: "monitored",
|
|
650
|
+
label: labels.monitored,
|
|
651
|
+
value: formatters.formatDuration(run.monitored)
|
|
652
|
+
});
|
|
653
|
+
rows.push({
|
|
654
|
+
key: "excluded",
|
|
655
|
+
label: labels.excludedRow,
|
|
656
|
+
value: formatters.formatDuration(run.excluded)
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
for (const entry of statusLevelBreakdown(run, formatters, labels)) rows.push({
|
|
660
|
+
key: `level-${entry.level}`,
|
|
661
|
+
label: entry.name,
|
|
662
|
+
value: entry.durationText
|
|
663
|
+
});
|
|
664
|
+
rows.push({
|
|
665
|
+
key: "worst",
|
|
666
|
+
label: labels.worstRow,
|
|
667
|
+
value: labels.levelNames[run.worst]
|
|
668
|
+
});
|
|
669
|
+
rows.push({
|
|
670
|
+
key: "incidents",
|
|
671
|
+
label: labels.incidentsRow,
|
|
672
|
+
value: formatNumber(run.incidents.length, "integer")
|
|
673
|
+
});
|
|
674
|
+
if (run.longestIncident) rows.push({
|
|
675
|
+
key: "longest",
|
|
676
|
+
label: labels.longestIncidentRow,
|
|
677
|
+
value: formatters.formatDuration(run.longestIncident.duration)
|
|
678
|
+
});
|
|
679
|
+
if (run.sla) {
|
|
680
|
+
rows.push({
|
|
681
|
+
key: "target",
|
|
682
|
+
label: labels.targetRow,
|
|
683
|
+
value: `${formatters.formatUptime(run.sla.target)} · ${labels.verdictNames[run.sla.verdict]}`,
|
|
684
|
+
tone: run.polarity
|
|
685
|
+
});
|
|
686
|
+
const over = run.sla.remaining <= 0 && run.sla.used > run.sla.allowed;
|
|
687
|
+
rows.push({
|
|
688
|
+
key: "budget",
|
|
689
|
+
label: labels.budgetRow,
|
|
690
|
+
value: over ? labels.budgetOver(formatters.formatDuration(run.sla.used - run.sla.allowed)) : labels.budgetLeft(formatters.formatDuration(run.sla.remaining)),
|
|
691
|
+
tone: over ? "negative" : "neutral"
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
if (strip) rows.push({
|
|
695
|
+
key: "resolution",
|
|
696
|
+
label: labels.stripRow,
|
|
697
|
+
value: describeResolution(strip, formatters, labels)
|
|
698
|
+
});
|
|
699
|
+
return rows;
|
|
700
|
+
}
|
|
701
|
+
/** The incident table twin — every incident the strip paints, with its true duration. */
|
|
702
|
+
function buildStatusIncidentRows({ run, formatters, labels }) {
|
|
703
|
+
return run.incidents.map((incident, index) => ({
|
|
704
|
+
key: `incident-${index}`,
|
|
705
|
+
when: formatters.formatInstant(incident.start, "datetime"),
|
|
706
|
+
level: incident.level,
|
|
707
|
+
levelName: labels.levelNames[incident.level],
|
|
708
|
+
duration: formatters.formatDuration(incident.duration),
|
|
709
|
+
...incident.label ? { label: incident.label } : {}
|
|
710
|
+
}));
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* The card's generated accessible description: the state and how long it has
|
|
714
|
+
* held, availability over the window (and how much of it was monitored), the
|
|
715
|
+
* incidents and the downtime, the SLA verdict with the budget it leaves, the
|
|
716
|
+
* window, and when the data was last true — Amy Cesal's alt-text formula
|
|
717
|
+
* applied to a status tile.
|
|
718
|
+
*/
|
|
719
|
+
function buildStatusDescription({ name, state, run, level, hold, formatters, labels, asOfText }) {
|
|
720
|
+
if (state === "loading" || state === "refreshing") return labels.loading(name);
|
|
721
|
+
if (state === "error") return `${name}: ${labels.error}.`;
|
|
722
|
+
if (state === "empty") return `${name}: ${labels.noData}.`;
|
|
723
|
+
if (state === "no-match") return `${name}: ${labels.noMatch}.`;
|
|
724
|
+
const current = level ?? run?.level ?? null;
|
|
725
|
+
if (!current) return `${name}: ${labels.noData}.`;
|
|
726
|
+
const sentences = [hold ? labels.levelSentence({
|
|
727
|
+
name,
|
|
728
|
+
level: labels.levelNames[current],
|
|
729
|
+
duration: formatters.formatDuration(hold.duration),
|
|
730
|
+
atLeast: hold.atLeast
|
|
731
|
+
}) : `${name}: ${labels.levelNames[current]}.`];
|
|
732
|
+
if (state === "insufficient" || !run) sentences.push(`${labels.noHistory}.`);
|
|
733
|
+
else {
|
|
734
|
+
sentences.push(labels.uptimeSentence({
|
|
735
|
+
uptime: formatters.formatUptime(run.uptime),
|
|
736
|
+
span: formatters.formatDuration(run.window.span),
|
|
737
|
+
monitored: formatters.formatDuration(run.monitored),
|
|
738
|
+
mode: run.mode
|
|
739
|
+
}));
|
|
740
|
+
sentences.push(labels.incidentSentence({
|
|
741
|
+
count: run.incidents.length,
|
|
742
|
+
countText: formatNumber(run.incidents.length, "integer"),
|
|
743
|
+
downtime: formatters.formatDuration(run.downtime),
|
|
744
|
+
longest: run.longestIncident ? formatters.formatDuration(run.longestIncident.duration) : null
|
|
745
|
+
}));
|
|
746
|
+
if (run.sla) {
|
|
747
|
+
const over = run.sla.remaining <= 0 && run.sla.used > run.sla.allowed;
|
|
748
|
+
sentences.push(labels.slaSentence({
|
|
749
|
+
target: formatters.formatUptime(run.sla.target),
|
|
750
|
+
met: run.sla.verdict === "met",
|
|
751
|
+
remaining: formatters.formatDuration(over ? run.sla.used - run.sla.allowed : run.sla.remaining),
|
|
752
|
+
over
|
|
753
|
+
}));
|
|
754
|
+
}
|
|
755
|
+
sentences.push(labels.windowSentence({ range: formatters.formatSpan(run.window.start, run.window.end, "day") }));
|
|
756
|
+
}
|
|
757
|
+
if (asOfText) sentences.push(`${labels.asOf} ${asOfText}.`);
|
|
758
|
+
return sentences.join(" ");
|
|
759
|
+
}
|
|
760
|
+
/** The strip's own `aria-label` — chart type, what data, at what granularity, and what it adds up to. */
|
|
761
|
+
function buildStatusChartDescription({ name, run, strip, formatters, labels }) {
|
|
762
|
+
return labels.chartDescription({
|
|
763
|
+
name,
|
|
764
|
+
rangeText: formatters.formatSpan(run.window.start, run.window.end, "day"),
|
|
765
|
+
resolutionText: describeResolution(strip, formatters, labels),
|
|
766
|
+
levels: statusLevelBreakdown(run, formatters, labels).map((entry) => [entry.name, entry.durationText]),
|
|
767
|
+
incidents: labels.incidentSentence({
|
|
768
|
+
count: run.incidents.length,
|
|
769
|
+
countText: formatNumber(run.incidents.length, "integer"),
|
|
770
|
+
downtime: formatters.formatDuration(run.downtime),
|
|
771
|
+
longest: null
|
|
772
|
+
})
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
//#endregion
|
|
776
|
+
export { DEFAULT_STATUS_LABELS, STATUS_LEVELS, STATUS_SEVERITY, buildStatusChartDescription, buildStatusDescription, buildStatusDetailRows, buildStatusExplanation, buildStatusIncidentRows, buildStatusStrip, createStatusFormatters, deriveIncidents, deriveStatusRun, deriveStatusState, describeResolution, formatDuration, formatStripCell, normalizeHistory, resolveStatusHold, resolveStatusLabels, resolveStatusWindow, resolveStep, statusHue, statusLevelBreakdown, statusStepKey };
|