mnfst 0.5.119 → 0.5.122
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/lib/manifest.chart.css +195 -0
- package/lib/manifest.charts.js +593 -0
- package/lib/manifest.checkbox.css +2 -2
- package/lib/manifest.colorpicker.js +198 -41
- package/lib/manifest.css +762 -36
- package/lib/manifest.data.js +35 -7
- package/lib/manifest.datepicker.css +504 -0
- package/lib/manifest.datepicker.js +1208 -0
- package/lib/manifest.dialog.css +12 -19
- package/lib/manifest.dropdown.css +12 -19
- package/lib/manifest.integrity.json +9 -5
- package/lib/manifest.js +18 -4
- package/lib/manifest.localization.js +5 -1
- package/lib/manifest.min.css +1 -1
- package/lib/manifest.payments.js +583 -0
- package/lib/manifest.schema.json +77 -0
- package/lib/manifest.sidebar.css +14 -19
- package/lib/manifest.status.js +680 -0
- package/lib/manifest.theme.css +6 -4
- package/lib/manifest.toast.css +1 -1
- package/lib/manifest.tooltip.css +52 -30
- package/lib/manifest.utilities.css +3 -2
- package/lib/manifest.utilities.js +18 -2
- package/package.json +3 -1
|
@@ -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
|
+
})();
|