admin update

master
Denis Ranneft 1 month ago
parent 907c192577
commit 409cad4031

@ -39,6 +39,14 @@
.runtime-const-group-title { font-size: 14px; margin: 0 0 8px; font-weight: 600; color: #cfe3ff; }
.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.5); display: flex; align-items: center; justify-content: center; }
.modal { width: 420px; max-width: 95vw; background: #151b2a; border: 1px solid #2a3551; border-radius: 8px; padding: 14px; }
.live-ws-bar { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-start; margin-top: 8px; }
.live-ws-bar-main { flex: 1; min-width: 220px; }
.live-ws-bar-actions { flex-shrink: 0; display: flex; flex-direction: column; gap: 6px; align-items: stretch; }
.live-ws-bar-actions .btn { margin-top: 0; margin-right: 0; }
details.live-details > summary { cursor: pointer; color: #cfe3ff; font-weight: 600; list-style-position: outside; }
details.live-details > summary::-webkit-details-marker { color: #9eb0d6; }
.live-json-pre { max-height: 320px; overflow: auto; font-size: 11px; margin: 8px 0 0; padding: 10px; background: #0f1522; border: 1px solid #2f3b5a; border-radius: 6px; white-space: pre-wrap; word-break: break-word; }
.quest-world-panel { margin-top: 12px; padding-top: 12px; border-top: 1px solid #2a3551; }
</style>
</head>
<body>
@ -86,6 +94,8 @@
_heroLiveWsStatus: "disconnected",
_heroLiveWsError: "",
_heroLiveWsLastAt: null,
_liveSnapshotOpen: false,
_heroQuestWorldOpen: false,
};
state._confirmAction = null;
@ -562,6 +572,26 @@
const [info, engine] = await Promise.all([api("info"), api("engine/status")]);
state.serverInfo = info; state.engine = engine; render();
}
function toggleLiveSnapshotOpen(ev) {
if (ev) ev.preventDefault();
state._liveSnapshotOpen = !state._liveSnapshotOpen;
render();
}
function toggleHeroQuestWorldOpen(ev) {
if (ev) ev.preventDefault();
state._heroQuestWorldOpen = !state._heroQuestWorldOpen;
render();
}
async function pauseServerTime() {
await api("time/pause", { method: "POST", body: "{}" });
await loadServer();
setMessage("Время на сервере приостановлено");
}
async function resumeServerTime() {
await api("time/resume", { method: "POST", body: "{}" });
await loadServer();
setMessage("Время на сервере возобновлено");
}
async function searchHeroes() {
const q = document.getElementById("hero-query")?.value || "";
const data = await api(`heroes?limit=50&offset=0&query=${encodeURIComponent(q)}`);
@ -710,15 +740,33 @@
const last = state._heroLiveWsLastAt ? new Date(state._heroLiveWsLastAt).toLocaleTimeString() : "—";
const heroId = state._heroLiveWsHeroId;
const err = state._heroLiveWsError;
const h = state.selectedHero;
const jsonSnap = h && h.id
? `<pre class="live-json-pre">${e(JSON.stringify(h, null, 2))}</pre>`
: `<p class="muted" style="margin-top:8px">Нет JSON — подключите live или дождитесь первого сообщения.</p>`;
const openAttr = state._liveSnapshotOpen ? " open" : "";
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 class="live-ws-bar">
<div class="live-ws-bar-main">
<button type="button" class="btn" onclick="connectHeroLiveWS()">Подключить</button>
<button type="button" class="btn" onclick="stopHeroLiveWS()">Отключить</button>
<details class="live-details"${openAttr}>
<summary onclick="toggleLiveSnapshotOpen(event)">Снимок героя (JSON)</summary>
${jsonSnap}
</details>
<p class="muted" style="margin-top:8px"><kbd>/admin-ws/hero/{heroId}</kbd>, авторизация как у API.</p>
</div>
<div class="live-ws-bar-actions">
<span class="muted" style="font-size:12px">Сервер (тик движка)</span>
<button type="button" class="btn warn" onclick="withAction(pauseServerTime)">Пауза времени</button>
<button type="button" class="btn" onclick="withAction(resumeServerTime)">Возобновить время</button>
</div>
</div>
</div>`;
}
async function loadHero(heroId) {
@ -1232,16 +1280,20 @@
<div class="card"><h4>Inventory</h4><table class="table"><thead><tr><th>ID</th><th>Slot</th><th>Name</th><th>Rarity</th><th>iLvl</th><th>Actions</th></tr></thead><tbody>${invRows || `<tr><td colspan="6" class="muted">No inventory items</td></tr>`}</tbody></table>${pagerHtml("gearInventory", invPage.page, invPage.total)}</div>
</div>
<div class="card"><h4>Quest log — this hero</h4><table class="table"><thead><tr><th>QuestID</th><th>Title</th><th>Status</th><th>Progress</th><th>Actions</th></tr></thead><tbody>${heroRows || `<tr><td colspan="5" class="muted">No quests for hero</td></tr>`}</tbody></table>${pagerHtml("heroQuests", heroPage.page, heroPage.total)}</div>
<div class="card">
<h4>Give quest from world — this hero</h4>
<button class="btn" onclick="withAction(loadQuestTowns)">Load towns</button>
<button class="btn" onclick="withAction(() => loadHero(state.selectedHeroId))">Reload hero quests</button>
</div>
<div class="row-2">
<div class="card"><h4>Towns</h4><div class="list">${towns || `<div class="list-row"><span class="muted">No towns loaded</span><span></span><span></span><span></span></div>`}</div>${pagerHtml("towns", townsPage.page, townsPage.total)}</div>
<div class="card"><h4>NPCs in town</h4><div class="list">${npcs || `<div class="list-row"><span class="muted">Select town</span><span></span><span></span><span></span></div>`}</div>${pagerHtml("npcs", npcPage.page, npcPage.total)}</div>
</div>
<div class="card"><h4>Quest templates by NPC</h4><table class="table"><thead><tr><th>ID</th><th>Title</th><th>Type</th><th>Rewards</th><th>Action</th></tr></thead><tbody>${templates || `<tr><td colspan="5" class="muted">Select NPC</td></tr>`}</tbody></table>${pagerHtml("npcQuests", tmplPage.page, tmplPage.total)}</div>`;
<details class="card live-details"${state._heroQuestWorldOpen ? " open" : ""}>
<summary onclick="toggleHeroQuestWorldOpen(event)">Квесты из мира <span class="muted">города → NPC → шаблон для этого героя</span></summary>
<div class="quest-world-panel">
<p class="muted">Полный обзор городов и NPC есть на вкладке «Towns».</p>
<button type="button" class="btn" onclick="withAction(loadQuestTowns)">Загрузить города</button>
<button type="button" class="btn" onclick="withAction(() => loadHero(state.selectedHeroId))">Обновить квесты героя</button>
<div class="row-2" style="margin-top:12px">
<div class="card" style="margin-bottom:0"><h4>Города</h4><div class="list">${towns || `<div class="list-row"><span class="muted">Нет данных</span><span></span><span></span><span></span></div>`}</div>${pagerHtml("towns", townsPage.page, townsPage.total)}</div>
<div class="card" style="margin-bottom:0"><h4>NPC в городе</h4><div class="list">${npcs || `<div class="list-row"><span class="muted">Выберите город</span><span></span><span></span><span></span></div>`}</div>${pagerHtml("npcs", npcPage.page, npcPage.total)}</div>
</div>
<h4 style="margin:16px 0 8px">Шаблоны квестов у NPC</h4>
<table class="table"><thead><tr><th>ID</th><th>Title</th><th>Type</th><th>Rewards</th><th>Action</th></tr></thead><tbody>${templates || `<tr><td colspan="5" class="muted">Выберите NPC</td></tr>`}</tbody></table>${pagerHtml("npcQuests", tmplPage.page, tmplPage.total)}
</div>
</details>`;
}
return `
@ -1566,6 +1618,8 @@
window.confirmProceed = confirmProceed;
window.connectHeroLiveWS = connectHeroLiveWS;
window.stopHeroLiveWS = stopHeroLiveWS;
window.toggleLiveSnapshotOpen = toggleLiveSnapshotOpen;
window.toggleHeroQuestWorldOpen = toggleHeroQuestWorldOpen;
render();
</script>
</body>

Loading…
Cancel
Save