mnfst 0.5.121 → 0.5.123

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.
@@ -0,0 +1,680 @@
1
+ /* Manifest Status — config
2
+ /* By Andrew Matlock under MIT license
3
+ /* https://manifestx.dev
4
+ /*
5
+ /* Reads the top-level `status` block from manifest.json and normalizes each
6
+ /* named entry into { signals[], rollup, refresh, ... }. Pure signal layer —
7
+ /* no UI. Each entry resolves to $status.<name> (see manifest.status.main.js).
8
+ */
9
+
10
+ (function () {
11
+ 'use strict';
12
+
13
+ // Infer a signal's provider type from which field is present (mirrors the
14
+ // data plugin's field-presence inference).
15
+ function normalizeSignal(sig) {
16
+ if (typeof sig === 'string') return { type: 'probe', url: sig, label: sig };
17
+ if (!sig || typeof sig !== 'object') return { type: 'unknown', label: 'unknown' };
18
+
19
+ if (sig.static) return { type: 'static', state: sig.static, label: sig.label || 'static' };
20
+ if (sig.feed) return { type: 'feed', url: sig.feed, path: sig.path, label: sig.label || sig.feed };
21
+ if (sig.mirror) return { type: 'mirror', mirror: sig.mirror, label: sig.label || sig.mirror };
22
+ if (sig.appwriteService || sig.appwriteTableId)
23
+ return { type: 'appwrite', service: sig.appwriteService || 'health', label: sig.label || 'appwrite' };
24
+ if (sig.mcp) return { type: 'mcp', mcp: sig.mcp, label: sig.label || 'mcp' };
25
+ if (sig.heartbeat)
26
+ return { type: 'heartbeat', heartbeat: sig.heartbeat, expectEvery: sig.expectEvery || 60000, label: sig.label || sig.heartbeat };
27
+ if (sig.url)
28
+ return {
29
+ type: 'probe', url: sig.url, label: sig.label || sig.url,
30
+ expect: sig.expect, degradedAbove: sig.degradedAbove,
31
+ method: sig.method || 'GET', headers: sig.headers, timeout: sig.timeout
32
+ };
33
+ return { type: 'unknown', label: sig.label || 'unknown' };
34
+ }
35
+
36
+ // Normalize a manifest.status entry (string | array | object) into a record.
37
+ function normalizeEntry(name, def) {
38
+ const opts = { rollup: 'worst', refresh: 30000, confirmations: 1, staleAfter: null, history: 90 };
39
+ let signals = [];
40
+
41
+ if (typeof def === 'string') {
42
+ signals = [normalizeSignal(def)];
43
+ } else if (Array.isArray(def)) {
44
+ signals = def.map(normalizeSignal);
45
+ } else if (def && typeof def === 'object') {
46
+ if (Array.isArray(def.signals)) {
47
+ signals = def.signals.map(normalizeSignal);
48
+ if (def.rollup) opts.rollup = def.rollup;
49
+ if (def.refresh) opts.refresh = def.refresh;
50
+ if (def.confirmations) opts.confirmations = def.confirmations;
51
+ if (def.staleAfter) opts.staleAfter = def.staleAfter;
52
+ } else {
53
+ // Single-signal object (carries its own provider field).
54
+ signals = [normalizeSignal(def)];
55
+ if (def.refresh) opts.refresh = def.refresh;
56
+ }
57
+ }
58
+
59
+ if (def && typeof def === 'object' && !Array.isArray(def) && def.history) opts.history = def.history;
60
+ if (opts.staleAfter == null) opts.staleAfter = opts.refresh * 3;
61
+ return { name, signals, ...opts };
62
+ }
63
+
64
+ // Resolve manifest.json. Reuse a cached copy only if it actually carries the
65
+ // `status` block — other plugins may cache a normalized manifest that omits
66
+ // keys they don't consume, so fall through to a fresh fetch otherwise.
67
+ async function ensureStatusManifest() {
68
+ const cached = window.ManifestComponentsRegistry?.manifest || window.__manifestLoaded;
69
+ if (cached && cached.status) return cached;
70
+ try {
71
+ const url = document.querySelector('link[rel="manifest"]')?.getAttribute('href') || '/manifest.json';
72
+ const res = await fetch(url);
73
+ return await res.json();
74
+ } catch (_) {
75
+ return cached || null;
76
+ }
77
+ }
78
+
79
+ async function getStatusConfig() {
80
+ const manifest = await ensureStatusManifest();
81
+ if (!manifest?.status || typeof manifest.status !== 'object') return null;
82
+ const entries = {};
83
+ for (const [name, def] of Object.entries(manifest.status)) {
84
+ entries[name] = normalizeEntry(name, def);
85
+ }
86
+ return { entries, appwriteEndpoint: manifest.appwrite?.endpoint || null };
87
+ }
88
+
89
+ window.ManifestStatusConfig = { getStatusConfig, normalizeEntry, normalizeSignal, ensureStatusManifest };
90
+ })();
91
+
92
+
93
+ /* Manifest Status — store
94
+ /* By Andrew Matlock under MIT license
95
+ /* https://manifestx.dev
96
+ /*
97
+ /* Internal reactive store backing $status. Mirrors store('data'):
98
+ /* named entries + a _version counter bumped on every update.
99
+ */
100
+
101
+ (function () {
102
+ 'use strict';
103
+
104
+ // Ordered severity. maintenance sits between operational and degraded so a
105
+ // worst-of rollup surfaces it over a fully-operational set. unknown = -1.
106
+ const LEVELS = {
107
+ operational: 0,
108
+ maintenance: 0.5,
109
+ degraded: 1,
110
+ partial_outage: 2,
111
+ major_outage: 3,
112
+ unknown: -1
113
+ };
114
+
115
+ function level(state) {
116
+ return Object.prototype.hasOwnProperty.call(LEVELS, state) ? LEVELS[state] : -1;
117
+ }
118
+
119
+ // Placeholder health for an unknown/not-yet-resolved entry.
120
+ function emptyHealth(name) {
121
+ return { name, state: 'unknown', level: -1, up: false, latencyMs: null, message: null, uptime: null, history: [], incidents: [], updatedAt: null, stale: false, signals: [] };
122
+ }
123
+
124
+ function initializeStore() {
125
+ if (!window.Alpine) return;
126
+ if (window.Alpine.store('status')) return;
127
+ window.Alpine.store('status', { _version: 0, _ready: false, entries: {}, incidents: [] });
128
+ }
129
+
130
+ window.ManifestStatusStore = { LEVELS, level, emptyHealth, initializeStore };
131
+ })();
132
+
133
+
134
+ /* Manifest Status — signal resolvers + rollup
135
+ /* By Andrew Matlock under MIT license
136
+ /* https://manifestx.dev
137
+ /*
138
+ /* Each provider type resolves a normalized signal to { state, latencyMs }.
139
+ /* Generic providers (probe/feed/static/heartbeat) are fully implemented;
140
+ /* appwrite resolves via the public /health endpoint; mirror uses a small
141
+ /* registry of known upstream feeds; mcp is feed-based and pluggable.
142
+ */
143
+
144
+ (function () {
145
+ 'use strict';
146
+
147
+ // Map an arbitrary upstream state string onto the Manifest vocabulary.
148
+ function normalizeFeedState(raw) {
149
+ if (!raw) return 'unknown';
150
+ const s = String(raw).toLowerCase();
151
+ if (['operational', 'up', 'pass', 'ok', 'none', 'healthy', 'available'].includes(s)) return 'operational';
152
+ if (['degraded', 'minor', 'slow', 'degraded_performance'].includes(s)) return 'degraded';
153
+ if (['partial', 'partial_outage'].includes(s)) return 'partial_outage';
154
+ if (['major', 'major_outage', 'down', 'critical', 'fail', 'outage', 'unavailable'].includes(s)) return 'major_outage';
155
+ if (['maintenance', 'maintenance_in_progress', 'under_maintenance'].includes(s)) return 'maintenance';
156
+ return 'unknown';
157
+ }
158
+
159
+ function dig(obj, path) {
160
+ if (!path) return obj;
161
+ return path.split('.').reduce((o, k) => (o == null ? o : o[k]), obj);
162
+ }
163
+
164
+ async function probe(sig) {
165
+ const timeout = sig.timeout || 8000;
166
+ const controller = new AbortController();
167
+ const t = setTimeout(() => controller.abort(), timeout);
168
+ const start = (window.performance && performance.now) ? performance.now() : Date.now();
169
+ try {
170
+ const res = await fetch(sig.url, {
171
+ method: sig.method || 'GET',
172
+ headers: sig.headers || undefined,
173
+ signal: controller.signal,
174
+ cache: 'no-store'
175
+ });
176
+ clearTimeout(t);
177
+ const latencyMs = Math.round(((window.performance && performance.now) ? performance.now() : Date.now()) - start);
178
+ const ok = sig.expect ? res.status === sig.expect : res.ok;
179
+ let state;
180
+ if (ok) state = (sig.degradedAbove && latencyMs > sig.degradedAbove) ? 'degraded' : 'operational';
181
+ else if (res.status >= 500) state = 'major_outage';
182
+ else state = 'partial_outage';
183
+ return { state, latencyMs };
184
+ } catch (err) {
185
+ clearTimeout(t);
186
+ // Network error / CORS / timeout: can't distinguish down from blocked,
187
+ // so report unknown rather than a false outage.
188
+ return { state: 'unknown', latencyMs: null, error: err && err.name ? err.name : 'fetch failed' };
189
+ }
190
+ }
191
+
192
+ async function feed(sig) {
193
+ try {
194
+ const res = await fetch(sig.url, { cache: 'no-store' });
195
+ if (!res.ok) return { state: 'unknown', latencyMs: null, error: 'feed ' + res.status };
196
+ const json = await res.json();
197
+ // The feed node may be a bare state string, or an object carrying
198
+ // both a state and a human update message.
199
+ const node = sig.path ? dig(json, sig.path) : json;
200
+ const obj = (node && typeof node === 'object') ? node : null;
201
+ const raw = obj ? (obj.state != null ? obj.state : obj.status) : node;
202
+ const message = (obj && obj.message) ? obj.message : (json.message || null);
203
+ // Optional historical data a backend can hydrate.
204
+ const history = Array.isArray(obj && obj.history) ? obj.history : (Array.isArray(json.history) ? json.history : undefined);
205
+ const uptime = (obj && typeof obj.uptime === 'number') ? obj.uptime : (typeof json.uptime === 'number' ? json.uptime : undefined);
206
+ const incidents = Array.isArray(obj && obj.incidents) ? obj.incidents : (Array.isArray(json.incidents) ? json.incidents : undefined);
207
+ return { state: normalizeFeedState(raw), latencyMs: null, message, history, uptime, incidents };
208
+ } catch (_) {
209
+ return { state: 'unknown', latencyMs: null, error: 'feed failed' };
210
+ }
211
+ }
212
+
213
+ // Atlassian Statuspage v2 indicator → Manifest vocabulary. Most hosted
214
+ // status pages (and their /api/v2/status.json) follow this shape and serve
215
+ // permissive CORS, so they resolve client-side.
216
+ function mapStatuspageIndicator(ind) {
217
+ switch (String(ind || '').toLowerCase()) {
218
+ case 'none': return 'operational';
219
+ case 'minor': return 'degraded';
220
+ case 'major': return 'partial_outage';
221
+ case 'critical': return 'major_outage';
222
+ case 'maintenance': return 'maintenance';
223
+ default: return 'unknown';
224
+ }
225
+ }
226
+
227
+ async function fetchStatuspage(base) {
228
+ const res = await fetch(base.replace(/\/$/, '') + '/api/v2/status.json', { cache: 'no-store' });
229
+ if (!res.ok) return { state: 'unknown', latencyMs: null, error: 'mirror ' + res.status };
230
+ const json = await res.json();
231
+ return { state: mapStatuspageIndicator(json && json.status && json.status.indicator), latencyMs: null };
232
+ }
233
+
234
+ // Known upstream status feeds. Extend this registry, or pass a Statuspage
235
+ // base URL directly as the mirror value.
236
+ const MIRRORS = {
237
+ appwrite: { url: 'https://cloud.appwrite.io/v1/health', path: 'status', map: v => (v === 'pass' ? 'operational' : 'degraded') },
238
+ github: { statuspage: 'https://www.githubstatus.com' },
239
+ cloudflare: { statuspage: 'https://www.cloudflarestatus.com' },
240
+ stripe: { statuspage: 'https://status.stripe.com' },
241
+ openai: { statuspage: 'https://status.openai.com' },
242
+ anthropic: { statuspage: 'https://status.anthropic.com' },
243
+ discord: { statuspage: 'https://discordstatus.com' },
244
+ npm: { statuspage: 'https://status.npmjs.org' },
245
+ vercel: { statuspage: 'https://www.vercel-status.com' },
246
+ netlify: { statuspage: 'https://www.netlifystatus.com' }
247
+ };
248
+
249
+ async function mirror(sig) {
250
+ // A bare URL is treated as a Statuspage base, so any provider works
251
+ // without a registry entry.
252
+ if (/^https?:\/\//.test(sig.mirror)) {
253
+ try { return await fetchStatuspage(sig.mirror); }
254
+ catch (_) { return { state: 'unknown', latencyMs: null, error: 'mirror failed' }; }
255
+ }
256
+ const m = MIRRORS[sig.mirror];
257
+ if (!m) return { state: 'unknown', latencyMs: null, error: 'unknown mirror: ' + sig.mirror };
258
+ try {
259
+ if (m.statuspage) return await fetchStatuspage(m.statuspage);
260
+ const res = await fetch(m.url, { cache: 'no-store' });
261
+ if (!res.ok) return { state: 'unknown', latencyMs: null, error: 'mirror ' + res.status };
262
+ const json = await res.json();
263
+ const raw = m.path ? dig(json, m.path) : json;
264
+ return { state: m.map ? m.map(raw) : normalizeFeedState(raw), latencyMs: null };
265
+ } catch (_) {
266
+ return { state: 'unknown', latencyMs: null, error: 'mirror failed' };
267
+ }
268
+ }
269
+
270
+ // Heartbeats: external systems call $status.beat(key); absence past the
271
+ // window reads as a major outage (dead-man's switch).
272
+ const heartbeats = {};
273
+ function recordHeartbeat(key) { heartbeats[key] = Date.now(); }
274
+ function resolveHeartbeat(sig) {
275
+ const last = heartbeats[sig.heartbeat];
276
+ if (!last) return { state: 'unknown', latencyMs: null, error: 'no heartbeat yet' };
277
+ const overdue = (Date.now() - last) > (sig.expectEvery || 60000);
278
+ return { state: overdue ? 'major_outage' : 'operational', latencyMs: null };
279
+ }
280
+
281
+ async function resolveSignal(sig, ctx) {
282
+ switch (sig.type) {
283
+ case 'static': return { state: sig.state || 'unknown', latencyMs: null };
284
+ case 'probe': return await probe(sig);
285
+ case 'feed': return await feed(sig);
286
+ case 'mirror': return await mirror(sig);
287
+ case 'heartbeat': return resolveHeartbeat(sig);
288
+ case 'appwrite': {
289
+ const base = ctx && ctx.appwriteEndpoint;
290
+ if (!base) return { state: 'unknown', latencyMs: null, error: 'no appwrite endpoint' };
291
+ return await probe({ ...sig, url: base.replace(/\/$/, '') + '/health' });
292
+ }
293
+ case 'mcp':
294
+ if (typeof sig.mcp === 'string' && /^https?:\/\//.test(sig.mcp)) return await feed({ ...sig, url: sig.mcp });
295
+ return { state: 'unknown', latencyMs: null, error: 'mcp provider not configured' };
296
+ default:
297
+ return { state: 'unknown', latencyMs: null };
298
+ }
299
+ }
300
+
301
+ // Fold resolved child signals into one entry health.
302
+ function rollup(entry, resolved) {
303
+ const level = window.ManifestStatusStore.level;
304
+ const known = resolved.filter(r => r.state && r.state !== 'unknown');
305
+
306
+ let winner = null;
307
+ if (known.length) {
308
+ winner = entry.rollup === 'best'
309
+ ? known.reduce((a, b) => (level(b.state) < level(a.state) ? b : a))
310
+ : known.reduce((a, b) => (level(b.state) > level(a.state) ? b : a));
311
+ }
312
+ const state = winner ? winner.state : 'unknown';
313
+ const message = winner ? (winner.message || null) : null;
314
+
315
+ const latencies = resolved.map(r => r.latencyMs).filter(n => typeof n === 'number');
316
+ const latencyMs = latencies.length ? Math.round(latencies.reduce((a, b) => a + b, 0) / latencies.length) : null;
317
+ return { state, level: level(state), up: state === 'operational', latencyMs, message, history: winner ? winner.history : undefined, uptime: winner ? winner.uptime : undefined };
318
+ }
319
+
320
+ window.ManifestStatusSignals = { resolveSignal, rollup, recordHeartbeat, normalizeFeedState, MIRRORS };
321
+ })();
322
+
323
+
324
+ /* Manifest Status — main
325
+ /* By Andrew Matlock under MIT license
326
+ /* https://manifestx.dev
327
+ /*
328
+ /* Wires the store, polls each entry's signals, and registers the $status
329
+ /* magic. $status.<name> → rolled-up health object; $status.overall → worst
330
+ /* across all entries. The plugin renders nothing: the author drives their own
331
+ /* UI from these reactive values (:class, x-show, x-text, etc.).
332
+ */
333
+
334
+ (function () {
335
+ 'use strict';
336
+
337
+ /* Shared global: ManifestUI — universal `_ui` text resolver. Defined once
338
+ and shared across plugins (datepicker/colorpicker/charts/status); the
339
+ `if (!window.ManifestUI)` guard means whichever loads first wins. Lets
340
+ baked-in labels be localized/overridden via any loaded data source's
341
+ `_ui` key, locale-reactive. Kept behaviourally identical across copies. */
342
+ if (!window.ManifestUI) {
343
+ window.ManifestUI = {
344
+ _loadedSourceNames() {
345
+ try {
346
+ const store = window.ManifestDataStore && window.ManifestDataStore.rawDataStore;
347
+ if (store && typeof store.keys === 'function') return [...store.keys()];
348
+ } catch (_) { }
349
+ return [];
350
+ },
351
+ resolve(component, fallbacks) {
352
+ const merged = JSON.parse(JSON.stringify(fallbacks || {}));
353
+ try {
354
+ if (!window.Alpine || typeof Alpine.evaluate !== 'function') return merged;
355
+ try { Alpine.evaluate(document.body, '$locale && $locale.current'); } catch (_) { } // dep → re-resolve on locale switch
356
+ for (const name of this._loadedSourceNames()) {
357
+ let ui;
358
+ try { ui = Alpine.evaluate(document.body, `$x['${name}'] && $x['${name}']._ui && $x['${name}']._ui['${component}']`); } catch (_) { ui = null; }
359
+ if (ui && typeof ui === 'object' && !Array.isArray(ui)) this._deepOverlay(merged, ui);
360
+ }
361
+ } catch (_) { }
362
+ return merged;
363
+ },
364
+ _deepOverlay(target, src) {
365
+ for (const k of Object.keys(src)) {
366
+ if (k.startsWith('$') || k === 'contentType' || k === 'valueOf' || k === 'toString') continue;
367
+ const v = src[k];
368
+ if (typeof v === 'function') continue;
369
+ if (v && typeof v === 'object' && !Array.isArray(v)) {
370
+ if (!target[k] || typeof target[k] !== 'object') target[k] = {};
371
+ this._deepOverlay(target[k], v);
372
+ } else if (v !== undefined && v !== null && v !== '') {
373
+ target[k] = v;
374
+ }
375
+ }
376
+ }
377
+ };
378
+ }
379
+
380
+ let config = null;
381
+ const timers = {};
382
+ const overrides = {}; // name -> forced state (manual override wins)
383
+ const lastKnown = {}; // name -> ts of last non-unknown rollup
384
+ const committed = {}; // name -> currently displayed state (post-hysteresis)
385
+ const pending = {}; // name -> { state, count } candidate awaiting confirmations
386
+ const histories = {}; // name -> ring buffer of observed states
387
+ let incidentSeq = 0;
388
+
389
+ function store() { return window.Alpine && window.Alpine.store('status'); }
390
+
391
+ // Debounce state changes: a new state must be observed `confirmations`
392
+ // times in a row before it replaces the committed state. confirmations:1
393
+ // (default) commits immediately.
394
+ function applyHysteresis(entry, observed) {
395
+ const need = entry.confirmations || 1;
396
+ const prev = committed[entry.name];
397
+ if (prev === undefined) { committed[entry.name] = observed; return observed; }
398
+ if (observed === prev) { delete pending[entry.name]; return prev; }
399
+ const p = pending[entry.name];
400
+ if (p && p.state === observed) p.count++;
401
+ else pending[entry.name] = { state: observed, count: 1 };
402
+ if (pending[entry.name].count >= need) {
403
+ committed[entry.name] = observed;
404
+ delete pending[entry.name];
405
+ return observed;
406
+ }
407
+ return prev;
408
+ }
409
+
410
+ function commit(name, health) {
411
+ const s = store();
412
+ if (!s) return;
413
+ s.entries[name] = health;
414
+ s._version++;
415
+ }
416
+
417
+ // --- Incident log (shared on the store, surfaced as $status.incidents) ---
418
+ function logIncident(name, state, message) {
419
+ const s = store(); if (!s) return;
420
+ const open = s.incidents.find(i => i.name === name && !i.resolved);
421
+ const now = Date.now();
422
+ if (open) { open.state = state; if (message) open.message = message; open.updatedAt = now; }
423
+ else { s.incidents.unshift({ id: 'inc_' + (++incidentSeq), name, state, message: message || null, at: now, updatedAt: now, resolved: false, resolvedAt: null }); }
424
+ s._version++;
425
+ }
426
+ function resolveIncident(name) {
427
+ const s = store(); if (!s) return;
428
+ const open = s.incidents.find(i => i.name === name && !i.resolved);
429
+ if (open) { open.resolved = true; open.resolvedAt = Date.now(); s._version++; }
430
+ }
431
+ function mergeIncidents(list) {
432
+ const s = store(); if (!s) return;
433
+ let changed = false;
434
+ for (const inc of list) {
435
+ if (!inc || !inc.id) continue;
436
+ if (!s.incidents.some(x => x.id === inc.id)) { s.incidents.unshift(inc); changed = true; }
437
+ }
438
+ if (changed) s._version++;
439
+ }
440
+
441
+ async function refreshEntry(entry) {
442
+ const ctx = { appwriteEndpoint: config && config.appwriteEndpoint };
443
+ const Signals = window.ManifestStatusSignals;
444
+ const level = window.ManifestStatusStore.level;
445
+
446
+ const resolved = await Promise.all(entry.signals.map(async (sig) => {
447
+ const r = await Signals.resolveSignal(sig, ctx);
448
+ return { type: sig.type, label: sig.label, state: r.state, level: level(r.state), up: r.state === 'operational', latencyMs: r.latencyMs, message: r.message || null, history: r.history, uptime: r.uptime, incidents: r.incidents, error: r.error || null };
449
+ }));
450
+
451
+ const rolled = Signals.rollup(entry, resolved);
452
+
453
+ // Manual override wins and bypasses hysteresis; otherwise debounce the
454
+ // observed state through applyHysteresis.
455
+ let state, message = null;
456
+ if (overrides[entry.name]) {
457
+ state = overrides[entry.name].state;
458
+ message = overrides[entry.name].message || null;
459
+ committed[entry.name] = state;
460
+ delete pending[entry.name];
461
+ } else {
462
+ state = applyHysteresis(entry, rolled.state);
463
+ // Only carry the rolled message if the committed state matches it.
464
+ message = (state === rolled.state) ? (rolled.message || null) : null;
465
+ }
466
+
467
+ const now = Date.now();
468
+ if (state !== 'unknown') lastKnown[entry.name] = now;
469
+ const stale = lastKnown[entry.name] ? (now - lastKnown[entry.name]) > entry.staleAfter : true;
470
+
471
+ // History + uptime: use feed-provided values when present, else accumulate live.
472
+ let hist;
473
+ if (Array.isArray(rolled.history)) {
474
+ hist = rolled.history.slice(-entry.history);
475
+ while (hist.length < entry.history) hist.unshift('unknown');
476
+ } else {
477
+ // Pre-fill with `unknown` so the bar is always full width and fills
478
+ // from the right as checks accrue (StatusPage-style no-data segments).
479
+ hist = histories[entry.name] ? histories[entry.name].slice() : new Array(entry.history).fill('unknown');
480
+ hist.push(state);
481
+ if (hist.length > entry.history) hist = hist.slice(hist.length - entry.history);
482
+ }
483
+ histories[entry.name] = hist;
484
+ const seen = hist.filter(x => x && x !== 'unknown');
485
+ const uptime = (typeof rolled.uptime === 'number') ? rolled.uptime
486
+ : (seen.length ? Math.round((seen.filter(x => x === 'operational').length / seen.length) * 1000) / 10 : null);
487
+
488
+ // Merge any feed-sourced incidents into the shared log.
489
+ const feedIncidents = resolved.flatMap(r => Array.isArray(r.incidents) ? r.incidents : []);
490
+ if (feedIncidents.length) mergeIncidents(feedIncidents);
491
+
492
+ const s = store();
493
+ commit(entry.name, {
494
+ name: entry.name,
495
+ state,
496
+ level: level(state),
497
+ up: state === 'operational',
498
+ latencyMs: rolled.latencyMs,
499
+ message,
500
+ uptime,
501
+ history: hist,
502
+ incidents: s ? s.incidents.filter(i => i.name === entry.name) : [],
503
+ updatedAt: now,
504
+ stale,
505
+ signals: resolved
506
+ });
507
+ }
508
+
509
+ function refreshAll() {
510
+ if (!config) return;
511
+ for (const entry of Object.values(config.entries)) refreshEntry(entry);
512
+ }
513
+
514
+ function startPolling() {
515
+ if (!config) return;
516
+ for (const entry of Object.values(config.entries)) {
517
+ refreshEntry(entry);
518
+ if (entry.refresh && entry.refresh > 0) {
519
+ timers[entry.name] = setInterval(() => refreshEntry(entry), entry.refresh);
520
+ }
521
+ }
522
+ const s = store();
523
+ if (s) { s._ready = true; s._version++; }
524
+ }
525
+
526
+ function overall() {
527
+ const s = store();
528
+ const level = window.ManifestStatusStore.level;
529
+ if (!s) return 'unknown';
530
+ const known = Object.values(s.entries).filter(v => v.state && v.state !== 'unknown');
531
+ if (!known.length) return 'unknown';
532
+ return known.reduce((a, b) => (level(b.state) > level(a.state) ? b : a)).state;
533
+ }
534
+
535
+ // Default English labels per state; overridable per-locale via any loaded
536
+ // data source's `_ui.status.label` (same mechanism as datepicker/colorpicker).
537
+ const UI_FALLBACK = {
538
+ label: {
539
+ operational: 'Operational',
540
+ degraded: 'Degraded',
541
+ partial_outage: 'Partial Outage',
542
+ major_outage: 'Major Outage',
543
+ maintenance: 'Maintenance',
544
+ unknown: 'Unknown'
545
+ }
546
+ };
547
+
548
+ // Resolve `$x`/`$locale`/`${…}` reference strings in `_ui` values (same
549
+ // treatment as the colorpicker's _resolveRefString — kept semantically
550
+ // identical). Reading inside the caller's effect registers the deps.
551
+ function _resolveRefString(val) {
552
+ if (typeof val !== 'string' || val.length === 0) return val;
553
+ const trimmed = val.trim();
554
+ const isBareRef = trimmed.startsWith('$x.') || trimmed.startsWith('$locale')
555
+ || trimmed.startsWith('$x[') || trimmed.startsWith('$locale[');
556
+ const hasInterp = /\$\{[^}]+\}/.test(trimmed);
557
+ if (!isBareRef && !hasInterp) return val;
558
+ try {
559
+ if (window.Alpine?.evaluate) {
560
+ const expr = isBareRef && !hasInterp ? trimmed : '`' + trimmed + '`';
561
+ const out = Alpine.evaluate(document.body, expr);
562
+ if (out == null) return val;
563
+ return typeof out === 'string' ? out : String(out);
564
+ }
565
+ } catch { }
566
+ return val;
567
+ }
568
+
569
+ // Human label for a state string, localized through `_ui` when available.
570
+ // Falls back to the English default, then a generic title-case for any
571
+ // custom/unrecognized state.
572
+ function formatStateLabel(state) {
573
+ // Dep on the data store version so labels re-resolve when a `_ui`
574
+ // source loads late or reloads on locale switch (same fix as the
575
+ // datepicker's ui effect).
576
+ try { void (window.Alpine && Alpine.store('data')?._dataVersion); } catch (_) { }
577
+ const ui = window.ManifestUI ? window.ManifestUI.resolve('status', UI_FALLBACK) : UI_FALLBACK;
578
+ const labels = (ui && ui.label) || UI_FALLBACK.label;
579
+ if (labels[state]) return _resolveRefString(labels[state]);
580
+ return String(state == null ? '' : state).replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
581
+ }
582
+
583
+ // Reserved accessor names — never treated as entry names or enumerated, so
584
+ // `x-for="(service, name) in $status"` iterates only real services.
585
+ const RESERVED = new Set(['overall', 'all', 'ready', 'incidents', 'label', 'set', 'clear', 'refresh', 'beat']);
586
+
587
+ function setOverride(name, state, message) {
588
+ overrides[name] = { state, message: message || null };
589
+ committed[name] = state;
590
+ delete pending[name];
591
+ // An operator update is an incident: open/append, or resolve when set back to operational.
592
+ if (state === 'operational') { if (message) logIncident(name, state, message); resolveIncident(name); }
593
+ else { logIncident(name, state, message); }
594
+ const e = config && config.entries[name];
595
+ if (e) refreshEntry(e);
596
+ }
597
+
598
+ function clearOverride(name) {
599
+ delete overrides[name];
600
+ delete committed[name]; // re-seed from next observation
601
+ delete pending[name];
602
+ resolveIncident(name);
603
+ const e = config && config.entries[name];
604
+ if (e) refreshEntry(e);
605
+ }
606
+
607
+ function registerMagic() {
608
+ if (!window.Alpine) return;
609
+ window.Alpine.magic('status', () => {
610
+ const s = store();
611
+ const entries = () => (s ? s.entries : {});
612
+ return new Proxy({}, {
613
+ get(_, prop) {
614
+ if (prop === Symbol.toPrimitive) return () => overall();
615
+ if (prop === Symbol.iterator) { void (s && s._version); return Object.values(entries())[Symbol.iterator].bind(Object.values(entries())); }
616
+ if (typeof prop === 'symbol') return undefined;
617
+
618
+ // Touch the version counter so every read is a reactive dep.
619
+ void (s && s._version);
620
+
621
+ if (prop === 'overall') return overall();
622
+ if (prop === 'all') return entries();
623
+ if (prop === 'incidents') return s ? s.incidents : [];
624
+ if (prop === 'label') return formatStateLabel;
625
+ if (prop === 'ready') return !!(s && s._ready);
626
+ if (prop === 'set') return setOverride;
627
+ if (prop === 'clear') return clearOverride;
628
+ if (prop === 'refresh') return (name) => { if (name) { const e = config && config.entries[name]; if (e) refreshEntry(e); } else refreshAll(); };
629
+ if (prop === 'beat') return (key) => window.ManifestStatusSignals.recordHeartbeat(key);
630
+
631
+ return entries()[prop] || window.ManifestStatusStore.emptyHealth(String(prop));
632
+ },
633
+ // Enumerate only real entries so x-for over $status yields services.
634
+ ownKeys() { void (s && s._version); return Object.keys(entries()); },
635
+ getOwnPropertyDescriptor(_, prop) {
636
+ if (typeof prop === 'string' && !RESERVED.has(prop) && Object.prototype.hasOwnProperty.call(entries(), prop)) {
637
+ return { enumerable: true, configurable: true, value: entries()[prop] };
638
+ }
639
+ return undefined;
640
+ },
641
+ has(_, prop) { return Object.prototype.hasOwnProperty.call(entries(), prop); }
642
+ });
643
+ });
644
+ }
645
+
646
+ async function init() {
647
+ window.ManifestStatusStore.initializeStore();
648
+ registerMagic();
649
+ config = await window.ManifestStatusConfig.getStatusConfig();
650
+ if (!config) return; // no status block → plugin stays inert
651
+ startPolling();
652
+ }
653
+
654
+ let initialized = false;
655
+ function ensureInitialized() {
656
+ if (initialized) return;
657
+ if (!window.Alpine || typeof window.Alpine.magic !== 'function') return;
658
+ initialized = true;
659
+ init();
660
+ }
661
+
662
+ window.ensureManifestStatusInitialized = ensureInitialized;
663
+
664
+ if (document.readyState === 'loading') {
665
+ document.addEventListener('DOMContentLoaded', ensureInitialized);
666
+ }
667
+ document.addEventListener('alpine:init', ensureInitialized);
668
+
669
+ if (window.Alpine && typeof window.Alpine.magic === 'function') {
670
+ setTimeout(ensureInitialized, 0);
671
+ } else {
672
+ const checkAlpine = setInterval(() => {
673
+ if (window.Alpine && typeof window.Alpine.magic === 'function') {
674
+ clearInterval(checkAlpine);
675
+ ensureInitialized();
676
+ }
677
+ }, 10);
678
+ setTimeout(() => clearInterval(checkAlpine), 5000);
679
+ }
680
+ })();