fix: 7 bug fixes + port config with auto-restart

- HTTPException import added (was causing 500 on missing dashboard)
- game_manager.remove_plugin() restored (was AttributeError)
- api.js checks HTTP status codes, fails fast on errors
- sidebar game highlight only matches selected gameId
- scene resize handler uses stored canvas dims (was NaN)
- forwarder socket properly closed on listener stop
- telemetry port: use_unified_port toggle, off = per-game port, auto-restart on switch
This commit is contained in:
2026-07-26 17:23:14 +08:00
parent 41da7c03d5
commit e28be26ab3
8 changed files with 51 additions and 31 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ import os
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, Request
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse, FileResponse
+1
View File
@@ -17,6 +17,7 @@ DEFAULT_CONFIG: dict[str, Any] = {
"theme": "dark",
"sidebar_collapsed": False,
"telemetry_port": 20777,
"use_unified_port": True,
"telemetry_host": "0.0.0.0",
"server_host": "0.0.0.0",
"server_port": 9527,
+14
View File
@@ -130,6 +130,20 @@ class GamePluginManager:
self._discover()
self._parser_cache.clear()
def remove_plugin(self, plugin_id: str) -> bool:
self._discover()
gp = self._plugins.get(plugin_id)
if gp and gp.is_builtin:
logger.warning("Cannot remove builtin plugin: %s", plugin_id)
return False
dest_dir = USER_DIR / plugin_id
if dest_dir.exists():
shutil.rmtree(dest_dir)
self._discover()
logger.info("Plugin removed: %s", plugin_id)
return True
return False
def install_plugin_zip(self, zip_data: bytes) -> bool:
try:
with zipfile.ZipFile(io.BytesIO(zip_data), 'r') as zf:
+3
View File
@@ -115,6 +115,9 @@ class TelemetryListener:
if self._transport:
self._transport.close()
self._transport = None
if self.forwarder._sock:
self.forwarder._sock.close()
self.forwarder._sock = None
logger.info("Telemetry listener stopped")
def _get_parser(self, key: str) -> BaseParser:
+15 -24
View File
@@ -1,33 +1,24 @@
const API = {
_base: '/api',
async get(path) {
const res = await fetch(`${this._base}${path}`);
async _req(method, path, body) {
const opts = { method };
if (body) {
opts.headers = { 'Content-Type': 'application/json' };
opts.body = JSON.stringify(body);
}
const res = await fetch(`${this._base}${path}`, opts);
if (!res.ok) {
const text = await res.text().catch(() => '');
throw new Error(`${method} ${path} ${res.status}: ${text.slice(0, 200)}`);
}
return res.json();
},
async post(path, data) {
const res = await fetch(`${this._base}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return res.json();
},
async put(path, data) {
const res = await fetch(`${this._base}${path}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return res.json();
},
async del(path) {
const res = await fetch(`${this._base}${path}`, { method: 'DELETE' });
return res.json();
},
async get(path) { return this._req('GET', path); },
async post(path, data) { return this._req('POST', path, data); },
async put(path, data) { return this._req('PUT', path, data); },
async del(path) { return this._req('DELETE', path); },
async getStatus() { return this.get('/status'); },
async getConfig() { return this.get('/config'); },
+10 -2
View File
@@ -3,6 +3,7 @@ const Sidebar = {
_gameListEl: null,
_gameToggleEl: null,
_gameLabelEl: null,
_gameData: [],
init() {
this._el = document.getElementById('sidebar');
@@ -44,6 +45,7 @@ const Sidebar = {
async _loadGames() {
const games = await API.getGames();
this._gameData = games;
this._gameListEl.innerHTML = '';
if (!Array.isArray(games) || games.length === 0) {
@@ -80,11 +82,17 @@ const Sidebar = {
async _selectGame(gameId) {
await API.updateConfig({ selected_game_id: gameId });
const cfg = await API.getConfig();
const game = this._gameData.find(g => g.id === gameId);
if (game && !cfg.use_unified_port && game.default_port) {
await API.updateConfig({ telemetry_port: game.default_port });
}
await API.stopTelemetry();
await API.startTelemetry();
this._gameListEl.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
const items = this._gameListEl.querySelectorAll('.nav-item');
items.forEach(item => {
if (item.textContent.trim()) item.classList.add('active');
items.forEach((item, idx) => {
if (this._gameData[idx] && this._gameData[idx].id === gameId) item.classList.add('active');
});
const games = await API.getGames();
this._updateGameLabel(gameId, games);
+7 -2
View File
@@ -25,7 +25,12 @@ const PageSettings = {
_bindEvents() {
document.getElementById('settings-telemetry-port')?.addEventListener('change', async (e) => {
await API.updateConfig({ telemetry_port: parseInt(e.target.value) || 20777 });
Toast.show('遥测端口已更新,重启监听后生效', 'info');
Toast.show('端口已更新,重启后生效', 'info');
});
document.getElementById('settings-unified-port')?.addEventListener('change', async (e) => {
await API.updateConfig({ use_unified_port: e.target.checked });
Toast.show('已更新', 'info');
});
document.getElementById('settings-restart-telemetry')?.addEventListener('click', async () => {
@@ -119,7 +124,7 @@ const PageSettings = {
'<div class="settings-section"><h3>数据转发</h3><p style="font-size:12px;color:var(--text-tertiary);margin-bottom:12px">将收到的游戏原始 UDP 数据包完整转发到下游物理外设</p><div id="forward-list">' + this._forwardListHtml(forwards) + '</div><div style="display:flex;gap:8px;margin-top:8px"><button id="btn-add-forward" class="btn btn-sm btn-secondary">+ 添加目标</button><button id="btn-save-forwards" class="btn btn-sm btn-primary">保存并生效</button></div></div>' +
'<div class="settings-section"><h3>连接设置</h3><div class="settings-row"><div><div class="settings-label">遥测监听端口</div><div class="settings-desc">游戏内 UDP 输出端口 (默认 20777)</div></div><div class="settings-control"><input type="number" id="settings-telemetry-port" value="' + (cfg.telemetry_port||20777) + '" min="1024" max="65535"></div></div><div class="settings-row"><div><div class="settings-label">重启遥测监听</div><div class="settings-desc">修改端口或切换游戏后</div></div><div class="settings-control"><button id="settings-restart-telemetry" class="btn btn-secondary btn-sm">重启监听</button></div></div></div>' +
'<div class="settings-section"><h3>连接设置</h3><div class="settings-row"><div><div class="settings-label">遥测监听端口</div><div class="settings-desc">全局统一端口</div></div><div class="settings-control"><input type="number" id="settings-telemetry-port" value="' + (cfg.telemetry_port||20777) + '" min="1024" max="65535"></div></div><div class="settings-row"><div><div class="settings-label">使用游戏独立端口</div><div class="settings-desc">关闭后切游戏自动切换到各游戏的默认端口</div></div><div class="settings-control"><label class="forward-toggle"><input type="checkbox" id="settings-unified-port" ' + (cfg.use_unified_port!==false?'checked':'') + '> 统一端口</label></div></div><div class="settings-row"><div><div class="settings-label">重启遥测监听</div><div class="settings-desc">手动重启</div></div><div class="settings-control"><button id="settings-restart-telemetry" class="btn btn-secondary btn-sm">重启监听</button></div></div></div>' +
'<div class="settings-section"><h3>仪表盘管理</h3><div style="display:flex;gap:8px;margin-bottom:16px"><button id="btn-import-dashboard" class="btn btn-secondary btn-sm">导入 .tsd</button><span style="font-size:12px;color:var(--text-tertiary);display:flex;align-items:center">右键卡片导出</span></div></div>' +
-2
View File
@@ -124,8 +124,6 @@
}
window.addEventListener('resize', () => {
const cw = parseInt(sceneCanvas.style.width) / (parseInt(sceneCanvas.style.width) / 1920) || 1920;
const ch = parseInt(sceneCanvas.style.height) / (parseInt(sceneCanvas.style.height) / 1080) || 1080;
const vw = window.innerWidth;
const vh = window.innerHeight;
const scale = Math.min(vw / cw, vh / ch);