Игра приводит путь к вложенным файлам анимаций в нижний регистр целиком, вместе с именем папки мода. На Linux такие файлы не находятся, и сервер сыплет в лог сотнями FileNotFoundException. Кнопка на вкладке «Моды» раскладывает рядом строчные симлинки: файлы модов не меняются, повторный запуск после докачки безопасен, а на файловых системах без различия регистра кнопка честно говорит, что делать нечего. Ожидание строки о готовности стало настройкой профиля со значением по умолчанию 30 минут вместо общих 15. Сборке на три сотни модов пятнадцати не хватает, и панель объявляла бы работающим сервер, который ещё никого не пустит. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
393 lines
16 KiB
HTML
393 lines
16 KiB
HTML
<!doctype html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>PZ Manager</title>
|
|
<link rel="stylesheet" href="/style.css">
|
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text y='26' font-size='26'>🧟</text></svg>">
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Экран входа: он же первичная настройка, когда учёток ещё нет. -->
|
|
<div id="auth-screen" class="auth-screen hidden">
|
|
<form id="auth-form" class="auth-card">
|
|
<h1 class="auth-title">🧟 PZ Manager</h1>
|
|
<p id="auth-subtitle" class="auth-subtitle" data-i18n="auth.subtitle"></p>
|
|
|
|
<label class="field" id="token-field" hidden>
|
|
<span data-i18n="auth.token"></span>
|
|
<input type="text" name="token" autocomplete="off" spellcheck="false">
|
|
<small data-i18n-html="auth.tokenHint"></small>
|
|
</label>
|
|
|
|
<label class="field">
|
|
<span data-i18n="auth.login"></span>
|
|
<input type="text" name="login" autocomplete="username" required>
|
|
</label>
|
|
|
|
<label class="field">
|
|
<span data-i18n="auth.password"></span>
|
|
<input type="password" name="password" autocomplete="current-password" required>
|
|
</label>
|
|
|
|
<p id="auth-error" class="error" hidden></p>
|
|
<button type="submit" class="btn btn-primary btn-block" id="auth-submit" data-i18n="auth.signIn"></button>
|
|
|
|
<label class="lang-picker lang-picker-auth">
|
|
<span data-i18n="lang.label"></span>
|
|
<select class="lang-select">
|
|
<option value="ru">Русский</option>
|
|
<option value="en">English</option>
|
|
</select>
|
|
</label>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Основное приложение -->
|
|
<div id="app" class="app hidden">
|
|
<header class="topbar">
|
|
<div class="brand">🧟 <span>PZ Manager</span></div>
|
|
|
|
<div class="status-pill" id="status-pill">
|
|
<span class="dot" id="status-dot"></span>
|
|
<span id="status-text">—</span>
|
|
</div>
|
|
|
|
<label class="profile-picker">
|
|
<span data-i18n="profiles.picker"></span>
|
|
<select id="profile-select" data-i18n-title="profiles.pickerTitle"></select>
|
|
</label>
|
|
|
|
<div class="controls">
|
|
<button class="btn btn-start" id="btn-start" data-i18n="btn.start"></button>
|
|
<button class="btn btn-stop" id="btn-stop" data-i18n="btn.stop"></button>
|
|
<button class="btn" id="btn-restart" data-i18n="btn.restart"></button>
|
|
</div>
|
|
|
|
<div class="spacer"></div>
|
|
<label class="lang-picker">
|
|
<select class="lang-select" data-i18n-title="lang.label">
|
|
<option value="ru">Русский</option>
|
|
<option value="en">English</option>
|
|
</select>
|
|
</label>
|
|
<span class="user" id="current-user"></span>
|
|
<button class="btn btn-ghost" id="btn-logout" data-i18n="btn.logout"></button>
|
|
</header>
|
|
|
|
<div id="task-banner" class="task-banner" hidden>
|
|
<span class="spinner"></span>
|
|
<span id="task-name"></span>
|
|
<button class="btn btn-ghost btn-sm" id="btn-task-cancel" data-i18n="btn.cancel"></button>
|
|
</div>
|
|
|
|
<nav class="tabs" id="tabs">
|
|
<button class="tab active" data-tab="overview" data-i18n="tab.overview"></button>
|
|
<button class="tab" data-tab="console" data-i18n="tab.console"></button>
|
|
<button class="tab" data-tab="config" data-i18n="tab.config"></button>
|
|
<button class="tab" data-tab="sandbox" data-i18n="tab.sandbox"></button>
|
|
<button class="tab" data-tab="mods" data-i18n="tab.mods"></button>
|
|
<button class="tab" data-tab="backups" data-i18n="tab.backups"></button>
|
|
<button class="tab" data-tab="profiles" data-i18n="tab.profiles"></button>
|
|
<button class="tab" data-tab="settings" data-i18n="tab.settings"></button>
|
|
</nav>
|
|
|
|
<main class="content">
|
|
|
|
<!-- Обзор -->
|
|
<section class="panel active" data-panel="overview">
|
|
<div class="cards">
|
|
<div class="card">
|
|
<h3 data-i18n="overview.stateCard"></h3>
|
|
<div class="stat" id="ov-state">—</div>
|
|
<div class="starting-note" id="ov-note"></div>
|
|
<div class="muted" id="ov-profile"></div>
|
|
<div class="muted" id="ov-uptime"></div>
|
|
<div class="muted" id="ov-pid"></div>
|
|
<div class="muted error" id="ov-lastexit"></div>
|
|
</div>
|
|
<div class="card">
|
|
<h3 data-i18n="overview.playersCard"></h3>
|
|
<div class="stat" id="ov-players-count">0</div>
|
|
<ul class="player-list" id="ov-players"></ul>
|
|
</div>
|
|
<div class="card">
|
|
<h3 data-i18n="overview.processCard"></h3>
|
|
<div class="metric"><span data-i18n="overview.cpu"></span><b id="ov-cpu">—</b></div>
|
|
<div class="metric"><span data-i18n="overview.memory"></span><b id="ov-rss">—</b></div>
|
|
<div class="metric"><span data-i18n="overview.threads"></span><b id="ov-threads">—</b></div>
|
|
</div>
|
|
<div class="card">
|
|
<h3 data-i18n="overview.hostCard"></h3>
|
|
<div class="metric"><span data-i18n="overview.ramFree"></span><b id="ov-mem">—</b></div>
|
|
<div class="metric"><span data-i18n="overview.load"></span><b id="ov-load">—</b></div>
|
|
<div class="metric"><span data-i18n="overview.diskFree"></span><b id="ov-disk">—</b></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" id="install-card">
|
|
<h3 data-i18n="install.card"></h3>
|
|
<p id="install-state" class="muted"></p>
|
|
<button class="btn btn-primary" id="btn-install" data-i18n="install.install"></button>
|
|
<p class="muted small" id="install-hint" data-i18n="install.hintInstall"></p>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Консоль -->
|
|
<section class="panel" data-panel="console">
|
|
<div class="console-toolbar">
|
|
<label class="checkbox"><input type="checkbox" id="console-autoscroll" checked>
|
|
<span data-i18n="console.autoscroll"></span></label>
|
|
<input type="search" id="console-filter" data-i18n-placeholder="console.filter" class="input-sm">
|
|
<button class="btn btn-ghost btn-sm" id="btn-console-clear" data-i18n="console.clear"></button>
|
|
</div>
|
|
<pre id="console-output" class="console"></pre>
|
|
<form class="console-input" id="console-form">
|
|
<input type="text" id="console-command" data-i18n-placeholder="console.placeholder"
|
|
autocomplete="off" spellcheck="false" disabled>
|
|
<button class="btn btn-primary" type="submit" data-i18n="console.send" disabled></button>
|
|
</form>
|
|
<p class="muted small" data-i18n-html="console.help"></p>
|
|
</section>
|
|
|
|
<!-- Конфиг сервера -->
|
|
<section class="panel" data-panel="config">
|
|
<div class="panel-head">
|
|
<div>
|
|
<h2 data-i18n="config.title"></h2>
|
|
<p class="muted small" id="config-path"></p>
|
|
</div>
|
|
<div>
|
|
<input type="search" id="config-filter" data-i18n-placeholder="config.search" class="input-sm">
|
|
<button class="btn btn-primary" id="btn-config-save" data-i18n="btn.save" disabled></button>
|
|
</div>
|
|
</div>
|
|
<p class="muted" id="config-empty" hidden></p>
|
|
<div id="config-fields"></div>
|
|
</section>
|
|
|
|
<!-- Песочница -->
|
|
<section class="panel" data-panel="sandbox">
|
|
<div class="panel-head">
|
|
<div>
|
|
<h2 data-i18n="sandbox.title"></h2>
|
|
<p class="muted small" id="sandbox-path"></p>
|
|
</div>
|
|
<div>
|
|
<input type="search" id="sandbox-filter" data-i18n-placeholder="config.search" class="input-sm">
|
|
<button class="btn btn-primary" id="btn-sandbox-save" data-i18n="btn.save" disabled></button>
|
|
</div>
|
|
</div>
|
|
<p class="muted" id="sandbox-empty" hidden></p>
|
|
<div id="sandbox-fields"></div>
|
|
</section>
|
|
|
|
<!-- Моды -->
|
|
<section class="panel" data-panel="mods">
|
|
<div class="panel-head">
|
|
<div>
|
|
<h2 data-i18n="mods.title"></h2>
|
|
<p class="muted small" data-i18n="mods.subtitle"></p>
|
|
</div>
|
|
<button class="btn btn-primary" id="btn-mods-save" data-i18n="btn.save" disabled></button>
|
|
</div>
|
|
|
|
<p class="muted" id="mods-empty" hidden></p>
|
|
|
|
<div id="mods-body">
|
|
<form class="mod-add" id="mod-add-form">
|
|
<input type="text" id="mod-input" autocomplete="off" data-i18n-placeholder="mods.inputPlaceholder">
|
|
<button class="btn" type="submit" data-i18n="mods.add"></button>
|
|
<button class="btn" type="button" id="btn-add-collection" data-i18n="mods.addCollection"></button>
|
|
</form>
|
|
<p class="muted small" data-i18n="mods.collectionHint"></p>
|
|
|
|
<div class="mod-toolbar">
|
|
<span class="muted small" id="mod-summary"></span>
|
|
<div class="spacer"></div>
|
|
<input type="search" id="mod-filter" class="input-sm" data-i18n-placeholder="mods.search">
|
|
<label class="checkbox"><input type="checkbox" id="mod-only-enabled">
|
|
<span data-i18n="mods.onlyEnabled"></span></label>
|
|
</div>
|
|
|
|
<ul class="mod-list" id="mod-list"></ul>
|
|
|
|
<h3 data-i18n="mods.listTitle"></h3>
|
|
<p class="muted small" data-i18n="mods.listHint"></p>
|
|
<form class="mod-add" id="mod-list-form">
|
|
<textarea id="mod-list-input" rows="3" spellcheck="false"
|
|
data-i18n-placeholder="mods.listPlaceholder"></textarea>
|
|
<button class="btn" type="submit" data-i18n="mods.listApply"></button>
|
|
</form>
|
|
|
|
<h3 data-i18n="mods.mapsTitle"></h3>
|
|
<textarea id="map-list" rows="3" spellcheck="false" data-i18n-placeholder="mods.mapsPlaceholder"></textarea>
|
|
<p class="muted small" data-i18n="mods.mapsHint"></p>
|
|
|
|
<h3 data-i18n="mods.caseTitle"></h3>
|
|
<p class="muted small" data-i18n="mods.caseHint"></p>
|
|
<button class="btn" type="button" id="btn-mods-casefix" data-i18n="mods.caseApply"></button>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Бэкапы -->
|
|
<section class="panel" data-panel="backups">
|
|
<div class="panel-head">
|
|
<div>
|
|
<h2 data-i18n="backups.title"></h2>
|
|
<p class="muted small" id="backup-dir"></p>
|
|
<p class="muted small" data-i18n="backups.note"></p>
|
|
</div>
|
|
<form id="backup-form">
|
|
<input type="text" id="backup-note" data-i18n-placeholder="backups.notePlaceholder" class="input-sm">
|
|
<button class="btn btn-primary" type="submit" data-i18n="backups.create"></button>
|
|
</form>
|
|
</div>
|
|
<table class="table">
|
|
<thead><tr>
|
|
<th data-i18n="backups.colArchive"></th>
|
|
<th data-i18n="backups.colProfile"></th>
|
|
<th data-i18n="backups.colSize"></th>
|
|
<th data-i18n="backups.colCreated"></th>
|
|
<th></th>
|
|
</tr></thead>
|
|
<tbody id="backup-rows"></tbody>
|
|
</table>
|
|
</section>
|
|
|
|
<!-- Профили -->
|
|
<section class="panel" data-panel="profiles">
|
|
<div class="panel-head">
|
|
<div>
|
|
<h2 data-i18n="profiles.title"></h2>
|
|
<p class="muted small" data-i18n="profiles.subtitle"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="profile-cards" class="cards"></div>
|
|
|
|
<h3 data-i18n="import.title"></h3>
|
|
<div class="card import-card">
|
|
<p class="muted small" data-i18n="import.fileHint"></p>
|
|
<input type="file" id="preset-file" accept=".zip,.cfg">
|
|
|
|
<div id="preset-details" hidden>
|
|
<div class="cfg-field" id="preset-pick" hidden>
|
|
<label for="preset-set" data-i18n="import.set"></label>
|
|
<select id="preset-set"></select>
|
|
</div>
|
|
|
|
<div class="preset-summary" id="preset-summary"></div>
|
|
|
|
<div class="fields">
|
|
<div class="cfg-field">
|
|
<label for="preset-target" data-i18n="import.target"></label>
|
|
<select id="preset-target">
|
|
<option value="new" data-i18n="import.targetNew"></option>
|
|
<option value="existing" data-i18n="import.targetExisting"></option>
|
|
</select>
|
|
</div>
|
|
<div class="cfg-field" id="preset-new-fields">
|
|
<label for="preset-id" data-i18n="import.newID"></label>
|
|
<input type="text" id="preset-id" autocomplete="off">
|
|
<div class="hint" data-i18n="profiles.idHint"></div>
|
|
</div>
|
|
<div class="cfg-field" id="preset-existing-fields" hidden>
|
|
<label for="preset-profile" data-i18n="import.profile"></label>
|
|
<select id="preset-profile"></select>
|
|
<div class="hint" data-i18n="import.overwriteHint"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<p class="muted small" data-i18n="import.keep"></p>
|
|
<div class="preset-keep">
|
|
<label class="checkbox"><input type="checkbox" data-keep="ports" checked>
|
|
<span data-i18n="import.keepPorts"></span></label>
|
|
<label class="checkbox"><input type="checkbox" data-keep="access" checked>
|
|
<span data-i18n="import.keepAccess"></span></label>
|
|
<label class="checkbox"><input type="checkbox" data-keep="identity" checked>
|
|
<span data-i18n="import.keepIdentity"></span></label>
|
|
</div>
|
|
|
|
<label class="checkbox"><input type="checkbox" id="preset-activate" checked>
|
|
<span data-i18n="import.activate"></span></label>
|
|
<div class="hint" data-i18n="import.activateHint"></div>
|
|
|
|
<button class="btn btn-primary" id="btn-preset-import" data-i18n="import.apply"></button>
|
|
</div>
|
|
</div>
|
|
|
|
<h3 data-i18n="profiles.newTitle"></h3>
|
|
<form id="profile-add-form" class="fields">
|
|
<div class="cfg-field">
|
|
<label for="new-profile-id" data-i18n="profiles.id"></label>
|
|
<input type="text" id="new-profile-id" placeholder="modded" autocomplete="off">
|
|
<div class="hint" data-i18n="profiles.idHint"></div>
|
|
</div>
|
|
<div class="cfg-field">
|
|
<label for="new-profile-title" data-i18n="profiles.name"></label>
|
|
<input type="text" id="new-profile-title" data-i18n-placeholder="profiles.namePlaceholder" autocomplete="off">
|
|
<div class="hint" data-i18n="profiles.nameHint"></div>
|
|
</div>
|
|
<div class="cfg-field">
|
|
<label for="new-profile-memory" data-i18n="profiles.memory"></label>
|
|
<input type="text" id="new-profile-memory" placeholder="8g" autocomplete="off">
|
|
<div class="hint" data-i18n="profiles.memoryHint"></div>
|
|
</div>
|
|
<div class="cfg-field">
|
|
<label for="new-profile-copy" data-i18n="profiles.copyFrom"></label>
|
|
<select id="new-profile-copy"></select>
|
|
<div class="hint" data-i18n="profiles.copyHint"></div>
|
|
</div>
|
|
<div class="cfg-field">
|
|
<button class="btn btn-primary" type="submit" data-i18n="profiles.create"></button>
|
|
</div>
|
|
</form>
|
|
</section>
|
|
|
|
<!-- Настройки -->
|
|
<section class="panel" data-panel="settings">
|
|
<h2 data-i18n="settings.versionTitle"></h2>
|
|
<div class="card update-card">
|
|
<div class="update-row">
|
|
<div>
|
|
<div class="stat" id="update-current">—</div>
|
|
<p class="muted small" id="update-state" data-i18n="update.checking"></p>
|
|
</div>
|
|
<div class="update-actions">
|
|
<button class="btn btn-sm" id="btn-update-check" data-i18n="update.check"></button>
|
|
<button class="btn btn-sm btn-primary" id="btn-update-apply" data-i18n="update.apply" hidden></button>
|
|
<button class="btn btn-sm" id="btn-manager-restart" data-i18n="update.restart"></button>
|
|
</div>
|
|
</div>
|
|
<p class="muted small" id="update-manual" hidden></p>
|
|
<details id="update-notes" hidden>
|
|
<summary data-i18n="update.whatsNew"></summary>
|
|
<div class="release-notes" id="update-notes-body"></div>
|
|
</details>
|
|
</div>
|
|
|
|
<h2 data-i18n="settings.title"></h2>
|
|
<form id="settings-form" class="fields"></form>
|
|
<button class="btn btn-primary" id="btn-settings-save" data-i18n="settings.save" disabled></button>
|
|
|
|
<h2 data-i18n="settings.usersTitle"></h2>
|
|
<ul class="user-list" id="user-list"></ul>
|
|
<form id="user-add-form" class="mod-add">
|
|
<input type="text" id="new-user-login" data-i18n-placeholder="users.loginPlaceholder" autocomplete="off">
|
|
<input type="password" id="new-user-password" data-i18n-placeholder="users.passwordPlaceholder"
|
|
autocomplete="new-password">
|
|
<button class="btn" type="submit" data-i18n="users.add"></button>
|
|
</form>
|
|
</section>
|
|
|
|
</main>
|
|
</div>
|
|
|
|
<div id="toasts" class="toasts"></div>
|
|
<script src="/i18n.js"></script>
|
|
<script src="/app.js"></script>
|
|
</body>
|
|
</html>
|