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:
AskaEth committed 2026-09-09 21:06:40 +08:00
1 parent 8dae63ee70
commit 7f9701c465
61 files changed
+3844 -333

No files matched your search

+89
View File
@@ -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()
}
+32 -3
View File
@@ -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>
+2
View File
@@ -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)
})
+50 -1
View File
@@ -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;
}