fix: 审查修复波次(P0x4 + P1x30+)
- domain/rules: 计分与排名解耦、破纪录按组别、tieBreak 生效、调整分不丢、移除 aggregate 假能力 - realtime: 新建 internal/dto,快照三项契约对齐,/ws 只认会话身份 - httpapi: 流水幂等、standings 整表载荷、广播接线、强制改密、XFF 防伪造、records 写入、审计、赛事包内容、发布前备份、result.points、实例列表 - web: FormData 上传、YAML 解析、快照归一化、大屏解析层、bootstrap 降级、访客判定、主题切换 - console: 优雅关闭握手、中文化、免验证改管理员账号、选屏与日志轮转修复 - docs: 用户文档三份 + README - 主代理: 破纪录加分接线(levels 未加载导致恒 0)
This commit is contained in:
1 parent
8dae63ee70
commit
7f9701c465
61 files changed
+3844
-333
No files matched your search
@@ -21,6 +21,12 @@ const el = {
|
||||
btnRelayout: document.getElementById('btn-relayout'),
|
||||
btnOpenAdmin: document.getElementById('btn-open-admin'),
|
||||
btnClearLog: document.getElementById('btn-clear-log'),
|
||||
btnResetAccount: document.getElementById('btn-reset-account'),
|
||||
acctUser: document.getElementById('acct-user'),
|
||||
acctNewUser: document.getElementById('acct-new-user'),
|
||||
acctPassword: document.getElementById('acct-password'),
|
||||
acctStatus: document.getElementById('acct-status'),
|
||||
acctOutput: document.getElementById('acct-output'),
|
||||
titlebar: document.getElementById('titlebar'),
|
||||
winMin: document.getElementById('win-min'),
|
||||
winMax: document.getElementById('win-max'),
|
||||
@@ -137,6 +143,88 @@ function bindActions() {
|
||||
setLog([])
|
||||
})
|
||||
el.btnOpenAdmin.addEventListener('click', () => window.consoleApi.openServerAdmin())
|
||||
el.btnResetAccount.addEventListener('click', resetAccount)
|
||||
}
|
||||
|
||||
let accountStreaming = false
|
||||
|
||||
function setAccountOutput(text) {
|
||||
el.acctOutput.hidden = !text
|
||||
el.acctOutput.textContent = text || ''
|
||||
if (text) el.acctOutput.scrollTop = el.acctOutput.scrollHeight
|
||||
}
|
||||
|
||||
function appendAccountOutput(text) {
|
||||
el.acctOutput.hidden = false
|
||||
el.acctOutput.textContent += text
|
||||
el.acctOutput.scrollTop = el.acctOutput.scrollHeight
|
||||
}
|
||||
|
||||
function handleAccountOutput(text) {
|
||||
// 只在提交中接收流式输出:重置结束后的最终结果由返回值统一渲染,避免重复
|
||||
if (accountStreaming) appendAccountOutput(text)
|
||||
}
|
||||
|
||||
function setAccountStatus(text, kind) {
|
||||
el.acctStatus.hidden = !text
|
||||
el.acctStatus.textContent = text || ''
|
||||
el.acctStatus.className = kind ? `hint ${kind}` : 'hint'
|
||||
}
|
||||
|
||||
function setAccountBusy(busy) {
|
||||
el.btnResetAccount.disabled = busy
|
||||
el.btnResetAccount.textContent = busy ? '正在重置…' : '重置账号'
|
||||
for (const input of [el.acctUser, el.acctNewUser, el.acctPassword]) input.disabled = busy
|
||||
}
|
||||
|
||||
async function resetAccount() {
|
||||
const currentUsername = el.acctUser.value.trim()
|
||||
const newUsername = el.acctNewUser.value.trim()
|
||||
const password = el.acctPassword.value
|
||||
|
||||
if (!currentUsername) {
|
||||
setAccountStatus('请先填写当前用户名', 'err')
|
||||
return
|
||||
}
|
||||
|
||||
const target = newUsername ? `用户名改为「${newUsername}」` : '只重置密码'
|
||||
// 重置会踢掉该用户的所有登录态,属于不可撤销操作,必须先二次确认
|
||||
const confirmed = window.confirm(
|
||||
'确认重置管理员账号?\n\n' +
|
||||
`当前用户名:${currentUsername}\n` +
|
||||
`本次操作:${target}\n` +
|
||||
`新密码:${password ? '使用输入框中的密码' : '由程序随机生成'}\n\n` +
|
||||
'重置会作废该用户的全部登录会话,正在使用管理面板的人会被强制退出。'
|
||||
)
|
||||
if (!confirmed) return
|
||||
|
||||
accountStreaming = true
|
||||
setAccountBusy(true)
|
||||
setAccountOutput('')
|
||||
setAccountStatus('正在调用 scoreboard.exe passwd …')
|
||||
|
||||
try {
|
||||
const result = await window.consoleApi.resetAdminAccount({ currentUsername, newUsername, password })
|
||||
accountStreaming = false
|
||||
setAccountOutput(result.output || result.error || '')
|
||||
|
||||
if (result.ok) {
|
||||
setAccountStatus(
|
||||
password ? '重置成功,原会话已全部作废。' : '重置成功,密码为随机生成,请从下方输出抄录后立即修改。',
|
||||
'ok'
|
||||
)
|
||||
el.acctUser.value = newUsername || currentUsername
|
||||
el.acctNewUser.value = ''
|
||||
el.acctPassword.value = ''
|
||||
} else {
|
||||
setAccountStatus(`重置失败:${result.error || `passwd 退出码 ${result.exitCode}`}`, 'err')
|
||||
}
|
||||
} catch (err) {
|
||||
setAccountStatus(`重置失败:${err.message}`, 'err')
|
||||
} finally {
|
||||
accountStreaming = false
|
||||
setAccountBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function boot() {
|
||||
@@ -144,6 +232,7 @@ async function boot() {
|
||||
bindWindowControls()
|
||||
window.consoleApi.onStatus(renderStatus)
|
||||
window.consoleApi.onLog(appendLog)
|
||||
window.consoleApi.onAccountOutput(handleAccountOutput)
|
||||
setLog(await window.consoleApi.getLog())
|
||||
await refresh()
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'none'"
|
||||
/>
|
||||
<title>Scoreboard Console · 启动管理</title>
|
||||
<title>赛事积分榜 · 服务管理</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
@@ -15,8 +15,8 @@
|
||||
<div class="brand">
|
||||
<img class="brand-mark" src="scoreboard.png" alt="" width="30" height="30" />
|
||||
<div>
|
||||
<h1>Scoreboard Console</h1>
|
||||
<p class="brand-sub">赛事积分榜 · 桌面宿主</p>
|
||||
<h1>赛事积分榜 · 服务管理</h1>
|
||||
<p class="brand-sub">服务端守护 · 大屏投放</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topbar-right">
|
||||
@@ -108,6 +108,35 @@
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="card-head">
|
||||
<h2>管理员账号</h2>
|
||||
<div class="actions">
|
||||
<button id="btn-reset-account" class="btn" type="button">重置账号</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">
|
||||
本机直接调用同目录的 <span class="mono">scoreboard.exe passwd</span>,无需登录验证。
|
||||
重置会作废该用户的全部登录会话,已在用的管理面板会被强制退出。
|
||||
</p>
|
||||
<div class="form-row">
|
||||
<label class="field">
|
||||
<span>当前用户名</span>
|
||||
<input id="acct-user" class="input" type="text" value="admin" autocomplete="off" spellcheck="false" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>新用户名(留空 = 不改)</span>
|
||||
<input id="acct-new-user" class="input" type="text" autocomplete="off" spellcheck="false" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>新密码(留空 = 随机生成)</span>
|
||||
<input id="acct-password" class="input" type="password" autocomplete="new-password" spellcheck="false" />
|
||||
</label>
|
||||
</div>
|
||||
<p id="acct-status" class="hint" hidden></p>
|
||||
<pre id="acct-output" class="log account-output" hidden></pre>
|
||||
</section>
|
||||
|
||||
<section class="card log-card">
|
||||
<div class="card-head">
|
||||
<h2>运行日志</h2>
|
||||
|
||||
@@ -18,11 +18,13 @@ contextBridge.exposeInMainWorld('consoleApi', {
|
||||
clearLog: () => ipcRenderer.invoke('log:clear'),
|
||||
relayoutDisplays: () => ipcRenderer.invoke('display:relayout'),
|
||||
openServerAdmin: () => ipcRenderer.invoke('app:open-server-admin'),
|
||||
resetAdminAccount: (payload) => ipcRenderer.invoke('account:reset', payload),
|
||||
minimize: () => ipcRenderer.invoke('window:minimize'),
|
||||
maximize: () => ipcRenderer.invoke('window:maximize'),
|
||||
close: () => ipcRenderer.invoke('window:close'),
|
||||
isMaximized: () => ipcRenderer.invoke('window:is-maximized'),
|
||||
onMaximizeChange: (callback) => subscribe('window:maximize-changed', callback),
|
||||
onAccountOutput: (callback) => subscribe('account:output', callback),
|
||||
onLog: (callback) => subscribe('log:line', callback),
|
||||
onStatus: (callback) => subscribe('service:status', callback)
|
||||
})
|
||||
@@ -152,7 +152,8 @@ main {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: auto auto minmax(160px, 1fr);
|
||||
/* 四张卡片各占一整行;最后一行(运行日志)吃掉剩余高度 */
|
||||
grid-template-rows: auto auto auto minmax(160px, 1fr);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -334,6 +335,48 @@ h2 {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px 16px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: #0a0e17;
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.input:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.hint.ok {
|
||||
color: var(--ok);
|
||||
}
|
||||
|
||||
.hint.err {
|
||||
color: var(--err);
|
||||
}
|
||||
|
||||
.log {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
@@ -349,3 +392,9 @@ h2 {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* 账号重置的输出框沿用 .log 的终端样式,但要限高、别把卡片撑开 */
|
||||
.account-output {
|
||||
max-height: 180px;
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
Reference in new issue
Block a user