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:
@@ -5,7 +5,7 @@ import os
|
|||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from pathlib import Path
|
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.staticfiles import StaticFiles
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from fastapi.responses import HTMLResponse, FileResponse
|
from fastapi.responses import HTMLResponse, FileResponse
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
|||||||
"theme": "dark",
|
"theme": "dark",
|
||||||
"sidebar_collapsed": False,
|
"sidebar_collapsed": False,
|
||||||
"telemetry_port": 20777,
|
"telemetry_port": 20777,
|
||||||
|
"use_unified_port": True,
|
||||||
"telemetry_host": "0.0.0.0",
|
"telemetry_host": "0.0.0.0",
|
||||||
"server_host": "0.0.0.0",
|
"server_host": "0.0.0.0",
|
||||||
"server_port": 9527,
|
"server_port": 9527,
|
||||||
|
|||||||
@@ -130,6 +130,20 @@ class GamePluginManager:
|
|||||||
self._discover()
|
self._discover()
|
||||||
self._parser_cache.clear()
|
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:
|
def install_plugin_zip(self, zip_data: bytes) -> bool:
|
||||||
try:
|
try:
|
||||||
with zipfile.ZipFile(io.BytesIO(zip_data), 'r') as zf:
|
with zipfile.ZipFile(io.BytesIO(zip_data), 'r') as zf:
|
||||||
|
|||||||
@@ -115,6 +115,9 @@ class TelemetryListener:
|
|||||||
if self._transport:
|
if self._transport:
|
||||||
self._transport.close()
|
self._transport.close()
|
||||||
self._transport = None
|
self._transport = None
|
||||||
|
if self.forwarder._sock:
|
||||||
|
self.forwarder._sock.close()
|
||||||
|
self.forwarder._sock = None
|
||||||
logger.info("Telemetry listener stopped")
|
logger.info("Telemetry listener stopped")
|
||||||
|
|
||||||
def _get_parser(self, key: str) -> BaseParser:
|
def _get_parser(self, key: str) -> BaseParser:
|
||||||
|
|||||||
+15
-24
@@ -1,33 +1,24 @@
|
|||||||
const API = {
|
const API = {
|
||||||
_base: '/api',
|
_base: '/api',
|
||||||
|
|
||||||
async get(path) {
|
async _req(method, path, body) {
|
||||||
const res = await fetch(`${this._base}${path}`);
|
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();
|
return res.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
async post(path, data) {
|
async get(path) { return this._req('GET', path); },
|
||||||
const res = await fetch(`${this._base}${path}`, {
|
async post(path, data) { return this._req('POST', path, data); },
|
||||||
method: 'POST',
|
async put(path, data) { return this._req('PUT', path, data); },
|
||||||
headers: { 'Content-Type': 'application/json' },
|
async del(path) { return this._req('DELETE', path); },
|
||||||
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 getStatus() { return this.get('/status'); },
|
async getStatus() { return this.get('/status'); },
|
||||||
async getConfig() { return this.get('/config'); },
|
async getConfig() { return this.get('/config'); },
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const Sidebar = {
|
|||||||
_gameListEl: null,
|
_gameListEl: null,
|
||||||
_gameToggleEl: null,
|
_gameToggleEl: null,
|
||||||
_gameLabelEl: null,
|
_gameLabelEl: null,
|
||||||
|
_gameData: [],
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this._el = document.getElementById('sidebar');
|
this._el = document.getElementById('sidebar');
|
||||||
@@ -44,6 +45,7 @@ const Sidebar = {
|
|||||||
|
|
||||||
async _loadGames() {
|
async _loadGames() {
|
||||||
const games = await API.getGames();
|
const games = await API.getGames();
|
||||||
|
this._gameData = games;
|
||||||
this._gameListEl.innerHTML = '';
|
this._gameListEl.innerHTML = '';
|
||||||
|
|
||||||
if (!Array.isArray(games) || games.length === 0) {
|
if (!Array.isArray(games) || games.length === 0) {
|
||||||
@@ -80,11 +82,17 @@ const Sidebar = {
|
|||||||
|
|
||||||
async _selectGame(gameId) {
|
async _selectGame(gameId) {
|
||||||
await API.updateConfig({ selected_game_id: 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();
|
await API.startTelemetry();
|
||||||
this._gameListEl.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
|
this._gameListEl.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
|
||||||
const items = this._gameListEl.querySelectorAll('.nav-item');
|
const items = this._gameListEl.querySelectorAll('.nav-item');
|
||||||
items.forEach(item => {
|
items.forEach((item, idx) => {
|
||||||
if (item.textContent.trim()) item.classList.add('active');
|
if (this._gameData[idx] && this._gameData[idx].id === gameId) item.classList.add('active');
|
||||||
});
|
});
|
||||||
const games = await API.getGames();
|
const games = await API.getGames();
|
||||||
this._updateGameLabel(gameId, games);
|
this._updateGameLabel(gameId, games);
|
||||||
|
|||||||
@@ -25,7 +25,12 @@ const PageSettings = {
|
|||||||
_bindEvents() {
|
_bindEvents() {
|
||||||
document.getElementById('settings-telemetry-port')?.addEventListener('change', async (e) => {
|
document.getElementById('settings-telemetry-port')?.addEventListener('change', async (e) => {
|
||||||
await API.updateConfig({ telemetry_port: parseInt(e.target.value) || 20777 });
|
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 () => {
|
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><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>' +
|
'<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>' +
|
||||||
|
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
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 vw = window.innerWidth;
|
||||||
const vh = window.innerHeight;
|
const vh = window.innerHeight;
|
||||||
const scale = Math.min(vw / cw, vh / ch);
|
const scale = Math.min(vw / cw, vh / ch);
|
||||||
|
|||||||
Reference in New Issue
Block a user