@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.
Files changed (156) hide show
  1. package/dist/_virtual/_rolldown/runtime.js +13 -0
  2. package/dist/components/AnomalyCard/AnomalyCard.chart.d.ts +64 -0
  3. package/dist/components/AnomalyCard/AnomalyCard.chart.js +341 -0
  4. package/dist/components/AnomalyCard/AnomalyCard.d.ts +169 -0
  5. package/dist/components/AnomalyCard/AnomalyCard.js +1019 -0
  6. package/dist/components/AnomalyCard/AnomalyCard.model.d.ts +508 -0
  7. package/dist/components/AnomalyCard/AnomalyCard.model.js +553 -0
  8. package/dist/components/AnomalyCard/index.d.ts +4 -0
  9. package/dist/components/AnomalyCard/index.js +4 -0
  10. package/dist/components/ChartCard/ChartCard.chart.d.ts +72 -0
  11. package/dist/components/ChartCard/ChartCard.chart.js +1333 -0
  12. package/dist/components/ChartCard/ChartCard.d.ts +206 -0
  13. package/dist/components/ChartCard/ChartCard.js +718 -0
  14. package/dist/components/ChartCard/ChartCard.model.d.ts +452 -0
  15. package/dist/components/ChartCard/ChartCard.model.js +1223 -0
  16. package/dist/components/ChartCard/index.d.ts +4 -0
  17. package/dist/components/ChartCard/index.js +4 -0
  18. package/dist/components/ComparisonCard/ComparisonCard.chart.d.ts +80 -0
  19. package/dist/components/ComparisonCard/ComparisonCard.chart.js +337 -0
  20. package/dist/components/ComparisonCard/ComparisonCard.d.ts +200 -0
  21. package/dist/components/ComparisonCard/ComparisonCard.js +1325 -0
  22. package/dist/components/ComparisonCard/ComparisonCard.model.d.ts +309 -0
  23. package/dist/components/ComparisonCard/ComparisonCard.model.js +263 -0
  24. package/dist/components/ComparisonCard/index.d.ts +4 -0
  25. package/dist/components/ComparisonCard/index.js +4 -0
  26. package/dist/components/ForecastCard/ForecastCard.chart.d.ts +82 -0
  27. package/dist/components/ForecastCard/ForecastCard.chart.js +416 -0
  28. package/dist/components/ForecastCard/ForecastCard.d.ts +148 -0
  29. package/dist/components/ForecastCard/ForecastCard.js +1029 -0
  30. package/dist/components/ForecastCard/ForecastCard.model.d.ts +497 -0
  31. package/dist/components/ForecastCard/ForecastCard.model.js +554 -0
  32. package/dist/components/ForecastCard/index.d.ts +4 -0
  33. package/dist/components/ForecastCard/index.js +4 -0
  34. package/dist/components/HighlightCard/HighlightCard.chart.d.ts +122 -0
  35. package/dist/components/HighlightCard/HighlightCard.chart.js +660 -0
  36. package/dist/components/HighlightCard/HighlightCard.d.ts +202 -0
  37. package/dist/components/HighlightCard/HighlightCard.js +1286 -0
  38. package/dist/components/HighlightCard/HighlightCard.model.d.ts +692 -0
  39. package/dist/components/HighlightCard/HighlightCard.model.js +875 -0
  40. package/dist/components/HighlightCard/index.d.ts +4 -0
  41. package/dist/components/HighlightCard/index.js +4 -0
  42. package/dist/components/InsightCard/InsightCard.chart.d.ts +91 -0
  43. package/dist/components/InsightCard/InsightCard.chart.js +471 -0
  44. package/dist/components/InsightCard/InsightCard.d.ts +161 -0
  45. package/dist/components/InsightCard/InsightCard.js +961 -0
  46. package/dist/components/InsightCard/InsightCard.model.d.ts +405 -0
  47. package/dist/components/InsightCard/InsightCard.model.js +421 -0
  48. package/dist/components/InsightCard/index.d.ts +4 -0
  49. package/dist/components/InsightCard/index.js +4 -0
  50. package/dist/components/MetricCard/MetricCard.chart.d.ts +79 -0
  51. package/dist/components/MetricCard/MetricCard.chart.js +435 -0
  52. package/dist/components/MetricCard/MetricCard.d.ts +227 -0
  53. package/dist/components/MetricCard/MetricCard.js +1293 -0
  54. package/dist/components/MetricCard/MetricCard.model.d.ts +521 -0
  55. package/dist/components/MetricCard/MetricCard.model.js +627 -0
  56. package/dist/components/MetricCard/index.d.ts +4 -0
  57. package/dist/components/MetricCard/index.js +4 -0
  58. package/dist/components/ProfileCard/ProfileCard.chart.d.ts +64 -0
  59. package/dist/components/ProfileCard/ProfileCard.chart.js +427 -0
  60. package/dist/components/ProfileCard/ProfileCard.d.ts +140 -0
  61. package/dist/components/ProfileCard/ProfileCard.js +735 -0
  62. package/dist/components/ProfileCard/ProfileCard.model.d.ts +279 -0
  63. package/dist/components/ProfileCard/ProfileCard.model.js +244 -0
  64. package/dist/components/ProfileCard/index.d.ts +4 -0
  65. package/dist/components/ProfileCard/index.js +4 -0
  66. package/dist/components/ProgressCard/ProgressCard.chart.d.ts +73 -0
  67. package/dist/components/ProgressCard/ProgressCard.chart.js +373 -0
  68. package/dist/components/ProgressCard/ProgressCard.d.ts +137 -0
  69. package/dist/components/ProgressCard/ProgressCard.js +908 -0
  70. package/dist/components/ProgressCard/ProgressCard.model.d.ts +385 -0
  71. package/dist/components/ProgressCard/ProgressCard.model.js +389 -0
  72. package/dist/components/ProgressCard/index.d.ts +4 -0
  73. package/dist/components/ProgressCard/index.js +4 -0
  74. package/dist/components/ReportCard/ReportCard.chart.d.ts +45 -0
  75. package/dist/components/ReportCard/ReportCard.chart.js +231 -0
  76. package/dist/components/ReportCard/ReportCard.d.ts +153 -0
  77. package/dist/components/ReportCard/ReportCard.js +1118 -0
  78. package/dist/components/ReportCard/ReportCard.model.d.ts +392 -0
  79. package/dist/components/ReportCard/ReportCard.model.js +399 -0
  80. package/dist/components/ReportCard/index.d.ts +4 -0
  81. package/dist/components/ReportCard/index.js +4 -0
  82. package/dist/components/SankeyCard/SankeyCard.chart.d.ts +75 -0
  83. package/dist/components/SankeyCard/SankeyCard.chart.js +249 -0
  84. package/dist/components/SankeyCard/SankeyCard.d.ts +185 -0
  85. package/dist/components/SankeyCard/SankeyCard.js +788 -0
  86. package/dist/components/SankeyCard/SankeyCard.model.d.ts +501 -0
  87. package/dist/components/SankeyCard/SankeyCard.model.js +789 -0
  88. package/dist/components/SankeyCard/index.d.ts +4 -0
  89. package/dist/components/SankeyCard/index.js +4 -0
  90. package/dist/components/StatusCard/StatusCard.chart.d.ts +60 -0
  91. package/dist/components/StatusCard/StatusCard.chart.js +188 -0
  92. package/dist/components/StatusCard/StatusCard.d.ts +176 -0
  93. package/dist/components/StatusCard/StatusCard.js +989 -0
  94. package/dist/components/StatusCard/StatusCard.model.d.ts +555 -0
  95. package/dist/components/StatusCard/StatusCard.model.js +776 -0
  96. package/dist/components/StatusCard/index.d.ts +4 -0
  97. package/dist/components/StatusCard/index.js +4 -0
  98. package/dist/components/TableCard/TableCard.chart.d.ts +53 -0
  99. package/dist/components/TableCard/TableCard.chart.js +192 -0
  100. package/dist/components/TableCard/TableCard.d.ts +140 -0
  101. package/dist/components/TableCard/TableCard.js +811 -0
  102. package/dist/components/TableCard/TableCard.model.d.ts +381 -0
  103. package/dist/components/TableCard/TableCard.model.js +475 -0
  104. package/dist/components/TableCard/index.d.ts +4 -0
  105. package/dist/components/TableCard/index.js +4 -0
  106. package/dist/components/TargetCard/TargetCard.chart.d.ts +52 -0
  107. package/dist/components/TargetCard/TargetCard.chart.js +273 -0
  108. package/dist/components/TargetCard/TargetCard.d.ts +143 -0
  109. package/dist/components/TargetCard/TargetCard.js +910 -0
  110. package/dist/components/TargetCard/TargetCard.model.d.ts +468 -0
  111. package/dist/components/TargetCard/TargetCard.model.js +581 -0
  112. package/dist/components/TargetCard/index.d.ts +4 -0
  113. package/dist/components/TargetCard/index.js +4 -0
  114. package/dist/components/TreeCard/TreeCard.chart.d.ts +80 -0
  115. package/dist/components/TreeCard/TreeCard.chart.js +178 -0
  116. package/dist/components/TreeCard/TreeCard.d.ts +182 -0
  117. package/dist/components/TreeCard/TreeCard.js +1187 -0
  118. package/dist/components/TreeCard/TreeCard.model.d.ts +330 -0
  119. package/dist/components/TreeCard/TreeCard.model.js +401 -0
  120. package/dist/components/TreeCard/index.d.ts +4 -0
  121. package/dist/components/TreeCard/index.js +4 -0
  122. package/dist/components/TrendCard/TrendCard.chart.d.ts +75 -0
  123. package/dist/components/TrendCard/TrendCard.chart.js +393 -0
  124. package/dist/components/TrendCard/TrendCard.d.ts +224 -0
  125. package/dist/components/TrendCard/TrendCard.js +1053 -0
  126. package/dist/components/TrendCard/TrendCard.model.d.ts +545 -0
  127. package/dist/components/TrendCard/TrendCard.model.js +623 -0
  128. package/dist/components/TrendCard/index.d.ts +4 -0
  129. package/dist/components/TrendCard/index.js +4 -0
  130. package/dist/index.d.ts +76 -0
  131. package/dist/index.js +57 -0
  132. package/dist/lib/vega/ArcVegaChart.d.ts +24 -0
  133. package/dist/lib/vega/ArcVegaChart.js +42 -0
  134. package/dist/lib/vega/ArcVegaSurface.d.ts +13 -0
  135. package/dist/lib/vega/ArcVegaSurface.js +38 -0
  136. package/dist/lib/vega/axisTicks.js +156 -0
  137. package/dist/lib/vega/config.d.ts +29 -0
  138. package/dist/lib/vega/config.js +113 -0
  139. package/dist/lib/vega/embed.d.ts +29 -0
  140. package/dist/lib/vega/embed.js +29 -0
  141. package/dist/lib/vega/engine.d.ts +10 -0
  142. package/dist/lib/vega/engine.js +6 -0
  143. package/dist/lib/vega/formatters.d.ts +125 -0
  144. package/dist/lib/vega/formatters.js +277 -0
  145. package/dist/lib/vega/index.d.ts +11 -0
  146. package/dist/lib/vega/load.d.ts +13 -0
  147. package/dist/lib/vega/load.js +21 -0
  148. package/dist/lib/vega/time.d.ts +45 -0
  149. package/dist/lib/vega/time.js +105 -0
  150. package/dist/lib/vega/tokens.d.ts +73 -0
  151. package/dist/lib/vega/tokens.js +137 -0
  152. package/dist/lib/vega/tooltip.d.ts +45 -0
  153. package/dist/lib/vega/tooltip.js +124 -0
  154. package/dist/lib/vega/useArcVega.d.ts +43 -0
  155. package/dist/lib/vega/useArcVega.js +121 -0
  156. 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 };