mirror of
https://github.com/andrey271192/Domain_web.git
synced 2026-09-20 14:41:58 +00:00
feat: deploy Domain Scanner via Docker, not GeoExport mirror
Replace VPS install with docker compose + nginx. Add network export module, Telegram bot footer card, and archive legacy site/ mirror. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
148
install.sh
148
install.sh
@@ -1,33 +1,55 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# GeoExport site — one-command install (Ubuntu/Debian VPS)
|
# Domain Scanner — one-command VPS install (Docker Compose + nginx)
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REPO_URL="${GEOEXPORT_REPO_URL:-https://github.com/andrey271192/Domain_web.git}"
|
REPO_URL="${DOMAIN_SCANNER_REPO_URL:-https://github.com/andrey271192/Domain_web.git}"
|
||||||
BRANCH="${GEOEXPORT_BRANCH:-main}"
|
BRANCH="${DOMAIN_SCANNER_BRANCH:-main}"
|
||||||
INSTALL_DIR="${GEOEXPORT_INSTALL_DIR:-/opt/domain_web}"
|
INSTALL_DIR="${DOMAIN_SCANNER_INSTALL_DIR:-/opt/domain-scanner}"
|
||||||
SITE_DIR="${INSTALL_DIR}/site"
|
SERVICE_NAME="domain-scanner"
|
||||||
SERVICE_NAME="geoexport-site"
|
NGINX_SITE="domain-scanner"
|
||||||
NGINX_SITE="geoexport-site"
|
APP_PORT="${DOMAIN_SCANNER_PORT:-3000}"
|
||||||
NODE_PORT="${GEOEXPORT_PORT:-4173}"
|
LEGACY_SERVICE="geoexport-site"
|
||||||
|
LEGACY_NGINX="geoexport-site"
|
||||||
|
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
log() { echo "[geoexport-install] $*"; }
|
log() { echo "[domain-scanner-install] $*"; }
|
||||||
need_root() {
|
need_root() {
|
||||||
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
|
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
|
||||||
echo "Запустите от root: sudo bash install.sh" >&2
|
echo "Run as root: sudo bash install.sh" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
install_packages() {
|
install_packages() {
|
||||||
log "Установка пакетов (nginx, git, curl, nodejs)..."
|
log "Installing system packages..."
|
||||||
apt-get update -qq
|
apt-get update -qq
|
||||||
apt-get install -y -qq nginx git curl ca-certificates nodejs npm >/dev/null
|
apt-get install -y -qq curl ca-certificates git nginx openssl >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
install_docker() {
|
||||||
|
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
|
||||||
|
log "Docker already installed"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
log "Installing Docker..."
|
||||||
|
curl -fsSL https://get.docker.com | sh
|
||||||
|
systemctl enable docker
|
||||||
|
systemctl start docker
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_legacy() {
|
||||||
|
log "Stopping legacy GeoExport mirror (if present)..."
|
||||||
|
systemctl stop "${LEGACY_SERVICE}.service" 2>/dev/null || true
|
||||||
|
systemctl disable "${LEGACY_SERVICE}.service" 2>/dev/null || true
|
||||||
|
rm -f "/etc/systemd/system/${LEGACY_SERVICE}.service"
|
||||||
|
rm -f "/etc/nginx/sites-enabled/${LEGACY_NGINX}"
|
||||||
|
rm -f "/etc/nginx/sites-available/${LEGACY_NGINX}"
|
||||||
|
systemctl daemon-reload 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
clone_or_update() {
|
clone_or_update() {
|
||||||
log "Клонирование/обновление репозитория в ${INSTALL_DIR}..."
|
log "Cloning/updating repository at ${INSTALL_DIR}..."
|
||||||
if [[ -d "${INSTALL_DIR}/.git" ]]; then
|
if [[ -d "${INSTALL_DIR}/.git" ]]; then
|
||||||
git -C "${INSTALL_DIR}" fetch origin "${BRANCH}"
|
git -C "${INSTALL_DIR}" fetch origin "${BRANCH}"
|
||||||
git -C "${INSTALL_DIR}" checkout "${BRANCH}"
|
git -C "${INSTALL_DIR}" checkout "${BRANCH}"
|
||||||
@@ -36,65 +58,63 @@ clone_or_update() {
|
|||||||
rm -rf "${INSTALL_DIR}"
|
rm -rf "${INSTALL_DIR}"
|
||||||
git clone --depth 1 --branch "${BRANCH}" "${REPO_URL}" "${INSTALL_DIR}"
|
git clone --depth 1 --branch "${BRANCH}" "${REPO_URL}" "${INSTALL_DIR}"
|
||||||
fi
|
fi
|
||||||
if [[ ! -f "${SITE_DIR}/server.mjs" ]]; then
|
}
|
||||||
echo "Ошибка: не найден ${SITE_DIR}/server.mjs" >&2
|
|
||||||
exit 1
|
setup_env() {
|
||||||
|
local ip
|
||||||
|
ip="$(hostname -I | awk '{print $1}')"
|
||||||
|
cd "${INSTALL_DIR}"
|
||||||
|
if [[ ! -f .env ]]; then
|
||||||
|
cp .env.example .env
|
||||||
|
fi
|
||||||
|
local secret
|
||||||
|
secret="$(openssl rand -base64 32)"
|
||||||
|
sed -i "s|^NEXTAUTH_SECRET=.*|NEXTAUTH_SECRET=${secret}|" .env
|
||||||
|
sed -i "s|^NEXTAUTH_URL=.*|NEXTAUTH_URL=http://${ip}|" .env
|
||||||
|
sed -i "s|^NEXT_PUBLIC_APP_URL=.*|NEXT_PUBLIC_APP_URL=http://${ip}|" .env
|
||||||
|
export PORT="${APP_PORT}"
|
||||||
|
}
|
||||||
|
|
||||||
|
deploy_compose() {
|
||||||
|
log "Building and starting Docker stack..."
|
||||||
|
cd "${INSTALL_DIR}"
|
||||||
|
export PORT="${APP_PORT}"
|
||||||
|
if docker compose version >/dev/null 2>&1; then
|
||||||
|
docker compose build --pull
|
||||||
|
docker compose up -d
|
||||||
|
docker compose exec -T web npx prisma db push
|
||||||
|
else
|
||||||
|
docker-compose build --pull
|
||||||
|
docker-compose up -d
|
||||||
|
docker-compose exec -T web npx prisma db push
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_patches() {
|
|
||||||
log "Применение патчей фронтенда (export preview/download)..."
|
|
||||||
git -C "${INSTALL_DIR}" checkout HEAD -- site/assets/index-VQj200iM.js
|
|
||||||
node "${SITE_DIR}/patch-bundle.mjs"
|
|
||||||
}
|
|
||||||
|
|
||||||
install_systemd() {
|
|
||||||
log "Настройка systemd (${SERVICE_NAME})..."
|
|
||||||
cat >"/etc/systemd/system/${SERVICE_NAME}.service" <<EOF
|
|
||||||
[Unit]
|
|
||||||
Description=GeoExport static mirror (Node)
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
WorkingDirectory=${SITE_DIR}
|
|
||||||
Environment=PORT=${NODE_PORT}
|
|
||||||
ExecStart=/usr/bin/node ${SITE_DIR}/server.mjs
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=5
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl enable "${SERVICE_NAME}.service"
|
|
||||||
systemctl restart "${SERVICE_NAME}.service"
|
|
||||||
}
|
|
||||||
|
|
||||||
install_nginx() {
|
install_nginx() {
|
||||||
log "Настройка nginx (${NGINX_SITE})..."
|
log "Configuring nginx reverse proxy..."
|
||||||
cat >"/etc/nginx/sites-available/${NGINX_SITE}" <<EOF
|
cat >"/etc/nginx/sites-available/${NGINX_SITE}" <<EOF
|
||||||
server {
|
server {
|
||||||
listen 80 default_server;
|
listen 80 default_server;
|
||||||
listen [::]:80 default_server;
|
listen [::]:80 default_server;
|
||||||
server_name _;
|
server_name _;
|
||||||
|
|
||||||
client_max_body_size 64m;
|
client_max_body_size 32m;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://127.0.0.1:${NODE_PORT};
|
proxy_pass http://127.0.0.1:${APP_PORT};
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host \$host;
|
proxy_set_header Host \$host;
|
||||||
proxy_set_header X-Real-IP \$remote_addr;
|
proxy_set_header X-Real-IP \$remote_addr;
|
||||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||||
|
proxy_set_header Upgrade \$http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
proxy_read_timeout 300s;
|
proxy_read_timeout 300s;
|
||||||
proxy_connect_timeout 60s;
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_buffering off;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
rm -f /etc/nginx/sites-enabled/domain-scanner 2>/dev/null || true
|
|
||||||
rm -f /etc/nginx/sites-available/domain-scanner 2>/dev/null || true
|
|
||||||
rm -f /etc/nginx/sites-enabled/default 2>/dev/null || true
|
rm -f /etc/nginx/sites-enabled/default 2>/dev/null || true
|
||||||
ln -sf "/etc/nginx/sites-available/${NGINX_SITE}" "/etc/nginx/sites-enabled/${NGINX_SITE}"
|
ln -sf "/etc/nginx/sites-available/${NGINX_SITE}" "/etc/nginx/sites-enabled/${NGINX_SITE}"
|
||||||
nginx -t
|
nginx -t
|
||||||
@@ -103,27 +123,35 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
verify() {
|
verify() {
|
||||||
log "Проверка..."
|
log "Verifying deployment..."
|
||||||
sleep 2
|
sleep 5
|
||||||
if ! systemctl is-active --quiet "${SERVICE_NAME}.service"; then
|
if ! curl -fsS -o /dev/null "http://127.0.0.1:${APP_PORT}/api/health"; then
|
||||||
systemctl status "${SERVICE_NAME}.service" --no-pager || true
|
echo "App health check failed on port ${APP_PORT}" >&2
|
||||||
|
cd "${INSTALL_DIR}" && docker compose logs --tail=40 web 2>/dev/null || true
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if ! curl -fsS -o /dev/null "http://127.0.0.1:${NODE_PORT}/"; then
|
local title
|
||||||
echo "Node-сервер не отвечает на порту ${NODE_PORT}" >&2
|
title="$(curl -fsS "http://127.0.0.1/" | grep -o '<title>[^<]*</title>' | head -1 || true)"
|
||||||
|
if echo "${title}" | grep -qi geoexport; then
|
||||||
|
echo "ERROR: GeoExport mirror still served — aborting" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if ! curl -fsS -o /dev/null "http://127.0.0.1/"; then
|
if ! curl -fsS -o /dev/null "http://127.0.0.1/"; then
|
||||||
echo "nginx не отдаёт сайт на :80" >&2
|
echo "nginx not serving on :80" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
log "Готово. Сайт доступен по http://$(hostname -I | awk '{print $1}')/"
|
local ip
|
||||||
|
ip="$(hostname -I | awk '{print $1}')"
|
||||||
|
log "Done. Domain Scanner: http://${ip}/"
|
||||||
|
log "Title check: ${title:-ok}"
|
||||||
}
|
}
|
||||||
|
|
||||||
need_root
|
need_root
|
||||||
install_packages
|
install_packages
|
||||||
|
install_docker
|
||||||
|
stop_legacy
|
||||||
clone_or_update
|
clone_or_update
|
||||||
apply_patches
|
setup_env
|
||||||
install_systemd
|
deploy_compose
|
||||||
install_nginx
|
install_nginx
|
||||||
verify
|
verify
|
||||||
|
|||||||
7
site/ARCHIVED.md
Normal file
7
site/ARCHIVED.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Archived — GeoExport mirror
|
||||||
|
|
||||||
|
The `site/` directory is a **legacy static mirror** of geoexport.org. It is **not** deployed by `install.sh`.
|
||||||
|
|
||||||
|
Production installs use the **Domain Scanner** Next.js app at the repository root (`docker compose`).
|
||||||
|
|
||||||
|
Kept in git for reference only.
|
||||||
@@ -1,19 +1,13 @@
|
|||||||
import { Hero } from "@/components/landing/hero";
|
import { Hero } from "@/components/landing/hero";
|
||||||
import { Features } from "@/components/landing/features";
|
import { Features } from "@/components/landing/features";
|
||||||
|
import { SiteFooter } from "@/components/layout/site-footer";
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Hero />
|
<Hero />
|
||||||
<Features />
|
<Features />
|
||||||
<footer className="border-t border-white/5 py-12 text-center text-sm text-zinc-500">
|
<SiteFooter />
|
||||||
<p>Domain Scanner — self-hosted domain intelligence</p>
|
|
||||||
<p className="mt-2">
|
|
||||||
<a href="https://github.com/andrey271192/Domain_web" className="text-violet-400 hover:underline">
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</footer>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
50
src/components/layout/site-footer.tsx
Normal file
50
src/components/layout/site-footer.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export function SiteFooter() {
|
||||||
|
return (
|
||||||
|
<footer className="border-t border-white/5 py-12">
|
||||||
|
<div className="mx-auto flex max-w-6xl flex-col gap-8 px-4 sm:flex-row sm:items-start sm:justify-between">
|
||||||
|
<div className="text-center sm:text-left">
|
||||||
|
<p className="text-sm font-medium text-zinc-300">Domain Scanner</p>
|
||||||
|
<p className="mt-1 text-sm text-zinc-500">Self-hosted domain intelligence</p>
|
||||||
|
<p className="mt-3">
|
||||||
|
<Link
|
||||||
|
href="https://github.com/andrey271192/Domain_web"
|
||||||
|
className="text-sm text-violet-400 hover:underline"
|
||||||
|
>
|
||||||
|
GitHub
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
data-telegram-bot-card
|
||||||
|
className="max-w-md rounded-2xl border border-white/10 bg-white/5 p-5 text-left backdrop-blur"
|
||||||
|
>
|
||||||
|
<p className="text-sm font-medium text-zinc-200">Telegram domain lookup</p>
|
||||||
|
<p className="mt-2 text-sm leading-relaxed text-zinc-400">
|
||||||
|
Quick DNS/IP checks and list exports on the go — same idea as a scan, in chat.
|
||||||
|
</p>
|
||||||
|
<p className="mt-3 font-mono text-sm">
|
||||||
|
<Link
|
||||||
|
href="https://t.me/domain_searchPro_bot"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-cyan-400 hover:underline"
|
||||||
|
>
|
||||||
|
@domain_searchPro_bot
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="https://t.me/domain_searchPro_bot"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="mt-4 inline-flex rounded-xl bg-gradient-to-r from-violet-600 to-cyan-500 px-4 py-2 text-sm font-medium text-white hover:opacity-90"
|
||||||
|
>
|
||||||
|
Open in Telegram
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
161
src/components/scan/network-export.tsx
Normal file
161
src/components/scan/network-export.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { Download, ChevronDown, Network } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import type { ScanResult } from "@/lib/types";
|
||||||
|
|
||||||
|
type ExportKind = "ip-list" | "dns-hosts" | "infra-json";
|
||||||
|
|
||||||
|
function downloadText(filename: string, content: string, mime = "text/plain") {
|
||||||
|
const blob = new Blob([content], { type: `${mime};charset=utf-8` });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildIpList(result: ScanResult): string {
|
||||||
|
const ips = new Set<string>();
|
||||||
|
if (result.geo?.ip) ips.add(result.geo.ip);
|
||||||
|
for (const r of result.dns.records) {
|
||||||
|
if (r.type === "A" || r.type === "AAAA") ips.add(r.value);
|
||||||
|
}
|
||||||
|
return [...ips].join("\n") + (ips.size ? "\n" : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHostsFile(result: ScanResult): string {
|
||||||
|
const lines: string[] = [
|
||||||
|
`# Infrastructure hosts — ${result.domain}`,
|
||||||
|
`# Generated ${result.scannedAt}`,
|
||||||
|
"",
|
||||||
|
];
|
||||||
|
for (const r of result.dns.records) {
|
||||||
|
if (r.type === "A" || r.type === "AAAA") {
|
||||||
|
lines.push(`${r.value}\t${result.domain}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.geo?.ip && !lines.some((l) => l.startsWith(result.geo!.ip))) {
|
||||||
|
lines.push(`${result.geo.ip}\t${result.domain}`);
|
||||||
|
}
|
||||||
|
return lines.join("\n") + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInfraBundle(result: ScanResult): string {
|
||||||
|
return JSON.stringify(
|
||||||
|
{
|
||||||
|
domain: result.domain,
|
||||||
|
scannedAt: result.scannedAt,
|
||||||
|
network: {
|
||||||
|
ips: buildIpList(result).trim().split("\n").filter(Boolean),
|
||||||
|
dns: result.dns.records,
|
||||||
|
nameservers: result.dns.nameservers,
|
||||||
|
mx: result.dns.mx,
|
||||||
|
},
|
||||||
|
edge: {
|
||||||
|
cdnWaf: result.cdnWaf.detected,
|
||||||
|
tech: result.tech,
|
||||||
|
httpStatus: result.http.status,
|
||||||
|
finalUrl: result.http.finalUrl,
|
||||||
|
},
|
||||||
|
geo: result.geo,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const OPTIONS: { id: ExportKind; label: string; desc: string; ext: string }[] = [
|
||||||
|
{
|
||||||
|
id: "ip-list",
|
||||||
|
label: "IP address list",
|
||||||
|
desc: "Plain text — A/AAAA + resolved IP",
|
||||||
|
ext: "txt",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dns-hosts",
|
||||||
|
label: "Hosts-style DNS map",
|
||||||
|
desc: "IP ↔ hostname lines for tooling",
|
||||||
|
ext: "hosts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "infra-json",
|
||||||
|
label: "Infrastructure bundle",
|
||||||
|
desc: "DNS, edge, geo subset (not geoip presets)",
|
||||||
|
ext: "json",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function NetworkExport({ result }: { result: ScanResult }) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [kind, setKind] = useState<ExportKind>("ip-list");
|
||||||
|
|
||||||
|
const preview = useMemo(() => {
|
||||||
|
if (kind === "ip-list") return buildIpList(result).slice(0, 400);
|
||||||
|
if (kind === "dns-hosts") return buildHostsFile(result).slice(0, 400);
|
||||||
|
return buildInfraBundle(result).slice(0, 400);
|
||||||
|
}, [kind, result]);
|
||||||
|
|
||||||
|
const exportFile = () => {
|
||||||
|
const opt = OPTIONS.find((o) => o.id === kind)!;
|
||||||
|
const base = `${result.domain}-network`;
|
||||||
|
if (kind === "ip-list") {
|
||||||
|
downloadText(`${base}-ips.${opt.ext}`, buildIpList(result));
|
||||||
|
} else if (kind === "dns-hosts") {
|
||||||
|
downloadText(`${base}.${opt.ext}`, buildHostsFile(result));
|
||||||
|
} else {
|
||||||
|
downloadText(`${base}.${opt.ext}`, buildInfraBundle(result), "application/json");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="border-violet-500/20">
|
||||||
|
<CardHeader className="cursor-pointer" onClick={() => setOpen(!open)}>
|
||||||
|
<CardTitle className="flex items-center justify-between gap-2 text-lg">
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<Network className="h-5 w-5 text-violet-400" />
|
||||||
|
Network export
|
||||||
|
</span>
|
||||||
|
<ChevronDown
|
||||||
|
className={`h-5 w-5 text-zinc-500 transition ${open ? "rotate-180" : ""}`}
|
||||||
|
/>
|
||||||
|
</CardTitle>
|
||||||
|
<p className="text-sm font-normal text-zinc-500">
|
||||||
|
Export resolved IPs and DNS from this scan — infrastructure report, not routing presets.
|
||||||
|
</p>
|
||||||
|
</CardHeader>
|
||||||
|
{open && (
|
||||||
|
<CardContent className="space-y-4 border-t border-white/5 pt-4">
|
||||||
|
<div className="grid gap-2 sm:grid-cols-3">
|
||||||
|
{OPTIONS.map((o) => (
|
||||||
|
<button
|
||||||
|
key={o.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setKind(o.id)}
|
||||||
|
className={`rounded-xl border p-3 text-left transition ${
|
||||||
|
kind === o.id
|
||||||
|
? "border-violet-500/50 bg-violet-500/10"
|
||||||
|
: "border-white/10 bg-white/5 hover:border-white/20"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<p className="text-sm font-medium text-zinc-200">{o.label}</p>
|
||||||
|
<p className="mt-1 text-xs text-zinc-500">{o.desc}</p>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<pre className="max-h-40 overflow-auto rounded-xl bg-black/40 p-3 font-mono text-xs text-zinc-400">
|
||||||
|
{preview}
|
||||||
|
{preview.length >= 400 ? "\n…" : ""}
|
||||||
|
</pre>
|
||||||
|
<Button type="button" onClick={exportFile}>
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
Download {OPTIONS.find((o) => o.id === kind)?.label}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Badge, Skeleton } from "@/components/ui/badge";
|
import { Badge, Skeleton } from "@/components/ui/badge";
|
||||||
import type { ScanResult } from "@/lib/types";
|
import type { ScanResult } from "@/lib/types";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { NetworkExport } from "@/components/scan/network-export";
|
||||||
|
|
||||||
export function ScanPanel() {
|
export function ScanPanel() {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@@ -269,6 +270,10 @@ function ScanResults({ result, scanId }: { result: ScanResult; scanId: string |
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<NetworkExport result={result} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Domain Scanner — one-command uninstall
|
# Domain Scanner — one-command uninstall
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
INSTALL_DIR="${DOMAIN_SCANNER_INSTALL_DIR:-/opt/domain_web}"
|
INSTALL_DIR="${DOMAIN_SCANNER_INSTALL_DIR:-/opt/domain-scanner}"
|
||||||
SERVICE_NAME="domain-scanner"
|
SERVICE_NAME="domain-scanner"
|
||||||
NGINX_SITE="domain-scanner"
|
NGINX_SITE="domain-scanner"
|
||||||
LEGACY_SERVICE="geoexport-site"
|
LEGACY_SERVICE="geoexport-site"
|
||||||
|
|||||||
Reference in New Issue
Block a user