Refine ProxyWarden routing workflow

This commit is contained in:
2026-07-22 01:26:51 +03:00
parent 90b2eb507c
commit 67792f245f
15 changed files with 5058 additions and 448 deletions
+37 -104
View File
@@ -67,13 +67,11 @@ import {
} from "../ui";
import { ProxiFyreSetupStrip } from "./components/ProxiFyreSetupStrip";
import { SummaryStatusControl } from "./components/SummaryStatusControl";
import { ConnectionCheckPanel } from "./components/ConnectionCheckPanel";
import { AppList } from "./components/AppList";
import { useNoticeLog } from "./hooks/useNoticeLog";
import { parseProxy, type ParsedProxy } from "./lib/parseProxy";
import {
itemTypeLabel,
normalizeItemValue,
type DraftItemType,
} from "./lib/profileItems";
import { normalizeItemValue, type DraftItemType } from "./lib/profileItems";
import {
configChangeRows,
configSnapshotFromUi,
@@ -115,7 +113,6 @@ import {
serverTooltip,
pingSummary,
errorMessage,
type ConnectionCheckView,
type DraftItem,
type RouteChainInput,
} from "./viewModel";
@@ -928,6 +925,7 @@ export function App() {
return;
}
setProxyCheck(null);
setIsProxyChecking(true);
try {
const result = await pingProxyTarget(target.host, target.port);
@@ -1228,40 +1226,7 @@ export function App() {
</div>
)}
<div className="app-list">
{isLoading ? (
<div className="list-skeleton" aria-label="Загрузка приложений">
<span />
<span />
</div>
) : items.length ? (
items.map((item) => (
<div className="app-row" key={item.id}>
<div className="app-row-main">
<span className="item-icon" aria-hidden="true">
{itemIcon(item.type)}
</span>
<div>
<strong>{item.value}</strong>
<span>{itemTypeLabel(item.type)}</span>
</div>
</div>
<Button
type="button"
variant="danger"
onClick={() => removeItem(item.id)}
aria-label={`Удалить ${item.value}`}
>
Удалить
</Button>
</div>
))
) : (
<div className="empty-state">
Список пуст. Добавь первое приложение сверху.
</div>
)}
</div>
<AppList items={items} loading={isLoading} onRemove={removeItem} />
</section>
);
}
@@ -1373,54 +1338,6 @@ export function App() {
);
}
function renderConnectionCheck(check: ConnectionCheckView) {
const buttonDisabled = Boolean(check.disabledReason);
return (
<div className="connection-check" aria-label="Проверка соединения">
<div className="connection-check-status">
<span>Проверка маршрута</span>
<strong>{check.title}</strong>
<p>{check.text}</p>
</div>
{check.probes.length ? (
<div className="connection-probes" aria-label="Контрольные точки">
{check.probes.map((probe) => (
<span className={`connection-probe ${probe.tone}`} key={probe.id}>
<strong>{probe.label}</strong>
<span>{probe.value}</span>
</span>
))}
</div>
) : null}
<DetailsPopover
className="connection-endpoint"
details={check.details}
popoverLabel="Проверка маршрута"
align="end"
aria-label={`${check.endpoint}. ${check.details.join(". ")}`}
>
<strong>{check.endpoint}</strong>
</DetailsPopover>
<Button
type="button"
variant="neutral"
size="md"
onClick={() => void checkRouteProxy()}
disabled={buttonDisabled}
loading={check.loading}
loadingLabel="Проверяю"
>
Проверить
</Button>
<p className="network-disclosure">
Проверка отправит HTTPS-запросы через выбранный прокси в Cloudflare и
ipify, чтобы увидеть внешний IP.
</p>
</div>
);
}
function renderSingBoxCard() {
const state = serviceControlState(singbox, isDetectingComponents);
const setupSummary = singBoxSetupStatus
@@ -1637,7 +1554,10 @@ export function App() {
aria-label="Состояние прокси"
>
{check.tone === "checking" ? <BusyRing /> : null}
{renderConnectionCheck(check)}
<ConnectionCheckPanel
check={check}
onCheck={() => void checkRouteProxy()}
/>
</section>
);
}
@@ -1650,32 +1570,51 @@ export function App() {
id="panel-proxy"
aria-labelledby="tab-proxy"
>
{renderProxyOverview()}
<section className="route-panel" aria-label="Маршрут приложений">
<section className="route-mode-selector" aria-label="Режим прокси">
<span>Режим маршрута</span>
<div className="route-switch">
<Button
type="button"
variant={routeMode === "external" ? "primary" : "neutral"}
className="route-mode-option external"
onClick={() => changeRouteMode("external")}
aria-pressed={routeMode === "external"}
aria-controls="route-mode-content"
>
Внешний прокси
<span className="route-mode-label">
<strong>Внешний прокси</strong>
<small>Готовый SOCKS5 endpoint</small>
</span>
</Button>
<Button
type="button"
variant={routeMode === "local-singbox" ? "primary" : "neutral"}
className="route-mode-option local"
onClick={() => changeRouteMode("local-singbox")}
aria-pressed={routeMode === "local-singbox"}
aria-controls="route-mode-content"
>
Локальный прокси
<span className="route-mode-label">
<strong>Локальный прокси</strong>
<small>sing-box на этом компьютере</small>
</span>
</Button>
</div>
{routeMode === "external"
? renderExternalProxyControls()
: renderSingBoxCard()}
</section>
<div
className={`route-mode-content ${routeMode === "external" ? "external" : "local"}`}
id="route-mode-content"
key={routeMode}
>
<section className="route-panel" aria-label="Маршрут приложений">
{routeMode === "external"
? renderExternalProxyControls()
: renderSingBoxCard()}
</section>
{renderProxyOverview()}
</div>
</section>
);
}
@@ -1803,9 +1742,3 @@ function nextFrame() {
window.requestAnimationFrame(() => resolve());
});
}
function itemIcon(type: DraftItemType) {
if (type === "process") return <Cpu size={18} strokeWidth={1.9} />;
if (type === "folder") return <FolderOpen size={18} strokeWidth={1.9} />;
return <FileCode2 size={18} strokeWidth={1.9} />;
}