580 lines
No EOL
27 KiB
HTML
580 lines
No EOL
27 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Clickduck Control Center</title>
|
|
<link rel="icon" type="image/png" href="/static/clickduck_logo.png">
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link
|
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap"
|
|
rel="stylesheet">
|
|
<link rel="stylesheet" href="/static/style.css">
|
|
</head>
|
|
|
|
<body class="dark-theme">
|
|
<div class="app-container">
|
|
<!-- Header -->
|
|
<header class="app-header">
|
|
<div class="brand">
|
|
<div class="logo-icon" style="background: none; padding: 0; box-shadow: none;">
|
|
<img src="/static/clickduck_logo.png" alt="Clickduck" style="width: 38px; height: 38px; border-radius: 50%; object-fit: cover; box-shadow: 0 0 12px var(--primary-glow); border: 1.5px solid rgba(56, 189, 248, 0.4);" onerror="this.src='/static/clickduck_logo_small.jpg'">
|
|
</div>
|
|
<div>
|
|
<h1 class="brand-title">Clickduck <span class="badge-tag">v1.0</span></h1>
|
|
<p class="brand-subtitle">Autonomous Playwright Web Crawler & Broken Link Detector</p>
|
|
</div>
|
|
</div>
|
|
<div class="header-status">
|
|
<div id="wsStatus" class="ws-pill connecting" title="WebSocket Connection">
|
|
<span class="status-dot"></span>
|
|
<span class="ws-text">Connecting...</span>
|
|
</div>
|
|
<div id="crawlStatusBadge" class="status-badge idle">
|
|
<span class="pulse-ring"></span>
|
|
<span class="status-label">IDLE</span>
|
|
</div>
|
|
<div id="userProfile" class="user-profile" style="display: none; align-items: center; gap: 0.5rem; margin-left: 0.5rem;">
|
|
<span id="userName" style="font-size: 0.8rem; color: var(--text-muted); font-weight: 500;"></span>
|
|
<button id="btnLogout" class="btn btn-secondary btn-sm" title="Log Out" style="padding: 0.35rem 0.65rem; font-size: 0.8rem; border-radius: var(--radius-sm); display: flex; align-items: center; gap: 0.3rem;">
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
|
|
<polyline points="16 17 21 12 16 7"></polyline>
|
|
<line x1="21" y1="12" x2="9" y2="12"></line>
|
|
</svg>
|
|
<span>Logout</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Main Content Layout -->
|
|
<main class="dashboard-grid">
|
|
|
|
<!-- Top Left: Crawler Configuration -->
|
|
<section class="card config-card">
|
|
<div class="card-header">
|
|
<div class="header-title">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path
|
|
d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z">
|
|
</path>
|
|
<circle cx="12" cy="12" r="3"></circle>
|
|
</svg>
|
|
<h2>Crawler Configuration (.env)</h2>
|
|
</div>
|
|
<span class="header-hint">Synced with .env</span>
|
|
</div>
|
|
|
|
<form id="configForm" class="config-form">
|
|
<!-- Start URL Input -->
|
|
<div class="form-group full-width">
|
|
<label for="startUrl">Start Target URL <span class="required">*</span></label>
|
|
<div class="input-with-icon">
|
|
<span class="input-icon">🌐</span>
|
|
<input type="url" id="startUrl" name="start_url" placeholder="https://example.com" required>
|
|
</div>
|
|
<small class="field-help">The root URL where the crawler begins crawling and finding internal/external
|
|
links.</small>
|
|
</div>
|
|
|
|
<!-- Numeric controls grid -->
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="numTabs">Concurrent Tabs: <span id="numTabsVal" class="val-badge">5</span></label>
|
|
<input type="range" id="numTabs" name="num_tabs" min="1" max="20" value="5" class="range-slider">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="numIterations">Iterations</label>
|
|
<input type="number" id="numIterations" name="num_iterations" min="1" max="100" value="1"
|
|
class="form-input">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Iteration Scope Strategy -->
|
|
<div class="iteration-mode-section" style="margin-bottom: 16px;">
|
|
<div class="subgroup-header" style="margin-bottom: 8px;">
|
|
<span class="subgroup-title">🎯 Iteration Scope & Website Visits</span>
|
|
</div>
|
|
<div class="circuit-mode-selector">
|
|
<label class="radio-card active" id="labelIterModeAll">
|
|
<input type="radio" name="iteration_mode" value="all" id="iterModeAll" checked>
|
|
<div class="radio-card-body">
|
|
<div class="radio-card-header">
|
|
<span class="radio-icon">🌐</span>
|
|
<span class="radio-card-title">All Websites</span>
|
|
</div>
|
|
<span class="radio-card-desc">Crawl all discovered internal/external links per iteration</span>
|
|
</div>
|
|
</label>
|
|
|
|
<label class="radio-card" id="labelIterModeVisits">
|
|
<input type="radio" name="iteration_mode" value="visits" id="iterModeVisits">
|
|
<div class="radio-card-body">
|
|
<div class="radio-card-header">
|
|
<span class="radio-icon">🎯</span>
|
|
<span class="radio-card-title">Limit Visits</span>
|
|
</div>
|
|
<span class="radio-card-desc">Visit up to N pages before starting next iteration</span>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
|
|
<div id="iterVisitsField" class="form-group" style="display: none; margin-top: 10px;">
|
|
<label for="maxVisitsPerIteration">Website Visits per Iteration:</label>
|
|
<div class="input-with-stepper">
|
|
<span class="input-icon">🎯</span>
|
|
<input type="number" id="maxVisitsPerIteration" name="max_visits_per_iteration" min="1" max="10000" value="10"
|
|
class="form-input">
|
|
<span class="input-unit">visits</span>
|
|
</div>
|
|
<small class="field-help">Caps page visits in this iteration. Once reached, Websurfer immediately starts the next iteration.</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toggles Grid -->
|
|
<div class="toggles-grid">
|
|
<label class="toggle-card">
|
|
<div class="toggle-info">
|
|
<span class="toggle-title">Headless Mode</span>
|
|
<span class="toggle-desc">Run browser in background without visible window</span>
|
|
</div>
|
|
<input type="checkbox" id="headless" name="headless" class="toggle-checkbox">
|
|
<span class="toggle-switch"></span>
|
|
</label>
|
|
|
|
<label class="toggle-card">
|
|
<div class="toggle-info">
|
|
<span class="toggle-title">Accept Cookie Banners</span>
|
|
<span class="toggle-desc">Auto-clicks "Accept All", "Allow", "Agree" dialogs</span>
|
|
</div>
|
|
<input type="checkbox" id="acceptCookies" name="accept_cookies" checked class="toggle-checkbox">
|
|
<span class="toggle-switch"></span>
|
|
</label>
|
|
|
|
<label class="toggle-card">
|
|
<div class="toggle-info">
|
|
<span class="toggle-title">Tor Proxy Routing</span>
|
|
<span class="toggle-desc">Route all Chromium traffic through SOCKS5 127.0.0.1:9050</span>
|
|
</div>
|
|
<input type="checkbox" id="useTor" name="use_tor" class="toggle-checkbox">
|
|
<span class="toggle-switch"></span>
|
|
</label>
|
|
|
|
<label class="toggle-card">
|
|
<div class="toggle-info">
|
|
<span class="toggle-title">Randomize Queue</span>
|
|
<span class="toggle-desc">Pick random URLs from queue instead of FIFO order</span>
|
|
</div>
|
|
<input type="checkbox" id="randomizeQueue" name="randomize_queue" checked class="toggle-checkbox">
|
|
<span class="toggle-switch"></span>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Tor Circuit Strategy Sub-Section (Visible when Tor toggle is active) -->
|
|
<div id="torOptionsGroup" class="tor-options-subgroup" style="display: none;">
|
|
<div class="subgroup-header">
|
|
<span class="subgroup-title">🧅 Tor Circuit Rotation Strategy</span>
|
|
</div>
|
|
|
|
<div class="circuit-mode-selector">
|
|
<label class="radio-card active" id="labelTorModeIteration">
|
|
<input type="radio" name="tor_circuit_mode" value="iteration" id="torModeIteration" checked>
|
|
<div class="radio-card-body">
|
|
<div class="radio-card-header">
|
|
<span class="radio-icon">🔄</span>
|
|
<span class="radio-card-title">Per Iteration</span>
|
|
</div>
|
|
<span class="radio-card-desc">Request fresh Tor circuit before each crawl iteration</span>
|
|
</div>
|
|
</label>
|
|
|
|
<label class="radio-card" id="labelTorModePages">
|
|
<input type="radio" name="tor_circuit_mode" value="pages" id="torModePages">
|
|
<div class="radio-card-body">
|
|
<div class="radio-card-header">
|
|
<span class="radio-icon">📄</span>
|
|
<span class="radio-card-title">Per N Pages</span>
|
|
</div>
|
|
<span class="radio-card-desc">Rotate Tor circuit dynamically after crawling N pages</span>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
|
|
<div id="torPagesField" class="form-group tor-pages-field" style="display: none;">
|
|
<label for="torCircuitPages">Rotate Circuit Every (Pages):</label>
|
|
<div class="input-with-stepper">
|
|
<span class="input-icon">📄</span>
|
|
<input type="number" id="torCircuitPages" name="tor_circuit_pages" min="1" max="1000" value="10"
|
|
class="form-input">
|
|
<span class="input-unit">pages</span>
|
|
</div>
|
|
<small class="field-help">Requests a new Tor exit IP & circuit after every N pages crawled across worker
|
|
tabs.</small>
|
|
</div>
|
|
|
|
<div class="form-group tor-fallback-field" style="margin-top: 10px;">
|
|
<label for="torMaxConsecutiveTimeouts">Timeout Fallback (Consecutive Timeouts):</label>
|
|
<div class="input-with-stepper">
|
|
<span class="input-icon">⏱</span>
|
|
<input type="number" id="torMaxConsecutiveTimeouts" name="tor_max_consecutive_timeouts" min="1" max="100" value="5"
|
|
class="form-input">
|
|
<span class="input-unit">timeouts</span>
|
|
</div>
|
|
<small class="field-help">Automatically requests a fresh Tor circuit if 5 consecutive pages fail with net::ERR_TIMED_OUT.</small>
|
|
</div>
|
|
|
|
<!-- European Exit Node Country Selector -->
|
|
<div class="tor-exit-nodes-section">
|
|
<div class="exit-nodes-header">
|
|
<div class="exit-nodes-title-group">
|
|
<span class="subgroup-title">🎯 European Exit Node Countries</span>
|
|
<span id="exitNodesCountBadge" class="count-pill-sm">Any / Global</span>
|
|
</div>
|
|
<div class="country-action-btns">
|
|
<button type="button" id="btnExitMajor" class="btn-chip-action"
|
|
title="Select Germany, Netherlands, France, Switzerland">Major EU</button>
|
|
<button type="button" id="btnExitAll" class="btn-chip-action"
|
|
title="Select all European countries">All EU</button>
|
|
<button type="button" id="btnExitClear" class="btn-chip-action danger"
|
|
title="Clear selection to use any Tor node globally">Clear (Any)</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="countryChipsGrid" class="country-chips-grid">
|
|
<!-- Country chips populated dynamically by app.js -->
|
|
</div>
|
|
|
|
<div class="exit-nodes-summary">
|
|
<span class="summary-icon">📍</span>
|
|
<span id="exitNodesSummaryText" class="summary-text">Active Exit Filter: Worldwide / Any Tor exit
|
|
node</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Collapsible Advanced Settings -->
|
|
<details class="advanced-section">
|
|
<summary class="advanced-summary">
|
|
<span>⚙️ Advanced Crawler Options</span>
|
|
<span class="chevron">▼</span>
|
|
</summary>
|
|
<div class="advanced-body form-row">
|
|
<div class="form-group" style="grid-column: span 2;">
|
|
<label for="userAgentSelect">Browser User-Agent</label>
|
|
<select id="userAgentSelect" class="form-input">
|
|
<option value="random">🔄 Auto-Rotate Realistic Desktop User-Agents (Recommended)</option>
|
|
<option value="default">🌐 Native Chromium Browser Default</option>
|
|
<option value="custom">✏️ Custom User-Agent String...</option>
|
|
</select>
|
|
<input type="text" id="userAgentCustom" name="user_agent" value="random" placeholder="Enter custom User-Agent string..." class="form-input" style="display: none; margin-top: 6px;">
|
|
<small class="field-help">Rotates real modern Chrome, Firefox, Safari & Edge headers to avoid WAF rate-limiting and bot blocks (HTTP 503/429/403).</small>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="timeoutMs">Timeout (ms)</label>
|
|
<input type="number" id="timeoutMs" name="timeout_ms" value="35000" step="1000" class="form-input">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="slowMo">Slow-Mo Delay (ms)</label>
|
|
<input type="number" id="slowMo" name="slow_mo" value="100" step="50" class="form-input">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="torSocksPort">Tor SOCKS Port</label>
|
|
<input type="number" id="torSocksPort" name="tor_socks_port" value="9050" class="form-input">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="torControlPort">Tor Control Port</label>
|
|
<input type="number" id="torControlPort" name="tor_control_port" value="9051" class="form-input">
|
|
</div>
|
|
</div>
|
|
</details>
|
|
|
|
<!-- Form Actions -->
|
|
<div class="form-actions">
|
|
<button type="submit" id="btnStart" class="btn btn-primary">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
|
<polygon points="5 3 19 12 5 21 5 3"></polygon>
|
|
</svg>
|
|
<span>Start Crawling</span>
|
|
</button>
|
|
|
|
<button type="button" id="btnStop" class="btn btn-danger" disabled>
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
|
<rect x="4" y="4" width="16" height="16" rx="2"></rect>
|
|
</svg>
|
|
<span>Stop Crawl</span>
|
|
</button>
|
|
|
|
<button type="button" id="btnSaveEnv" class="btn btn-secondary" title="Save current parameters to .env">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
|
|
<polyline points="17 21 17 13 7 13 7 21"></polyline>
|
|
<polyline points="7 3 7 8 15 8"></polyline>
|
|
</svg>
|
|
<span>Save .env</span>
|
|
</button>
|
|
|
|
<button type="button" id="btnReset" class="btn btn-ghost" title="Reset form to saved .env">
|
|
<span>Reset</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</section>
|
|
|
|
<!-- Top Right: Live Session Metrics -->
|
|
<section class="card stats-card">
|
|
<div class="card-header">
|
|
<div class="header-title">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="18" y1="20" x2="18" y2="10"></line>
|
|
<line x1="12" y1="20" x2="12" y2="4"></line>
|
|
<line x1="6" y1="20" x2="6" y2="14"></line>
|
|
</svg>
|
|
<h2>Live Session Metrics</h2>
|
|
</div>
|
|
<div class="timer-badge">
|
|
<span class="timer-icon">⏱</span>
|
|
<span id="elapsedTimer">00:00:00</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="session-progress-strip">
|
|
<div class="progress-info">
|
|
<span id="progressMessage">Ready to start crawl</span>
|
|
<span id="iterationBadge" class="iter-badge">Iteration 1 / 1</span>
|
|
</div>
|
|
<div class="progress-bar-container">
|
|
<div id="progressBar" class="progress-bar-fill"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- All Iterations Cumulative Stats -->
|
|
<div class="metrics-subgroup">
|
|
<div class="subgroup-header" style="margin-top: 14px; margin-bottom: 8px;">
|
|
<span class="subgroup-title" style="color: #38bdf8;">📊 All Iterations Cumulative</span>
|
|
</div>
|
|
<div class="metrics-grid metrics-grid-2col">
|
|
<div class="metric-tile tile-total-visited">
|
|
<div class="metric-icon">🌐</div>
|
|
<div class="metric-content">
|
|
<div class="metric-val" id="statTotalVisitedAll">0</div>
|
|
<div class="metric-label">Total Websites Visited</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="metric-tile tile-total-errors">
|
|
<div class="metric-icon">⚠️</div>
|
|
<div class="metric-content">
|
|
<div class="metric-val" id="statTotalErrorsAll">0</div>
|
|
<div class="metric-label">Total Errors Seen</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Current Iteration Stats -->
|
|
<div class="metrics-subgroup" style="margin-top: 14px;">
|
|
<div class="subgroup-header" style="margin-bottom: 8px;">
|
|
<span class="subgroup-title" style="color: #94a3b8;">🔄 Current Iteration</span>
|
|
</div>
|
|
<div class="metrics-grid">
|
|
<div class="metric-tile tile-visited">
|
|
<div class="metric-icon">📄</div>
|
|
<div class="metric-content">
|
|
<div class="metric-val" id="statVisited">0</div>
|
|
<div class="metric-label">Pages Visited</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="metric-tile tile-internal">
|
|
<div class="metric-icon">🔗</div>
|
|
<div class="metric-content">
|
|
<div class="metric-val" id="statInternal">0</div>
|
|
<div class="metric-label">Internal Pages</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="metric-tile tile-external">
|
|
<div class="metric-icon">🌐</div>
|
|
<div class="metric-content">
|
|
<div class="metric-val" id="statExternal">0</div>
|
|
<div class="metric-label">External Pages</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="metric-tile tile-cookies">
|
|
<div class="metric-icon">🍪</div>
|
|
<div class="metric-content">
|
|
<div class="metric-val" id="statCookies">0</div>
|
|
<div class="metric-label">Cookies Accepted</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="metric-tile tile-queue">
|
|
<div class="metric-icon">📥</div>
|
|
<div class="metric-content">
|
|
<div class="metric-val" id="statQueue">0</div>
|
|
<div class="metric-label">Queue Backlog</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="metric-tile tile-errors">
|
|
<div class="metric-icon">⚠️</div>
|
|
<div class="metric-content">
|
|
<div class="metric-val" id="statErrors">0</div>
|
|
<div class="metric-label">Failed Websites</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Middle 1: Tor Network Identity & Region -->
|
|
<section class="card tor-card full-width-card" id="torCard">
|
|
<div class="card-header">
|
|
<div class="header-title">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
|
class="tor-shield-icon">
|
|
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
|
|
</svg>
|
|
<h2>Tor Network Identity & Geolocation</h2>
|
|
<span id="torStatusBadge" class="tor-pill tor-off">Tor Inactive</span>
|
|
</div>
|
|
<span class="header-hint" id="torCircuitHint">Queried once per circuit on start/rotation</span>
|
|
</div>
|
|
|
|
<div class="tor-identity-grid">
|
|
<div class="tor-info-tile">
|
|
<span class="tor-label">Tor Exit IP</span>
|
|
<div class="tor-val monospace" id="torIpVal">--</div>
|
|
</div>
|
|
<div class="tor-info-tile">
|
|
<span class="tor-label">Country & Region</span>
|
|
<div class="tor-val" id="torCountryVal">--</div>
|
|
</div>
|
|
<div class="tor-info-tile">
|
|
<span class="tor-label">City</span>
|
|
<div class="tor-val" id="torCityVal">--</div>
|
|
</div>
|
|
<div class="tor-info-tile">
|
|
<span class="tor-label">ISP / Organization</span>
|
|
<div class="tor-val" id="torOrgVal">--</div>
|
|
</div>
|
|
<div class="tor-info-tile">
|
|
<span class="tor-label">Rotation Strategy</span>
|
|
<div class="tor-val" id="torStrategyVal">Per Iteration</div>
|
|
</div>
|
|
<div class="tor-info-tile">
|
|
<span class="tor-label">Circuits Rotated</span>
|
|
<div class="tor-val monospace" id="torRotationsVal">0</div>
|
|
</div>
|
|
<div class="tor-info-tile full-width-sm">
|
|
<span class="tor-label">Exit Node Filter</span>
|
|
<div class="tor-val" id="torFilterVal">Global (Any)</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Middle 2: Dedicated Websites with Errors & Broken Links Section -->
|
|
<section class="card errors-card full-width-card">
|
|
|
|
<div class="card-header">
|
|
<div class="header-title">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f43f5e" stroke-width="2">
|
|
<circle cx="12" cy="12" r="10"></circle>
|
|
<line x1="12" y1="8" x2="12" y2="12"></line>
|
|
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
|
</svg>
|
|
<h2>Websites with Errors & Broken Links</h2>
|
|
<span id="errorCountBadge" class="count-pill">0</span>
|
|
</div>
|
|
<div class="table-toolbar">
|
|
<input type="text" id="errorSearchInput" placeholder="Filter errors or URLs..." class="filter-input">
|
|
<button id="btnExportCsv" class="btn btn-sm btn-outline">
|
|
<span>📥 Export CSV</span>
|
|
</button>
|
|
<button id="btnExportJson" class="btn btn-sm btn-outline">
|
|
<span>{ } JSON</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-container">
|
|
<table class="data-table" id="errorsTable">
|
|
<thead>
|
|
<tr>
|
|
<th width="50">#</th>
|
|
<th>Failed / Broken URL</th>
|
|
<th width="200">Error Reason</th>
|
|
<th>Found On (Source URL)</th>
|
|
<th width="110">Time</th>
|
|
<th width="80">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="errorsTableBody">
|
|
<tr class="empty-row" id="emptyErrorsRow">
|
|
<td colspan="6">
|
|
<div class="empty-state">
|
|
<span class="empty-icon">✨</span>
|
|
<p>No website errors recorded yet. Broken links or HTTP error codes will appear here.</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Bottom: Live Streaming Crawler Logs -->
|
|
<section class="card logs-card full-width-card">
|
|
<div class="card-header">
|
|
<div class="header-title">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline points="4 17 10 11 4 5"></polyline>
|
|
<line x1="12" y1="19" x2="20" y2="19"></line>
|
|
</svg>
|
|
<h2>Live Crawl Logs</h2>
|
|
<span id="logCountBadge" class="count-pill gray">0 lines</span>
|
|
</div>
|
|
<div class="log-toolbar">
|
|
<label class="checkbox-label">
|
|
<input type="checkbox" id="autoScrollCheck" checked>
|
|
<span>Auto-scroll</span>
|
|
</label>
|
|
<select id="logLevelSelect" class="form-select-sm">
|
|
<option value="all">All Levels</option>
|
|
<option value="info">Info</option>
|
|
<option value="success">Success</option>
|
|
<option value="warn">Warnings</option>
|
|
<option value="error">Errors</option>
|
|
</select>
|
|
<input type="text" id="logSearchInput" placeholder="Search logs..." class="filter-input-sm">
|
|
<button id="btnClearLogs" class="btn btn-xs btn-ghost" title="Clear log window">Clear</button>
|
|
<button id="btnCopyLogs" class="btn btn-xs btn-ghost" title="Copy all logs to clipboard">Copy</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="terminal-window" id="terminalContainer">
|
|
<div class="terminal-body" id="logOutput">
|
|
<div class="log-line system-line">
|
|
<span class="log-time">[00:00:00]</span>
|
|
<span class="log-tag tag-sys">[SYSTEM]</span>
|
|
<span class="log-msg">Websurfer dashboard initialized. Configure parameters and click Start
|
|
Crawling.</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
</main>
|
|
|
|
<!-- Toast Notification Container -->
|
|
<div id="toastContainer" class="toast-container"></div>
|
|
</div>
|
|
|
|
<script src="/static/app.js"></script>
|
|
</body>
|
|
|
|
</html> |