internal/gui: surface warn status + new voice tests in Classic UI
Frontend now renders three-tier status (passed/warn/failed) instead of
two. Warn rows get a yellow exclamation-mark dot, expand by default to
show the hint, and contribute to a new "(with warnings)" suffix on the
"All checks passed" header.
Test catalog gains "voice-quality" and "voice-srv" rows replacing the
single "stun" row, in the same position (after udp, before api). RU
descriptions explain what each test actually probes.
useDrover's lastSummary now reports {total, failed, warnings} so the
Classic header can pick the right tier color.
App.go counts StatusWarn as passed in the final {passed, failed} summary
emitted via "check:done" — warn is a soft pass per spec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+5
-3
@@ -68,8 +68,8 @@ type Config struct {
|
|||||||
// for them on the "check:result" event. Mirrors checker.Result but with
|
// for them on the "check:result" event. Mirrors checker.Result but with
|
||||||
// Duration converted to milliseconds (int) for the JS side.
|
// Duration converted to milliseconds (int) for the JS side.
|
||||||
type CheckResult struct {
|
type CheckResult struct {
|
||||||
ID string `json:"id"` // tcp / greet / auth / connect / udp / stun / api
|
ID string `json:"id"` // tcp / greet / auth / connect / udp / voice-quality / voice-srv / api
|
||||||
Status string `json:"status"` // running | passed | failed | skipped
|
Status string `json:"status"` // running | passed | warn | failed | skipped
|
||||||
Metric string `json:"metric,omitempty"`
|
Metric string `json:"metric,omitempty"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
Hint string `json:"hint,omitempty"`
|
Hint string `json:"hint,omitempty"`
|
||||||
@@ -136,7 +136,9 @@ func (a *App) RunCheck(cfg Config) {
|
|||||||
Attempt: r.Attempt,
|
Attempt: r.Attempt,
|
||||||
})
|
})
|
||||||
switch r.Status {
|
switch r.Status {
|
||||||
case checker.StatusPassed:
|
case checker.StatusPassed, checker.StatusWarn:
|
||||||
|
// Warn is a "soft pass" — counted as passed for the
|
||||||
|
// final summary, but the row still surfaces the hint.
|
||||||
passed++
|
passed++
|
||||||
case checker.StatusFailed:
|
case checker.StatusFailed:
|
||||||
failed++
|
failed++
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export function ClassicWindow({ mode = 'dark', initial, onToggleMode }) {
|
|||||||
const D = useDrover(initial);
|
const D = useDrover(initial);
|
||||||
const [version, setVersion] = React.useState('');
|
const [version, setVersion] = React.useState('');
|
||||||
React.useEffect(() => { GoVersion().then(setVersion).catch(() => {}); }, []);
|
React.useEffect(() => { GoVersion().then(setVersion).catch(() => {}); }, []);
|
||||||
const palette = { pending: t.dimmer, running: t.accent, passed: t.pass, failed: t.danger, skipped: t.skip };
|
const palette = { pending: t.dimmer, running: t.accent, passed: t.pass, failed: t.danger, skipped: t.skip, warn: t.warn };
|
||||||
const fontMono = '"JetBrains Mono","SF Mono",ui-monospace,Menlo,Consolas,monospace';
|
const fontMono = '"JetBrains Mono","SF Mono",ui-monospace,Menlo,Consolas,monospace';
|
||||||
const fontUI = '"Inter","Segoe UI",system-ui,sans-serif';
|
const fontUI = '"Inter","Segoe UI",system-ui,sans-serif';
|
||||||
const isActive = D.phase === 'active';
|
const isActive = D.phase === 'active';
|
||||||
@@ -288,7 +288,9 @@ function ClassicStatus({ t, D, palette, fontMono }) {
|
|||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
: D.lastSummary?.failed === 0
|
: D.lastSummary?.failed === 0
|
||||||
? <span style={{ color: t.pass, fontWeight: 600 }}>All checks passed. Ready to start.</span>
|
? (D.lastSummary?.warnings > 0
|
||||||
|
? <span style={{ color: t.warn, fontWeight: 600 }}>All checks passed (with warnings).</span>
|
||||||
|
: <span style={{ color: t.pass, fontWeight: 600 }}>All checks passed. Ready to start.</span>)
|
||||||
: <span style={{ color: t.warn, fontWeight: 600 }}>{D.lastSummary?.failed} of {D.tests.length} checks failed. Some features won't work.</span>}
|
: <span style={{ color: t.warn, fontWeight: 600 }}>{D.lastSummary?.failed} of {D.tests.length} checks failed. Some features won't work.</span>}
|
||||||
</div>
|
</div>
|
||||||
{/* tests */}
|
{/* tests */}
|
||||||
@@ -308,23 +310,26 @@ function ClassicStatus({ t, D, palette, fontMono }) {
|
|||||||
{test.label}
|
{test.label}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ marginLeft:'auto', fontFamily: fontMono, fontSize: 11,
|
<span style={{ marginLeft:'auto', fontFamily: fontMono, fontSize: 11,
|
||||||
color: state === 'failed' ? t.danger : state === 'skipped' ? t.skip : t.dim }}>
|
color: state === 'failed' ? t.danger : state === 'warn' ? t.warn : state === 'skipped' ? t.skip : t.dim }}>
|
||||||
{r?.metric || (state === 'running' ? '...' : '')}
|
{r?.metric || (state === 'running' ? '...' : '')}
|
||||||
</span>
|
</span>
|
||||||
{r?.result === 'failed' && (
|
{(r?.result === 'failed' || r?.result === 'warn') && (
|
||||||
<button onClick={() => D.toggleExpand(test.id)} style={iconBtnStyle(t)} title="Подробнее">
|
<button onClick={() => D.toggleExpand(test.id)} style={iconBtnStyle(t)} title="Подробнее">
|
||||||
<IconChevron color={t.dim} dir={r.expanded ? 'up' : 'down'}/>
|
<IconChevron color={t.dim} dir={r.expanded ? 'up' : 'down'}/>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{r?.result === 'failed' && r.expanded && (
|
{(r?.result === 'failed' || r?.result === 'warn') && r.expanded && (
|
||||||
<div className="drv-fadein" style={{
|
<div className="drv-fadein" style={{
|
||||||
margin: '4px 0 6px 21px', padding: '8px 10px', borderRadius: 3,
|
margin: '4px 0 6px 21px', padding: '8px 10px', borderRadius: 3,
|
||||||
background: mode_mix(t.danger, t.panel, 0.9), border: `1px solid ${mode_mix(t.danger, t.panel, 0.78)}`,
|
background: mode_mix(r.result === 'warn' ? t.warn : t.danger, t.panel, 0.9),
|
||||||
|
border: `1px solid ${mode_mix(r.result === 'warn' ? t.warn : t.danger, t.panel, 0.78)}`,
|
||||||
fontSize: 11.5, color: t.text,
|
fontSize: 11.5, color: t.text,
|
||||||
}}>
|
}}>
|
||||||
<div style={{ color: t.danger, fontWeight: 600, marginBottom: 2 }}>{r.error}</div>
|
{r.error
|
||||||
<div style={{ color: t.dim }}>{r.hint}</div>
|
? <div style={{ color: r.result === 'warn' ? t.warn : t.danger, fontWeight: 600, marginBottom: 2 }}>{r.error}</div>
|
||||||
|
: (r.hint && <div style={{ color: r.result === 'warn' ? t.warn : t.danger, fontWeight: 600, marginBottom: 2 }}>{r.hint}</div>)}
|
||||||
|
{r.error && <div style={{ color: t.dim }}>{r.hint}</div>}
|
||||||
{r.rawHex && (
|
{r.rawHex && (
|
||||||
<div style={{
|
<div style={{
|
||||||
fontFamily: fontMono, fontSize: 10.5, color: t.dimmer,
|
fontFamily: fontMono, fontSize: 10.5, color: t.dimmer,
|
||||||
|
|||||||
@@ -18,13 +18,14 @@ import { EventsOn, EventsOff } from '../../wailsjs/runtime/runtime'
|
|||||||
|
|
||||||
// ─── Test catalog ──────────────────────────────────────────────────────────
|
// ─── Test catalog ──────────────────────────────────────────────────────────
|
||||||
export const ALL_TESTS = [
|
export const ALL_TESTS = [
|
||||||
{ id: 'tcp', label: 'TCP reachability', desc: 'TCP-соединение до прокси установлено' },
|
{ id: 'tcp', label: 'TCP reachability', desc: 'TCP-соединение до прокси установлено' },
|
||||||
{ id: 'greet', label: 'SOCKS5 greeting', desc: 'Прокси отвечает по протоколу SOCKS5' },
|
{ id: 'greet', label: 'SOCKS5 greeting', desc: 'Прокси отвечает по протоколу SOCKS5' },
|
||||||
{ id: 'auth', label: 'SOCKS5 authentication', desc: 'Аутентификация по логину/паролю принята', authOnly: true },
|
{ id: 'auth', label: 'SOCKS5 authentication', desc: 'Аутентификация по логину/паролю принята', authOnly: true },
|
||||||
{ id: 'connect', label: 'TCP CONNECT to Discord', desc: 'Прокси проксирует TCP к gateway' },
|
{ id: 'connect', label: 'TCP CONNECT to Discord', desc: 'Прокси проксирует TCP к gateway' },
|
||||||
{ id: 'udp', label: 'UDP ASSOCIATE', desc: 'Прокси выдал UDP-релей' },
|
{ id: 'udp', label: 'UDP ASSOCIATE', desc: 'Прокси выдал UDP-релей' },
|
||||||
{ id: 'stun', label: 'UDP round-trip via STUN', desc: 'UDP-пакеты ходят туда-обратно' },
|
{ id: 'voice-quality', label: 'UDP voice quality', desc: 'Бёрст 30 STUN-пакетов через релей: потери, джиттер, латентность' },
|
||||||
{ id: 'api', label: 'Discord API reachable', desc: 'HTTPS до discord.com через прокси' },
|
{ id: 'voice-srv', label: 'Discord voice servers', desc: 'Какие регионы Discord media доступны через прокси' },
|
||||||
|
{ id: 'api', label: 'Discord API reachable', desc: 'HTTPS до discord.com через прокси' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Pre-baked scenarios so the prototype feels alive. Each entry per test:
|
// Pre-baked scenarios so the prototype feels alive. Each entry per test:
|
||||||
@@ -91,7 +92,8 @@ export function useDrover(initial = {}) {
|
|||||||
if (phase !== 'checked' && phase !== 'active') return null;
|
if (phase !== 'checked' && phase !== 'active') return null;
|
||||||
const ids = tests.map(t => t.id);
|
const ids = tests.map(t => t.id);
|
||||||
const failed = ids.filter(id => results[id]?.result === 'failed').length;
|
const failed = ids.filter(id => results[id]?.result === 'failed').length;
|
||||||
return { total: ids.length, failed };
|
const warnings = ids.filter(id => results[id]?.result === 'warn').length;
|
||||||
|
return { total: ids.length, failed, warnings };
|
||||||
}, [phase, results, tests]);
|
}, [phase, results, tests]);
|
||||||
|
|
||||||
// ── actions ────────────────────────────────────────────────────────────
|
// ── actions ────────────────────────────────────────────────────────────
|
||||||
@@ -123,10 +125,10 @@ export function useDrover(initial = {}) {
|
|||||||
hint: r.hint,
|
hint: r.hint,
|
||||||
rawHex: r.rawHex,
|
rawHex: r.rawHex,
|
||||||
attempt: r.attempt,
|
attempt: r.attempt,
|
||||||
expanded: r.status === 'failed',
|
expanded: r.status === 'failed' || r.status === 'warn',
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
pushLog(r.status === 'failed' ? 'ERROR' : r.status === 'skipped' ? 'WARN' : 'INFO',
|
pushLog(r.status === 'failed' ? 'ERROR' : (r.status === 'skipped' || r.status === 'warn') ? 'WARN' : 'INFO',
|
||||||
`${r.id}: ${r.status}${r.metric ? ' · ' + r.metric : ''}`);
|
`${r.id}: ${r.status}${r.metric ? ' · ' + r.metric : ''}`);
|
||||||
});
|
});
|
||||||
const offDone = EventsOn('check:done', (s) => {
|
const offDone = EventsOn('check:done', (s) => {
|
||||||
@@ -332,6 +334,13 @@ export function StatusDot({ state, palette, size = 12 }) {
|
|||||||
<path d="M5.5 5.5l5 5M10.5 5.5l-5 5" stroke="white" strokeWidth="1.6" strokeLinecap="round"/>
|
<path d="M5.5 5.5l5 5M10.5 5.5l-5 5" stroke="white" strokeWidth="1.6" strokeLinecap="round"/>
|
||||||
</svg>;
|
</svg>;
|
||||||
}
|
}
|
||||||
|
if (state === 'warn') {
|
||||||
|
return <svg width={size} height={size} viewBox="0 0 16 16">
|
||||||
|
<circle cx="8" cy="8" r="7" fill={c}/>
|
||||||
|
<path d="M8 4v5" stroke="white" strokeWidth="1.6" strokeLinecap="round"/>
|
||||||
|
<circle cx="8" cy="11.5" r="0.9" fill="white"/>
|
||||||
|
</svg>;
|
||||||
|
}
|
||||||
if (state === 'skipped') {
|
if (state === 'skipped') {
|
||||||
return <svg width={size} height={size} viewBox="0 0 16 16">
|
return <svg width={size} height={size} viewBox="0 0 16 16">
|
||||||
<circle cx="8" cy="8" r="7" fill="none" stroke={c} strokeWidth="1.4" strokeDasharray="2 2"/>
|
<circle cx="8" cy="8" r="7" fill="none" stroke={c} strokeWidth="1.4" strokeDasharray="2 2"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user