-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.js
More file actions
303 lines (260 loc) · 7.04 KB
/
Copy pathapp.js
File metadata and controls
303 lines (260 loc) · 7.04 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
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
const LIST_URL = "./list.txt";
const CHECK_INTERVAL_MS = 60_000;
const CHECK_TIMEOUT_MS = 8_000;
const serviceList = document.querySelector("#service-list");
const overallTitle = document.querySelector("#overall-title");
const overallDescription = document.querySelector("#overall-description");
const overallIndicator = document.querySelector("#overall-indicator");
const lastChecked = document.querySelector("#last-checked");
const refreshButton = document.querySelector("#refresh-button");
let services = [];
let isChecking = false;
function parseList(text) {
return text
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0 && !line.startsWith("#"))
.map((line, index) => {
const separatorIndex = line.indexOf("|");
const label = separatorIndex >= 0
? line.slice(0, separatorIndex).trim()
: "";
const urlText = separatorIndex >= 0
? line.slice(separatorIndex + 1).trim()
: line;
let url;
try {
url = new URL(urlText);
} catch {
return {
id: `invalid-${index}`,
name: label || urlText,
url: urlText,
status: "outage",
message: "URLが不正です",
latency: null
};
}
return {
id: `${url.hostname}-${index}`,
name: label || deriveServiceName(url),
url: url.href,
status: "checking",
message: "確認中",
latency: null
};
});
}
function deriveServiceName(url) {
const hostname = url.hostname.replace(/^www\./, "");
const path = url.pathname
.replace(/\/(healthz?|status|api\/health)\/?$/i, "")
.replace(/^\/+|\/+$/g, "");
return path ? `${hostname}/${path}` : hostname;
}
function escapeHtml(value) {
return value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function renderServices() {
if (services.length === 0) {
serviceList.innerHTML = '<div class="empty-state">監視対象がありません。</div>';
return;
}
serviceList.innerHTML = services.map((service) => {
const latencyText = service.latency === null
? escapeHtml(service.message)
: `${escapeHtml(service.message)} · ${service.latency} ms`;
return `
<article class="service-row">
<div>
<p class="service-name">${escapeHtml(service.name)}</p>
<p class="service-url" title="${escapeHtml(service.url)}">${escapeHtml(service.url)}</p>
</div>
<div>
<div class="service-result">
<span class="status-dot ${service.status}" aria-hidden="true"></span>
<span class="status-text">${statusLabel(service.status)}</span>
</div>
<div class="latency">${latencyText}</div>
</div>
</article>
`;
}).join("");
}
function statusLabel(status) {
switch (status) {
case "operational":
return "正常";
case "degraded":
return "性能低下";
case "outage":
return "障害";
case "unknown":
return "確認不能";
default:
return "確認中";
}
}
async function checkService(service) {
if (service.id.startsWith("invalid-")) {
return service;
}
const controller = new AbortController();
const timeoutId = window.setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
const startedAt = performance.now();
try {
const response = await fetch(service.url, {
method: "GET",
cache: "no-store",
signal: controller.signal,
headers: {
"Accept": "application/json, text/plain, */*"
}
});
const latency = Math.round(performance.now() - startedAt);
if (response.ok) {
return {
...service,
status: latency >= 3_000 ? "degraded" : "operational",
message: latency >= 3_000 ? "応答が遅延しています" : `HTTP ${response.status}`,
latency
};
}
return {
...service,
status: "outage",
message: `HTTP ${response.status}`,
latency
};
} catch (error) {
const timedOut = error instanceof DOMException && error.name === "AbortError";
return {
...service,
status: timedOut ? "outage" : "unknown",
message: timedOut
? `${CHECK_TIMEOUT_MS / 1000}秒でタイムアウト`
: "CORSまたはネットワークエラー",
latency: null
};
} finally {
window.clearTimeout(timeoutId);
}
}
function updateOverallStatus() {
const statuses = services.map((service) => service.status);
if (statuses.includes("checking")) {
setOverall(
"checking",
"状態を確認しています",
"各サービスへ接続しています。"
);
return;
}
if (statuses.includes("outage")) {
setOverall(
"outage",
"一部のサービスで障害が発生しています",
"詳細は各サービスの状態を確認してください。"
);
return;
}
if (statuses.includes("degraded")) {
setOverall(
"degraded",
"一部のサービスで性能が低下しています",
"サービスは利用できますが、応答が遅い可能性があります。"
);
return;
}
if (statuses.includes("unknown")) {
setOverall(
"degraded",
"一部のサービスを確認できません",
"管理者にお問い合わせ下さい。"
);
return;
}
if (services.length === 0) {
setOverall(
"degraded",
"監視対象が設定されていません",
"管理者にお問い合わせください"
);
return;
}
setOverall(
"operational",
"すべてのシステムは正常です",
"現在、確認されている障害はありません。"
);
}
function setOverall(status, title, description) {
overallIndicator.className = `overall-indicator ${status}`;
overallTitle.textContent = title;
overallDescription.textContent = description;
}
function updateLastChecked() {
const formatter = new Intl.DateTimeFormat("ja-JP", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
});
lastChecked.textContent = `最終確認: ${formatter.format(new Date())}`;
}
async function runChecks() {
if (isChecking) {
return;
}
isChecking = true;
refreshButton.disabled = true;
services = services.map((service) => ({
...service,
status: service.id.startsWith("invalid-") ? "outage" : "checking",
message: service.id.startsWith("invalid-") ? "URLが不正です" : "確認中",
latency: null
}));
renderServices();
updateOverallStatus();
services = await Promise.all(services.map(checkService));
renderServices();
updateOverallStatus();
updateLastChecked();
refreshButton.disabled = false;
isChecking = false;
}
async function initialize() {
try {
const response = await fetch(LIST_URL, {
cache: "no-store"
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
services = parseList(await response.text());
renderServices();
updateOverallStatus();
await runChecks();
} catch (error) {
serviceList.innerHTML = `
<div class="empty-state">
内部エラー: ${error instanceof Error ? escapeHtml(error.message) : "不明なエラー"}
</div>
`;
setOverall(
"outage",
"監視設定を読み込めません",
error instanceof Error ? error.message : "不明なエラー"
);
}
}
refreshButton.addEventListener("click", runChecks);
window.setInterval(runChecks, CHECK_INTERVAL_MS);
initialize();