39 lines
2.6 KiB
TypeScript
39 lines
2.6 KiB
TypeScript
/** Operator UI. The token lives only in this page's memory, never in a URL or storage. */
|
|
export function showAdmin(root: HTMLElement) {
|
|
root.replaceChildren();
|
|
const panel = document.createElement('section'); panel.className = 'card picker-panel'; panel.style.maxWidth = '850px';
|
|
panel.innerHTML = `<h1>Администрирование</h1><label class="field">Токен оператора <input type="password" autocomplete="off" aria-label="Токен оператора"></label><button class="btn" data-refresh>Обновить</button><p role="status"></p><div data-results></div><a class="btn" href="/?play">Комнаты</a>`;
|
|
root.appendChild(panel);
|
|
const status = panel.querySelector<HTMLElement>('[role=status]')!;
|
|
const results = panel.querySelector<HTMLElement>('[data-results]')!;
|
|
const request = async (path: string, method = 'GET') => {
|
|
const token = panel.querySelector('input')!.value;
|
|
const response = await fetch(path, { method, headers: { Authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(25_000) });
|
|
const data = await response.json();
|
|
if (!response.ok) throw new Error(data.error ?? `HTTP ${response.status}`);
|
|
return data;
|
|
};
|
|
const refresh = async () => {
|
|
results.replaceChildren(); status.textContent = 'Загрузка…';
|
|
try {
|
|
const data = await request('/api/admin');
|
|
status.textContent = `Снимки: ${(data.saveBytes / 1048576).toFixed(1)} / ${(data.saveQuotaBytes / 1048576).toFixed(1)} МиБ`;
|
|
for (const room of data.rooms) {
|
|
const row = document.createElement('p');
|
|
row.textContent = `${room.id} · ${room.cityId} · ${room.players} игроков · тик ${room.tick} · сохранён ${room.savedTick} · ошибок записи ${room.saveErrors} `;
|
|
const button = document.createElement('button'); button.className = 'btn'; button.textContent = 'Сохранить'; button.disabled = !room.active;
|
|
button.onclick = async () => {
|
|
button.disabled = true;
|
|
try { await request(`/api/admin/rooms/${encodeURIComponent(room.id)}/checkpoint`, 'POST'); await refresh(); }
|
|
catch (e) { status.textContent = (e as Error).message; button.disabled = false; }
|
|
};
|
|
row.appendChild(button); results.appendChild(row);
|
|
}
|
|
for (const snapshot of data.snapshots) if (snapshot.error) {
|
|
const row = document.createElement('p'); row.textContent = `${snapshot.id}: ${snapshot.error}`; results.appendChild(row);
|
|
}
|
|
} catch (e) { status.textContent = (e as Error).message; }
|
|
};
|
|
panel.querySelector<HTMLButtonElement>('[data-refresh]')!.onclick = () => { void refresh(); };
|
|
}
|