|
|
|
@ -81,6 +81,11 @@
|
|
|
|
confirm: { open: false, title: "", message: "" },
|
|
|
|
confirm: { open: false, title: "", message: "" },
|
|
|
|
_heroPollTimer: null,
|
|
|
|
_heroPollTimer: null,
|
|
|
|
_heroPollUntil: null,
|
|
|
|
_heroPollUntil: null,
|
|
|
|
|
|
|
|
_heroLiveWs: null,
|
|
|
|
|
|
|
|
_heroLiveWsHeroId: null,
|
|
|
|
|
|
|
|
_heroLiveWsStatus: "disconnected",
|
|
|
|
|
|
|
|
_heroLiveWsError: "",
|
|
|
|
|
|
|
|
_heroLiveWsLastAt: null,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
state._confirmAction = null;
|
|
|
|
state._confirmAction = null;
|
|
|
|
|
|
|
|
|
|
|
|
@ -568,7 +573,21 @@
|
|
|
|
state._heroPollTimer = null;
|
|
|
|
state._heroPollTimer = null;
|
|
|
|
state._heroPollUntil = null;
|
|
|
|
state._heroPollUntil = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function stopHeroLiveWS() {
|
|
|
|
|
|
|
|
if (state._heroLiveWs) {
|
|
|
|
|
|
|
|
try { state._heroLiveWs.close(); } catch (err) {}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
state._heroLiveWs = null;
|
|
|
|
|
|
|
|
state._heroLiveWsHeroId = null;
|
|
|
|
|
|
|
|
state._heroLiveWsStatus = "disconnected";
|
|
|
|
|
|
|
|
state._heroLiveWsError = "";
|
|
|
|
|
|
|
|
state._heroLiveWsLastAt = null;
|
|
|
|
|
|
|
|
render();
|
|
|
|
|
|
|
|
}
|
|
|
|
function startHeroMovementPoll(durationSec = 55) {
|
|
|
|
function startHeroMovementPoll(durationSec = 55) {
|
|
|
|
|
|
|
|
if (state._heroLiveWs && state._heroLiveWs.readyState === WebSocket.OPEN) {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
stopHeroMovementPoll();
|
|
|
|
stopHeroMovementPoll();
|
|
|
|
state._heroPollUntil = Date.now() + durationSec * 1000;
|
|
|
|
state._heroPollUntil = Date.now() + durationSec * 1000;
|
|
|
|
state._heroPollTimer = setInterval(async () => {
|
|
|
|
state._heroPollTimer = setInterval(async () => {
|
|
|
|
@ -586,6 +605,55 @@
|
|
|
|
}, 1000);
|
|
|
|
}, 1000);
|
|
|
|
render();
|
|
|
|
render();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function connectHeroLiveWS() {
|
|
|
|
|
|
|
|
if (!state.selectedHeroId) { setMessage("Select hero first"); return; }
|
|
|
|
|
|
|
|
if (!state.auth.username || !state.auth.password) { setMessage("Set admin credentials first"); return; }
|
|
|
|
|
|
|
|
stopHeroMovementPoll();
|
|
|
|
|
|
|
|
stopHeroLiveWS();
|
|
|
|
|
|
|
|
const proto = location.protocol === "https:" ? "wss" : "ws";
|
|
|
|
|
|
|
|
const auth = btoa(`${state.auth.username}:${state.auth.password}`);
|
|
|
|
|
|
|
|
const url = `${proto}://${location.host}/admin-ws/hero/${state.selectedHeroId}?auth=${encodeURIComponent(auth)}`;
|
|
|
|
|
|
|
|
const ws = new WebSocket(url);
|
|
|
|
|
|
|
|
state._heroLiveWs = ws;
|
|
|
|
|
|
|
|
state._heroLiveWsHeroId = state.selectedHeroId;
|
|
|
|
|
|
|
|
state._heroLiveWsStatus = "connecting";
|
|
|
|
|
|
|
|
state._heroLiveWsError = "";
|
|
|
|
|
|
|
|
state._heroLiveWsLastAt = null;
|
|
|
|
|
|
|
|
render();
|
|
|
|
|
|
|
|
ws.onopen = () => {
|
|
|
|
|
|
|
|
state._heroLiveWsStatus = "connected";
|
|
|
|
|
|
|
|
render();
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
ws.onclose = () => {
|
|
|
|
|
|
|
|
state._heroLiveWsStatus = "disconnected";
|
|
|
|
|
|
|
|
render();
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
ws.onerror = () => {
|
|
|
|
|
|
|
|
state._heroLiveWsStatus = "error";
|
|
|
|
|
|
|
|
state._heroLiveWsError = "WebSocket error";
|
|
|
|
|
|
|
|
render();
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
ws.onmessage = (evt) => {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const data = JSON.parse(evt.data);
|
|
|
|
|
|
|
|
if (data && data.error) {
|
|
|
|
|
|
|
|
state._heroLiveWsStatus = "error";
|
|
|
|
|
|
|
|
state._heroLiveWsError = String(data.error);
|
|
|
|
|
|
|
|
render();
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (data && data.id && state.selectedHeroId === data.id) {
|
|
|
|
|
|
|
|
state.selectedHero = data;
|
|
|
|
|
|
|
|
state._heroLiveWsLastAt = Date.now();
|
|
|
|
|
|
|
|
render();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
|
|
state._heroLiveWsStatus = "error";
|
|
|
|
|
|
|
|
state._heroLiveWsError = "Failed to parse WS payload";
|
|
|
|
|
|
|
|
render();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
function formatRemainingMs(ms) {
|
|
|
|
function formatRemainingMs(ms) {
|
|
|
|
if (ms == null || !Number.isFinite(ms)) return "—";
|
|
|
|
if (ms == null || !Number.isFinite(ms)) return "—";
|
|
|
|
if (ms <= 0) return "истекло";
|
|
|
|
if (ms <= 0) return "истекло";
|
|
|
|
@ -637,9 +705,28 @@
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return `<div class="card" style="margin-top:10px"><h4>Путь, город, отдых</h4>${rows.join("")}${pollNote}</div>`;
|
|
|
|
return `<div class="card" style="margin-top:10px"><h4>Путь, город, отдых</h4>${rows.join("")}${pollNote}</div>`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function heroLiveWsCardHtml() {
|
|
|
|
|
|
|
|
const status = state._heroLiveWsStatus || "disconnected";
|
|
|
|
|
|
|
|
const last = state._heroLiveWsLastAt ? new Date(state._heroLiveWsLastAt).toLocaleTimeString() : "—";
|
|
|
|
|
|
|
|
const heroId = state._heroLiveWsHeroId;
|
|
|
|
|
|
|
|
const err = state._heroLiveWsError;
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
|
|
|
<div class="card" style="margin-top:10px">
|
|
|
|
|
|
|
|
<h4>Live snapshot (WebSocket)</h4>
|
|
|
|
|
|
|
|
<div class="kv"><kbd>status</kbd><div>${e(status)}${heroId ? " (hero " + e(heroId) + ")" : ""}</div></div>
|
|
|
|
|
|
|
|
<div class="kv"><kbd>lastUpdate</kbd><div>${e(last)}</div></div>
|
|
|
|
|
|
|
|
${err ? `<div class="status-err">${e(err)}</div>` : ""}
|
|
|
|
|
|
|
|
<button class="btn" onclick="connectHeroLiveWS()">Connect live</button>
|
|
|
|
|
|
|
|
<button class="btn" onclick="stopHeroLiveWS()">Disconnect</button>
|
|
|
|
|
|
|
|
<p class="muted" style="margin-top:6px">Endpoint: <kbd>/admin-ws/hero/{heroId}</kbd> (BasicAuth from saved credentials).</p>
|
|
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
|
|
}
|
|
|
|
async function loadHero(heroId) {
|
|
|
|
async function loadHero(heroId) {
|
|
|
|
if (!heroId) return;
|
|
|
|
if (!heroId) return;
|
|
|
|
if (state.selectedHeroId != null && state.selectedHeroId !== heroId) stopHeroMovementPoll();
|
|
|
|
if (state.selectedHeroId != null && state.selectedHeroId !== heroId) {
|
|
|
|
|
|
|
|
stopHeroMovementPoll();
|
|
|
|
|
|
|
|
stopHeroLiveWS();
|
|
|
|
|
|
|
|
}
|
|
|
|
state.selectedHeroId = heroId;
|
|
|
|
state.selectedHeroId = heroId;
|
|
|
|
const [hero, gear, quests] = await Promise.all([api(`heroes/${heroId}`), api(`heroes/${heroId}/gear`), api(`heroes/${heroId}/quests`)]);
|
|
|
|
const [hero, gear, quests] = await Promise.all([api(`heroes/${heroId}`), api(`heroes/${heroId}/gear`), api(`heroes/${heroId}/quests`)]);
|
|
|
|
state.selectedHero = hero; state.gear = gear; state.quests = quests;
|
|
|
|
state.selectedHero = hero; state.gear = gear; state.quests = quests;
|
|
|
|
@ -1176,6 +1263,7 @@
|
|
|
|
<div class="kv"><kbd>Name</kbd><div>${e(h.name)}</div></div>
|
|
|
|
<div class="kv"><kbd>Name</kbd><div>${e(h.name)}</div></div>
|
|
|
|
<div class="kv"><kbd>State</kbd><div>${e(h.state)}</div></div>
|
|
|
|
<div class="kv"><kbd>State</kbd><div>${e(h.state)}</div></div>
|
|
|
|
${heroMovementDetailHtml(h)}
|
|
|
|
${heroMovementDetailHtml(h)}
|
|
|
|
|
|
|
|
${heroLiveWsCardHtml()}
|
|
|
|
<div class="kv"><kbd>Telegram ID</kbd><div>${e(h.telegramId)}</div></div>
|
|
|
|
<div class="kv"><kbd>Telegram ID</kbd><div>${e(h.telegramId)}</div></div>
|
|
|
|
<div class="kv"><kbd>Level</kbd><div>${e(h.level)}</div></div>
|
|
|
|
<div class="kv"><kbd>Level</kbd><div>${e(h.level)}</div></div>
|
|
|
|
<div class="kv"><kbd>HP</kbd><div>${e(h.hp)}/${e(h.maxHp)}</div></div>
|
|
|
|
<div class="kv"><kbd>HP</kbd><div>${e(h.hp)}/${e(h.maxHp)}</div></div>
|
|
|
|
@ -1476,6 +1564,8 @@
|
|
|
|
window.openConfirm = openConfirm;
|
|
|
|
window.openConfirm = openConfirm;
|
|
|
|
window.closeConfirm = closeConfirm;
|
|
|
|
window.closeConfirm = closeConfirm;
|
|
|
|
window.confirmProceed = confirmProceed;
|
|
|
|
window.confirmProceed = confirmProceed;
|
|
|
|
|
|
|
|
window.connectHeroLiveWS = connectHeroLiveWS;
|
|
|
|
|
|
|
|
window.stopHeroLiveWS = stopHeroLiveWS;
|
|
|
|
render();
|
|
|
|
render();
|
|
|
|
</script>
|
|
|
|
</script>
|
|
|
|
</body>
|
|
|
|
</body>
|
|
|
|
|