feat: 补齐第一批已知缺口(关闭握手 / 定时备份 / Origin 白名单 / DTO 收敛)
- httpapi:新增 POST /api/shutdown(admin 会话或 X-Shutdown-Token 常量时间比对) 与 GET /api/rules/switches(recorder 可读,只回布尔开关);settings/standings 的 重复 DTO 收敛到 internal/dto;standings.showIndividualMedals 贯通到设置读写与前端 - export:抽出 export.Service,新增自动备份定时任务(backup.intervalMinutes/keep), 自动、手动、发布前三类备份共用同一把互斥锁与滚动清理 - realtime:/ws 去掉 InsecureSkipVerify,改用 OriginPatterns(同源与无 Origin 放行, 跨站需命中 server.allowedOrigins) - console:停止服务改走 POST /api/shutdown 优雅关闭(5 秒宽限后回退强杀), 服务管理器新增「已连接的大屏实例」卡片 - deploy:build.bat 改用 for /f 读内部署名——set /p 在 chcp 65001 下返回空, 导致最近这次构建的 exe 里根本没注入署名 - docs:备份策略、关闭超时、Origin 配置的说明与实现对齐
This commit is contained in:
1 parent
4d56c73ae6
commit
2c9ff65fc4
42 files changed
+1271
-275
No files matched your search
@@ -13,6 +13,8 @@ const el = {
|
||||
config: document.getElementById('k-config'),
|
||||
configWarning: document.getElementById('config-warning'),
|
||||
displayList: document.getElementById('display-list'),
|
||||
instanceList: document.getElementById('instance-list'),
|
||||
instancesSummary: document.getElementById('instances-summary'),
|
||||
log: document.getElementById('log'),
|
||||
autoscroll: document.getElementById('autoscroll'),
|
||||
btnStart: document.getElementById('btn-start'),
|
||||
@@ -94,6 +96,45 @@ function renderDisplays(displays) {
|
||||
}
|
||||
}
|
||||
|
||||
function renderInstances(payload) {
|
||||
el.instanceList.textContent = ''
|
||||
|
||||
if (!payload || !payload.ok) {
|
||||
el.instancesSummary.textContent = ''
|
||||
const li = document.createElement('li')
|
||||
li.className = 'muted'
|
||||
li.textContent = (payload && payload.message) || '暂时无法读取大屏实例'
|
||||
el.instanceList.append(li)
|
||||
return
|
||||
}
|
||||
|
||||
const list = payload.instances || []
|
||||
el.instancesSummary.textContent = `共 ${list.length} 个 · ${list.filter((item) => item.online).length} 在线`
|
||||
|
||||
if (!list.length) {
|
||||
const li = document.createElement('li')
|
||||
li.className = 'muted'
|
||||
li.textContent = '当前没有大屏实例连接'
|
||||
el.instanceList.append(li)
|
||||
return
|
||||
}
|
||||
|
||||
for (const item of list) {
|
||||
const li = document.createElement('li')
|
||||
const page = document.createElement('span')
|
||||
page.className = 'page'
|
||||
page.textContent = item.page || '未知页面'
|
||||
const id = document.createElement('span')
|
||||
id.className = 'url'
|
||||
id.textContent = item.id || '—'
|
||||
const state = document.createElement('span')
|
||||
state.className = `state ${item.online ? 'online' : 'offline'}`
|
||||
state.textContent = item.online ? '在线' : '离线'
|
||||
li.append(page, id, state)
|
||||
el.instanceList.append(li)
|
||||
}
|
||||
}
|
||||
|
||||
function appendLog(line) {
|
||||
const atBottom = el.log.scrollTop + el.log.clientHeight >= el.log.scrollHeight - 8
|
||||
el.log.textContent += `${line}\n`
|
||||
@@ -233,8 +274,11 @@ async function boot() {
|
||||
window.consoleApi.onStatus(renderStatus)
|
||||
window.consoleApi.onLog(appendLog)
|
||||
window.consoleApi.onAccountOutput(handleAccountOutput)
|
||||
window.consoleApi.onDisplayInstances(renderInstances)
|
||||
setLog(await window.consoleApi.getLog())
|
||||
await refresh()
|
||||
// 主进程只在窗口可见时轮询;这里补一次,保证窗口刚打开就有数据
|
||||
renderInstances(await window.consoleApi.getDisplayInstances())
|
||||
}
|
||||
|
||||
boot()
|
||||
@@ -108,6 +108,20 @@
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="card-head">
|
||||
<h2>已连接的大屏实例</h2>
|
||||
<span id="instances-summary" class="muted"></span>
|
||||
</div>
|
||||
<p class="hint">
|
||||
来自服务端 <span class="mono">GET /api/display/instances</span>,每 5 秒刷新一次;
|
||||
沿用服务端管理面板的登录态,需要记录员及以上权限。
|
||||
</p>
|
||||
<ul id="instance-list" class="display-list">
|
||||
<li class="muted">正在读取…</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="card-head">
|
||||
<h2>管理员账号</h2>
|
||||
|
||||
@@ -17,6 +17,7 @@ contextBridge.exposeInMainWorld('consoleApi', {
|
||||
getLog: () => ipcRenderer.invoke('log:tail'),
|
||||
clearLog: () => ipcRenderer.invoke('log:clear'),
|
||||
relayoutDisplays: () => ipcRenderer.invoke('display:relayout'),
|
||||
getDisplayInstances: () => ipcRenderer.invoke('display:instances-get'),
|
||||
openServerAdmin: () => ipcRenderer.invoke('app:open-server-admin'),
|
||||
resetAdminAccount: (payload) => ipcRenderer.invoke('account:reset', payload),
|
||||
minimize: () => ipcRenderer.invoke('window:minimize'),
|
||||
@@ -25,6 +26,7 @@ contextBridge.exposeInMainWorld('consoleApi', {
|
||||
isMaximized: () => ipcRenderer.invoke('window:is-maximized'),
|
||||
onMaximizeChange: (callback) => subscribe('window:maximize-changed', callback),
|
||||
onAccountOutput: (callback) => subscribe('account:output', callback),
|
||||
onDisplayInstances: (callback) => subscribe('display:instances', callback),
|
||||
onLog: (callback) => subscribe('log:line', callback),
|
||||
onStatus: (callback) => subscribe('service:status', callback)
|
||||
})
|
||||
@@ -152,8 +152,8 @@ main {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
/* 四张卡片各占一整行;最后一行(运行日志)吃掉剩余高度 */
|
||||
grid-template-rows: auto auto auto minmax(160px, 1fr);
|
||||
/* 五张卡片各占一整行;最后一行(运行日志)吃掉剩余高度 */
|
||||
grid-template-rows: auto auto auto auto minmax(160px, 1fr);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -326,6 +326,20 @@ h2 {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.display-list .state {
|
||||
margin-left: auto;
|
||||
flex: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.display-list .state.online {
|
||||
color: var(--ok);
|
||||
}
|
||||
|
||||
.display-list .state.offline {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in new issue
Block a user