feat(FirewallPage): implement IP info loading from ipinfo.io for subnet analysis suggestions
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m6s

This commit is contained in:
2026-02-21 19:09:11 +07:00
parent 8e8ce7cec0
commit ef7a4817c9
+69 -7
View File
@@ -229,6 +229,8 @@ export default function FirewallPage() {
const [analysisResult, setAnalysisResult] = useState(null);
const [applyConfirmOpen, setApplyConfirmOpen] = useState(false);
const [applying, setApplying] = useState(false);
const [ipInfoMap, setIpInfoMap] = useState({});
const [ipInfoLoading, setIpInfoLoading] = useState(false);
const routerServers = useMemo(
() =>
@@ -290,8 +292,41 @@ export default function FirewallPage() {
setMinCountN(n);
const result = analyzeBanList(data.ban, n);
setAnalysisResult(result);
setIpInfoMap({});
};
// Подгрузка AS/орг с ipinfo.io для подсетей из анализа (лимит 30, с задержкой между запросами)
useEffect(() => {
const suggestions = analysisResult?.suggestions;
if (!suggestions?.length) return;
const maxFetch = 30;
const slice = suggestions.slice(0, maxFetch);
let cancelled = false;
setIpInfoLoading(true);
(async () => {
for (let i = 0; i < slice.length && !cancelled; i++) {
const s = slice[i];
const firstIp = s.subnet.split('/')[0];
try {
const res = await fetch(`https://ipinfo.io/${firstIp}/json`, { method: 'GET' });
if (!res.ok) continue;
const info = await res.json();
if (!cancelled) {
setIpInfoMap((prev) => ({
...prev,
[s.subnet]: { org: info.org, country: info.country, city: info.city },
}));
}
} catch (_) {
if (!cancelled) setIpInfoMap((prev) => ({ ...prev, [s.subnet]: null }));
}
if (i < slice.length - 1) await new Promise((r) => setTimeout(r, 180));
}
if (!cancelled) setIpInfoLoading(false);
})();
return () => { cancelled = true; };
}, [analysisResult?.suggestions]);
const handleApplySummary = async () => {
if (!serverId || !analysisResult?.suggestions?.length) return;
setApplying(true);
@@ -422,6 +457,7 @@ export default function FirewallPage() {
setAnalysisOpen(false);
setAnalysisResult(null);
setApplyConfirmOpen(false);
setIpInfoMap({});
}}
title="Анализ списка ban"
size="lg"
@@ -455,6 +491,9 @@ export default function FirewallPage() {
<>
<p className="mb-2">
Найдено <strong>{analysisResult.suggestions.length}</strong> подсетей /24 для замены: удалить одиночные IP и добавить одну запись на подсеть.
{ipInfoLoading && (
<span className="ms-2 text-muted small">(загрузка AS/хостер с ipinfo.io)</span>
)}
</p>
<div className="table-responsive">
<table className="table table-sm table-vcenter mb-0">
@@ -462,17 +501,40 @@ export default function FirewallPage() {
<tr>
<th>Подсеть</th>
<th className="text-end">Кол-во IP</th>
<th>AS / Хостер</th>
<th>Ссылка</th>
<th>Действие</th>
</tr>
</thead>
<tbody>
{analysisResult.suggestions.map((s) => (
<tr key={s.subnet}>
<td className="font-monospace">{s.subnet}</td>
<td className="text-end">{s.count}</td>
<td className="text-muted small">Удалить {s.count} записей добавить 1</td>
</tr>
))}
{analysisResult.suggestions.map((s) => {
const firstIp = s.subnet.split('/')[0];
const info = ipInfoMap[s.subnet];
return (
<tr key={s.subnet}>
<td className="font-monospace">{s.subnet}</td>
<td className="text-end">{s.count}</td>
<td className="small">
{info === undefined
? (ipInfoLoading ? '…' : '—')
: info
? (info.org || info.country ? [info.org, info.country].filter(Boolean).join(' · ') : '—')
: '—'}
</td>
<td>
<a
href={`https://ipinfo.io/${firstIp}`}
target="_blank"
rel="noopener noreferrer"
className="text-primary"
>
ipinfo.io
</a>
</td>
<td className="text-muted small">Удалить {s.count} записей добавить 1</td>
</tr>
);
})}
</tbody>
</table>
</div>