From 409cad403158c947a1579b8042d86f615d7c19bd Mon Sep 17 00:00:00 2001 From: Denis Ranneft Date: Tue, 31 Mar 2026 01:39:02 +0300 Subject: [PATCH] admin update --- admin-web/index.html | 80 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 67 insertions(+), 13 deletions(-) diff --git a/admin-web/index.html b/admin-web/index.html index 5ffcabd..6f4834c 100644 --- a/admin-web/index.html +++ b/admin-web/index.html @@ -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; } @@ -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 + ? `
${e(JSON.stringify(h, null, 2))}
` + : `

Нет JSON — подключите live или дождитесь первого сообщения.

`; + const openAttr = state._liveSnapshotOpen ? " open" : ""; return `

Live snapshot (WebSocket)

status
${e(status)}${heroId ? " (hero " + e(heroId) + ")" : ""}
lastUpdate
${e(last)}
${err ? `
${e(err)}
` : ""} - - -

Endpoint: /admin-ws/hero/{heroId} (BasicAuth from saved credentials).

+
+
+ + +
+ Снимок героя (JSON) + ${jsonSnap} +
+

/admin-ws/hero/{heroId}, авторизация как у API.

+
+
+ Сервер (тик движка) + + +
+
`; } async function loadHero(heroId) { @@ -1232,16 +1280,20 @@

Inventory

${invRows || ``}
IDSlotNameRarityiLvlActions
No inventory items
${pagerHtml("gearInventory", invPage.page, invPage.total)}

Quest log — this hero

${heroRows || ``}
QuestIDTitleStatusProgressActions
No quests for hero
${pagerHtml("heroQuests", heroPage.page, heroPage.total)}
-
-

Give quest from world — this hero

- - -
-
-

Towns

${towns || `
No towns loaded
`}
${pagerHtml("towns", townsPage.page, townsPage.total)}
-

NPCs in town

${npcs || `
Select town
`}
${pagerHtml("npcs", npcPage.page, npcPage.total)}
-
-

Quest templates by NPC

${templates || ``}
IDTitleTypeRewardsAction
Select NPC
${pagerHtml("npcQuests", tmplPage.page, tmplPage.total)}
`; +
+ Квесты из мира города → NPC → шаблон для этого героя +
+

Полный обзор городов и NPC есть на вкладке «Towns».

+ + +
+

Города

${towns || `
Нет данных
`}
${pagerHtml("towns", townsPage.page, townsPage.total)}
+

NPC в городе

${npcs || `
Выберите город
`}
${pagerHtml("npcs", npcPage.page, npcPage.total)}
+
+

Шаблоны квестов у NPC

+ ${templates || ``}
IDTitleTypeRewardsAction
Выберите NPC
${pagerHtml("npcQuests", tmplPage.page, tmplPage.total)} +
+
`; } return ` @@ -1566,6 +1618,8 @@ window.confirmProceed = confirmProceed; window.connectHeroLiveWS = connectHeroLiveWS; window.stopHeroLiveWS = stopHeroLiveWS; + window.toggleLiveSnapshotOpen = toggleLiveSnapshotOpen; + window.toggleHeroQuestWorldOpen = toggleHeroQuestWorldOpen; render();