feedback7 G-SMALL: settings can copy local diagnostics for a bug report (PR #321 by @Cloudnyco)

Applied 设置 → 问题反馈. The report stays in the browser: the in-memory error log, device info, and the battle on screen only while 「附上本场战斗」 is on. Nothing is uploaded.

Redaction no longer treats a long name as a bare substring. Prose replaces each secret only as a whole token, so a name does not cut a longer word or a chess id. The attached JSON replaces a string only when it equals the secret, so chess_char_1013_chen2 and a note like "charge the char" stay, while an exact "char" value becomes the placeholder. The token boundary is a consumed prefix plus a lookahead, because a lookbehind is a SyntaxError in Safari before 16.4. PLAYING section 12 stays; the other section-12 proposal is not in this batch.

Tests: node --test test/diag.test.js — 10 pass. node --test test/client-static.test.js — 315 pass. node --test test/match/runner-pending.test.js test/match/watch-bonds.test.js test/ui/playtest6-unite.test.js test/ui/spectate-effects.test.js test/ui/feedback5-hotkeys.test.js test/i18n-server.test.js test/ui/bosshit-ticker.test.js test/ui/playtest3.test.js test/docs-consistency.test.js test/docs-paths.test.js test/version.test.js test/package.test.js — 138 pass. npm run -s lint — 0 errors. npm run -s typecheck and npm run -s check:imports passed. node tools/i18n.mjs check --all --strict — 1041/1041 in en, ja, ko and zh-TW, 0 errors.
This commit is contained in:
Cloudnyco authored and sganggs committed 2026-10-08 20:08:32 +08:00
1 parent 073507eca3
commit 3f8844129b
12 files changed
+599 -7

No files matched your search

+56 -3
View File
@@ -3,17 +3,20 @@
// the settings modal — which also holds the language switch (ui/lang.js; kept apart in `sp.pref.lang`; under it a note
// while the current language's pack is a machine translation, `_meta.machineTranslated`) and the 快捷键
// section that rebinds the in-match shortcuts (the key map: ui/gameLogic/shortcuts.js; the community request
// 「快捷键可不可以自己设置」, the owner's decision of 2026-10-07).
// 「快捷键可不可以自己设置」, the owner's decision of 2026-10-07) and 问题反馈, which copies the diagnostics of this page
// for a bug report (diag.js: the error log, this browser, optionally the battle on screen; nothing is uploaded).
import { useLayoutEffect, useState } from '../../vendor/hooks.module.js';
import { useLayoutEffect, useRef, useState } from '../../vendor/hooks.module.js';
import { html, Modal, Button, Icon, MicroLabel } from './components.js';
import { createStore, useStore, loadPref, savePref } from '../store.js';
import { createStore, useStore, loadPref, savePref, store } from '../store.js';
import { sanitizeSettings, HOTKEY_ACTIONS, DEFAULT_HOTKEYS, hotkeyLabel, rebindHotkey, isDefaultHotkeys, captureHotkey } from './gameLogic.js';
import { audio } from '../audio.js';
import { openGuide } from './guide.js';
import { detectFeatures } from './device.js';
import { LangToggle, machineTranslationNote } from './lang.js';
import { t, tc, N_ } from '../../../shared/i18n.js';
import { errorCount, currentBattle, diagnosticsText } from '../diag.js';
import { copyText } from './clipboard.js';
const cx = (...p) => p.flat().filter(Boolean).join(' ');
@@ -144,6 +147,55 @@ function HotkeySection({ keys, touchUi }) {
</section>`;
}
/** Where a report goes: the upstream issue tracker (shown as text the player can select; a link may open nothing). */
export const ISSUES_URL = 'https://github.com/sganggs/Stronghold-Protocol/issues';
/**
* 问题反馈: copy the diagnostics of this page (diag.js) for a GitHub issue — the errors recorded since the page opened,
* this browser and device, where the player is, and, switched on by default while a battle is on screen, that battle's
* spec (the dev tools replay it). Player names, the room code and the token are replaced; nothing is uploaded. Laid out
* like 快捷键 above it (a head with its button, a hint, the result line); the battle switch is the settings' Toggle, and
* when the clipboard refuses (some in-app browsers), the report is shown selected in a box styled like 干员调配's 导出.
*/
/** The result line of 复制诊断信息, by outcome (msgids: translated when shown, so a language switch reaches it). */
const DIAG_NOTES = { copied: N_('已复制诊断信息,可以粘贴到 GitHub issue 中'), refused: N_('无法写入剪贴板,请手动复制下面的内容') };
function DiagSection() {
const [attach, setAttach] = useState(true);
const [note, setNote] = useState(null); // 'copied' | 'refused' | null: the result line
const [manual, setManual] = useState(null); // the report, when the clipboard refused it
const boxRef = useRef(null);
// the box opens selected for a manual copy, scrolled to its first line (select() leaves it at the end)
useLayoutEffect(() => {
const el = boxRef.current;
if (manual && el) { el.focus(); el.select(); el.scrollTop = 0; }
}, [manual]);
const n = errorCount();
const battle = currentBattle();
const copy = async () => {
const text = diagnosticsText({ state: store.get(), settings: settingsStore.get(), attachBattle: attach && !!battle });
const ok = await copyText(text);
setManual(ok ? null : text);
setNote(ok ? 'copied' : 'refused');
};
return html`<section class="set-diag" aria-labelledby="set-diag-title">
<div class="set-keys__head">
<span class="set-row__label" id="set-diag-title">${t('问题反馈')}<${MicroLabel}>DIAGNOSTICS<//></span>
<${Button} variant="ghost" size="sm" icon="copy" class="set-diag__copy" data-testid="diag-copy" onClick=${copy}>${t('复制诊断信息')}<//>
</div>
<p class="set-hint">${t('遇到问题时,复制诊断信息并粘贴到 GitHub issue 中,开发者就能看到出错时的情况。诊断信息只在本机生成,不会自动上传;玩家名和房间号会被替换。')}</p>
<div class="set-diag__meta">
<span class="set-diag__stat">${t('已记录的错误')}<b class=${cx('set-diag__count num', n > 0 && 'is-warn')} data-testid="diag-count">${n}</b></span>
<span class="set-diag__url">${ISSUES_URL.replace(/^https:\/\//, '')}</span>
</div>
${battle ? html`<${Toggle} label=${t('附上本场战斗')} micro="BATTLE DATA" value=${attach} onChange=${setAttach} />
<p class="set-hint set-diag__battle-hint">${t('开发者可以用附上的战斗数据重现这场战斗。')}</p>` : null}
${note ? html`<p class=${cx('set-keys__note', note === 'refused' && 'is-warn')} role="status" aria-live="polite">${t(DIAG_NOTES[note])}</p>` : null}
${manual ? html`<textarea ref=${boxRef} class="set-diag__text" data-testid="diag-text" spellcheck=${false} readOnly
aria-label=${t('诊断信息')} value=${manual}></textarea>` : null}
</section>`;
}
/**
* Settings modal.
* @param {{ open: boolean, onClose: Function }} props
@@ -176,6 +228,7 @@ export function SettingsModal({ open, onClose }) {
</div>
</div>
<${HotkeySection} keys=${s.keys} touchUi=${touchUi} />
<${DiagSection} />
<p class="set-hint">${touchUi ? t('触屏操作:点击单位选中(撤退 / 出售)· 长按单位或卡牌查看详情 · 拖动部署后滑动选择朝向') : t('右键查看详情')}</p>
</div>
<//>`;
+2
View File
@@ -9,6 +9,7 @@ import { useEffect, useReducer } from '../../vendor/hooks.module.js';
import htm from '../../vendor/htm.module.js';
import { ERR_TEXT } from '../../../shared/constants.js';
import { t } from '../../../shared/i18n.js';
import { recordError } from '../diag.js';
// no shared static vnodes (see components.js hFresh: htm's static cache would retain unmounted DOM)
function hFresh(type, props, ...children) {
@@ -108,6 +109,7 @@ export function describeError(err) {
* @returns {number}
*/
export function toastError(err) {
recordError('request', err);
return toast(describeError(err), 'error');
}