Repository navigation
Expand file tree
/
Copy pathstatus.js
More file actions
241 lines (229 loc) · 10.5 KB
/
Copy pathstatus.js
File metadata and controls
241 lines (229 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
/* status.js —— 服务状态页(status.html / status.en.html,中英共用一份,跟随 <html lang>)。
诚实原则:
- 每个组件的状态都由本页实时向后端探测得出,不写死、不谎报;
- 探测失败(网络错 / 超时 / 5xx)= 显示「探测失败」,绝不算正常;
- 后端 503 *_not_configured = 显示「未配置」(该功能未开启,不影响其他服务);
- 需要登录才能探活的组件,用「路由存活」信号(401 JSON 也是存活)。
探测端点均为本站既有公开只读接口:/health、/auth/github/start、/auth/gitee/start、/devices、/afdian/sponsors、/notices。 */
(function () {
'use strict';
var EN = document.documentElement.lang === 'en';
var PAGES_ORIGIN = 'https://mox-site.pages.dev';
var API = location.hostname.slice(-10) === '.pages.dev' ? '' : PAGES_ORIGIN;
var T = {
overallOk: EN ? 'All systems operational' : '全部服务正常',
overallWarn: EN ? 'Operational (some features not configured)' : '基本正常(有功能未配置)',
overallErr: EN ? 'Some services unreachable' : '部分服务不可达',
overallUnk: EN ? 'Probing…' : '探测中…',
ok: EN ? 'Normal' : '正常',
warn: EN ? 'Not configured' : '未配置',
err: EN ? 'Unreachable' : '探测失败',
unk: EN ? 'Probing…' : '探测中…',
at: EN ? 'Probed at ' : '探测时间 ',
noNotice: EN ? 'No notices yet. Releases, maintenance and incidents are announced here.' : '暂无公告。版本发布、维护通知与异常都会在这里公布。',
loadFail: EN ? 'Failed to load notices' : '公告加载失败',
again: EN ? 'Retry' : '重新探测'
};
/* 组件表:key → { name(中/英), desc, probe }
probe 返回 Promise<'ok'|'warn'|'err'> */
var COMPS = [
{
key: 'site',
name: ['官网静态页面', 'Website (Cloudflare Pages)'],
desc: ['你正在看的这个页面,由 Pages 全球边缘节点分发。', 'This very page, served from Pages’ global edge.'],
probe: function () {
return fetch(API + '/', { method: 'GET', cache: 'no-store', signal: ac() })
.then(function (r) { return r.ok ? 'ok' : 'err'; })
.catch(function () { return 'err'; });
}
},
{
key: 'api',
name: ['mox-id 身份 API', 'mox-id identity API'],
desc: ['登录、会话、云同步、订单与兑换的后端。', 'Backend for sign-in, sessions, sync, orders and redemption.'],
probe: function () {
return fetch(API + '/health', { cache: 'no-store', signal: ac() })
.then(function (r) {
var ct = r.headers.get('content-type') || '';
if (ct.indexOf('json') < 0) return 'err';
return r.json().then(function (d) { return d && d.ok ? 'ok' : 'err'; },
function () { return 'err'; });
})
.catch(function () { return 'err'; });
}
},
{
key: 'gh',
name: ['GitHub 一键登录', 'GitHub sign-in'],
desc: ['OAuth 授权链路;302 跳转到 github.com 即正常。', 'OAuth chain; a 302 to github.com means healthy.'],
probe: function () { return oauthProbe('/auth/github/start'); }
},
{
key: 'gee',
name: ['Gitee 一键登录', 'Gitee sign-in'],
desc: ['国内更快的平行授权链路。', 'Parallel OAuth chain, faster in mainland China.'],
probe: function () { return oauthProbe('/auth/gitee/start'); }
},
{
key: 'sync',
name: ['云同步', 'Cloud sync'],
desc: ['以 /devices 路由存活为信号(未登录时 401 也代表服务在位)。', 'Liveness via the /devices route (a 401 when signed out still means the service is up).'],
probe: function () {
return fetch(API + '/devices', { cache: 'no-store', signal: ac() })
.then(function (r) {
var ct = r.headers.get('content-type') || '';
if (ct.indexOf('json') < 0) return 'err';
return 'ok'; /* 任何 JSON 响应(含 401)都说明路由与后端在位 */
})
.catch(function () { return 'err'; });
}
},
{
key: 'afdian',
name: ['爱发电赞助墙', 'Afdian sponsor wall'],
desc: ['赞助总额与名单;未配置时其他功能不受影响。', 'Sponsor total and list; when unconfigured, nothing else is affected.'],
probe: function () {
return fetch(API + '/afdian/sponsors', { cache: 'no-store', signal: ac() })
.then(function (r) {
var ct = r.headers.get('content-type') || '';
if (ct.indexOf('json') < 0) return 'err';
return r.json().then(function (d) {
if (r.status === 503 && d && d.error === 'afdian_not_configured') return 'warn';
return r.ok ? 'ok' : 'err';
}, function () { return 'err'; });
})
.catch(function () { return 'err'; });
}
},
{
key: 'notice',
name: ['公告服务', 'Notice service'],
desc: ['本页与官网首页公告栏的数据源。', 'Data source for this page and the homepage notice bar.'],
probe: function () {
return fetch(API + '/notices', { cache: 'no-store', signal: ac() })
.then(function (r) {
var ct = r.headers.get('content-type') || '';
if (ct.indexOf('json') < 0) return 'err';
return r.json().then(function (d) { return d && d.ok ? 'ok' : 'err'; },
function () { return 'err'; });
})
.catch(function () { return 'err'; });
}
}
];
function ac() {
var c = new AbortController();
setTimeout(function () { c.abort(); }, 8000);
return c.signal;
}
/* redirect:'manual' 下 302 会呈现为 opaqueredirect(status 0);
503 = 后端明确回答「未配置」;其余按 err。 */
function oauthProbe(path) {
return fetch(API + path, { redirect: 'manual', cache: 'no-store', signal: ac() })
.then(function (r) {
if (r.type === 'opaqueredirect' || r.status === 0) return 'ok';
if (r.status === 503) return 'warn';
return 'err';
})
.catch(function () { return 'err'; });
}
var LV = { ok: ['ok', T.ok], warn: ['warn', T.warn], err: ['err', T.err], unk: ['unk', T.unk] };
function setRow(key, lv) {
var el = document.getElementById('st-' + key);
if (!el) return;
var pair = LV[lv] || LV.unk;
var spin = lv === 'unk' ? '<span class="spin"></span>' : '';
el.className = 'bdg ' + pair[0];
el.innerHTML = spin + '<span>' + pair[1] + '</span>';
}
function setOverall(results) {
var box = document.getElementById('stOverall');
var time = document.getElementById('stTime');
if (!box) return;
var hasErr = false, hasWarn = false;
for (var i = 0; i < results.length; i++) {
if (results[i] === 'err') hasErr = true;
if (results[i] === 'warn') hasWarn = true;
}
var cls = 'ok', txt = T.overallOk;
if (hasErr) { cls = 'err'; txt = T.overallErr; }
else if (hasWarn) { cls = 'warn'; txt = T.overallWarn; }
box.className = 'bdg ' + cls;
box.textContent = txt;
if (time) time.textContent = T.at + new Date().toLocaleString(EN ? 'en-US' : 'zh-CN', { hour12: false });
}
function renderRows() {
var list = document.getElementById('stList');
if (!list) return;
var html = '';
for (var i = 0; i < COMPS.length; i++) {
var c = COMPS[i];
html += '<div class="st-row">' +
'<div class="nm">' + c.name[EN ? 1 : 0] + '<span class="d">' + c.desc[EN ? 1 : 0] + '</span></div>' +
'<span class="bdg unk" id="st-' + c.key + '"><span class="spin"></span><span>' + T.unk + '</span></span>' +
'</div>';
}
list.innerHTML = html;
}
function probeAll() {
renderRows();
var overall = document.getElementById('stOverall');
if (overall) { overall.className = 'bdg unk'; overall.textContent = T.overallUnk; }
var results = [];
var jobs = COMPS.map(function (c, i) {
return Promise.resolve()
.then(c.probe)
.then(function (lv) { results[i] = lv; setRow(c.key, lv); })
.catch(function () { results[i] = 'err'; setRow(c.key, 'err'); });
});
Promise.all(jobs).then(function () { setOverall(results); });
}
/* ===== 公告列表 ===== */
function renderNotices() {
var box = document.getElementById('noticeList');
if (!box) return;
fetch(API + '/notices', { cache: 'no-store', signal: ac() })
.then(function (r) {
var ct = r.headers.get('content-type') || '';
if (ct.indexOf('json') < 0) throw new Error('not_json');
return r.json();
})
.then(function (d) {
var ns = (d && d.notices) || [];
if (!ns.length) { box.innerHTML = '<p class="sub-empty">' + T.noNotice + '</p>'; return; }
ns.sort(function (a, b) { return (b.created_at || 0) - (a.created_at || 0); });
var html = '';
for (var i = 0; i < ns.length; i++) {
var n = ns[i];
var lv = n.level === 'critical' ? 'critical' : (n.level === 'warn' ? 'warn' : 'info');
var lvTxt = lv === 'critical' ? (EN ? 'Urgent' : '紧急')
: lv === 'warn' ? (EN ? 'Important' : '重要') : (EN ? 'Notice' : '公告');
html += '<div class="notice"><div class="nt">' +
'<span class="lv ' + lv + '">' + lvTxt + '</span>' +
'<b>' + esc(n.title || '') + '</b>' +
(n.created_at ? '<time>' + new Date(n.created_at).toLocaleString(EN ? 'en-US' : 'zh-CN', { hour12: false }) + '</time>' : '') +
'</div>' +
(n.body ? '<div class="nb">' + esc(n.body) + '</div>' : '') +
'</div>';
}
box.innerHTML = html;
})
.catch(function () {
box.innerHTML = '<p class="sub-empty">' + T.loadFail + ' · <a href="#" id="noticeRetry">' + T.again + '</a></p>';
var r = document.getElementById('noticeRetry');
if (r) r.addEventListener('click', function (e) { e.preventDefault(); renderNotices(); });
});
}
function esc(s) {
return String(s).replace(/[&<>"']/g, function (ch) {
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch];
});
}
function boot() { probeAll(); renderNotices(); }
/* 重新探测:按钮带 href 兜底(无 JS 时整页刷新),有 JS 时原地重探 */
var rf = document.getElementById('stRefresh');
if (rf) rf.addEventListener('click', function (e) { e.preventDefault(); probeAll(); });
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else { boot(); }
})();