Files
2026-06-18 15:03:00 +02:00

1134 lines
41 KiB
HTML
Executable File

{% extends "base.html" %}
{% block extra_head %}
<style>
/* Dashboard */
.dashboard-header {
padding: 1rem 0 1.25rem;
margin-bottom: 1.25rem;
border-bottom: 1px solid var(--gb-bg1);
}
.dashboard-title {
font-size: 1.4rem;
font-weight: 700;
color: var(--gb-blue-b);
letter-spacing: 0.06em;
text-transform: uppercase;
background: none;
-webkit-text-fill-color: unset;
-webkit-background-clip: unset;
background-clip: unset;
}
.dashboard-subtitle {
color: var(--text-dim);
font-size: 0.78rem;
}
.stat-icon {
display: none;
}
.stat-description {
color: var(--text-dim);
font-size: 0.72rem;
display: block;
margin-top: 0.2rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Discovery cards */
.discovery-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 0.75rem;
}
.discovery-card {
background-color: var(--gb-bg);
padding: 1.25rem;
border-left: 2px solid var(--gb-blue);
position: relative;
transition: border-left-color 0.1s, background-color 0.1s;
}
.discovery-card::before {
display: none;
}
.discovery-card:hover {
background-color: var(--gb-bg-s);
border-left-color: var(--gb-purple);
}
.discovery-card h4 {
color: var(--text-muted);
margin-bottom: 0.5rem;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.08em;
font-weight: 600;
}
.discovery-value {
font-size: 1.8rem;
font-weight: 700;
color: var(--gb-blue-b);
display: block;
font-family: 'JetBrains Mono', monospace;
}
.discovery-detail {
color: var(--text-dim);
font-size: 0.72rem;
margin-top: 0.2rem;
}
/* Ready analysis section */
.ready-analysis-section {
border-left: 3px solid var(--gb-blue);
}
.ready-analysis-section::before {
display: none;
}
.ready-hosts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 0.75rem;
}
.ready-host-card {
background-color: var(--gb-bg1);
border: 1px solid var(--gb-bg2);
border-left: 3px solid var(--gb-blue);
padding: 1rem;
cursor: pointer;
transition: background-color 0.1s, border-color 0.1s;
position: relative;
}
.ready-host-card::before {
display: none;
}
.ready-host-card:hover {
background-color: var(--gb-bg-s);
border-left-color: var(--gb-purple);
border-color: var(--gb-blue);
}
.ready-host-header h4 {
color: var(--gb-blue-b);
margin: 0;
font-size: 0.95rem;
font-weight: 700;
}
.ready-host-hostname {
color: var(--text-dim);
font-size: 0.75rem;
font-style: italic;
}
.ready-host-stats {
display: flex;
gap: 0;
margin: 0.75rem 0;
border: 1px solid var(--gb-bg1);
}
.ready-stat {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
padding: 0.5rem;
border-right: 1px solid var(--gb-bg1);
}
.ready-stat:last-child {
border-right: none;
}
.ready-stat-number {
color: var(--text-primary);
font-size: 1.1rem;
font-weight: 700;
font-family: 'JetBrains Mono', monospace;
}
.ready-stat-label {
color: var(--text-dim);
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.06em;
margin-top: 0.15rem;
}
.nuclei-severity-stats .ready-stat.severity-critical .ready-stat-number { color: var(--gb-red); }
.nuclei-severity-stats .ready-stat.severity-high .ready-stat-number { color: var(--gb-orange); }
.nuclei-severity-stats .ready-stat.severity-medium .ready-stat-number { color: var(--gb-yellow); }
.nuclei-severity-stats .ready-stat.severity-low .ready-stat-number { color: var(--gb-blue); }
.nuclei-severity-stats .ready-stat.severity-info .ready-stat-number,
.nuclei-severity-stats .ready-stat.severity-unknown .ready-stat-number { color: var(--gb-fg2); }
.ready-host-tags {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin-bottom: 0.5rem;
}
.ready-tag {
background: var(--gb-blue);
color: var(--gb-fg);
padding: 0.1rem 0.4rem;
font-size: 0.65rem;
font-weight: 600;
border-radius: 0;
font-family: 'JetBrains Mono', monospace;
}
.ready-tag-more {
background: var(--gb-bg2);
color: var(--text-muted);
padding: 0.1rem 0.4rem;
font-size: 0.65rem;
font-style: italic;
border-radius: 0;
}
.ready-host-action {
padding-top: 0.25rem;
}
.ready-action-text {
color: var(--text-muted);
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.empty-ready-hosts {
min-height: 180px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--gb-bg1);
background-color: var(--gb-bg);
}
.empty-icon {
font-size: 2.5rem;
margin-bottom: 0.75rem;
display: block;
opacity: 0.35;
}
/* Performance */
.performance-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 0.75rem;
}
.performance-card {
background-color: var(--gb-bg);
padding: 1.25rem;
border-left: 2px solid var(--gb-purple);
}
.performance-card h4 {
color: var(--text-muted);
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 0.5rem;
}
.performance-value {
color: var(--gb-blue-b);
font-size: 1.5rem;
font-weight: 700;
font-family: 'JetBrains Mono', monospace;
display: block;
}
.performance-label {
color: var(--text-dim);
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Concurrency */
.concurrency-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 0.75rem;
}
.concurrency-card {
background-color: var(--gb-bg);
padding: 1rem;
text-align: left;
border-left: 2px solid var(--gb-bg3);
}
.concurrency-card h4 {
color: var(--text-muted);
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 0.5rem;
}
.usage-fraction {
color: var(--gb-blue-b);
font-weight: 700;
font-size: 1rem;
font-family: 'JetBrains Mono', monospace;
}
.usage-bar {
background-color: var(--gb-bg1);
height: 8px;
margin: 0.35rem 0;
border: 1px solid var(--gb-bg2);
}
.usage-fill {
background-color: var(--gb-blue);
height: 100%;
}
/* No targets guidance */
.no-targets-guidance {
padding: 2rem;
background-color: var(--gb-bg);
border: 1px solid var(--gb-bg1);
border-left: 3px solid var(--gb-blue);
margin-bottom: 1.5rem;
}
.guidance-icon {
display: none;
}
.guidance-content h2,
.guidance-content h3 {
color: var(--gb-blue-b);
margin-bottom: 0.5rem;
font-size: 1.1rem;
}
.guidance-content p {
color: var(--text-secondary);
margin-bottom: 1rem;
font-size: 0.85rem;
}
.guidance-actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.guidance-btn {
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
.tasks-table-container {
border: 1px solid var(--gb-bg1);
overflow: hidden;
}
@media (max-width: 768px) {
.discovery-grid,
.performance-grid,
.concurrency-grid {
grid-template-columns: 1fr;
}
.ready-hosts-grid {
grid-template-columns: 1fr;
}
.guidance-actions {
flex-direction: column;
}
}
</style>
{% endblock %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<div class="dashboard">
<!-- Enhanced Dashboard Header -->
<div class="dashboard-header">
<h1 class="dashboard-title">ROFMAD Dashboard</h1>
<p class="dashboard-subtitle">Real-time reconnaissance and vulnerability assessment overview</p>
</div>
<!-- Main Statistics Grid -->
<div class="stats-grid">
<div class="stat-card">
<h3>Total Hosts</h3>
<span class="stat-number">{{ stats.total_hosts }}</span>
<span class="stat-description">Discovered targets</span>
</div>
<div class="stat-card">
<h3>Total Subnets</h3>
<span class="stat-number">{{ stats.total_subnets }}</span>
<span class="stat-description">Network segments</span>
</div>
<div class="stat-card">
<h3>Active Tasks</h3>
<span class="stat-number" id="active-count">{{ stats.active_tasks.len() }}</span>
<span class="stat-description">Currently running</span>
</div>
<div class="stat-card">
<h3>Queued Tasks</h3>
<span class="stat-number" id="queued-count">{{ stats.queued_tasks_count }}</span>
<span class="stat-description">Pending execution</span>
</div>
</div>
<!-- No Targets Guidance Box (Only shown when no targets exist) -->
{% if stats.total_hosts == 0 %}
<div class="no-targets-guidance">
<div class="guidance-content">
<h2>Welcome to ROFMAD</h2>
<p>No targets have been discovered yet. Get started by adding targets or configuring network scanning.</p>
<div class="guidance-actions">
<a href="/scanners" class="btn btn-primary guidance-btn">Go to Scanners</a>
<a href="/settings" class="btn btn-secondary guidance-btn">Configure Settings</a>
</div>
</div>
</div>
{% endif %}
<!-- Discovery Statistics Section -->
<div class="section-card">
<div class="section-header">
<h2 class="section-title">
Discovery Statistics
</h2>
<p class="section-subtitle">Overview of reconnaissance progress and findings</p>
</div>
<div class="section-content">
<div class="discovery-grid">
<div class="discovery-card">
<h4>Hosts with Open Ports</h4>
<div class="discovery-info">
<span class="discovery-value">{{ stats.hosts_with_open_ports }}</span>
<span class="discovery-detail">{{ stats.total_open_ports }} total ports discovered</span>
</div>
</div>
<div class="discovery-card">
<h4>Hosts with HTTP Services Found</h4>
<div class="discovery-info">
<span class="discovery-value">{{ stats.hosts_with_http_services }}</span>
<span class="discovery-detail">Http services found by probbing</span>
</div>
</div>
<div class="discovery-card">
<h4>Screenshots Captured</h4>
<div class="discovery-info">
<span class="discovery-value">{{ stats.total_screenshots }}</span>
<span class="discovery-detail">web interface captures</span>
</div>
</div>
</div>
</div>
</div>
<!-- Hosts Ready for Analysis Section -->
<div class="section-card ready-analysis-section">
<div class="section-header">
<h2 class="section-title">
Hosts Ready for Analysis
</h2>
<p class="section-subtitle">Targets that have completed all reconnaissance phases and are ready for manual
security assessment</p>
</div>
<div class="section-content">
{% if stats.hosts_ready_for_analysis.len() > 0 %}
<div class="ready-hosts-grid hosts-ready-analysis">
{% for host in stats.hosts_ready_for_analysis %}
<div class="ready-host-card" data-host-ip="{{ host.ip }}"
onclick="openHostForAnalysis('{{ host.ip }}')">
<div class="ready-host-header">
<h4>{{ host.ip }}</h4>
{% if host.hostname != "" %}
<span class="ready-host-hostname">{{ host.hostname }}</span>
{% endif %}
</div>
<div class="ready-host-stats">
<div class="ready-stat">
<span class="ready-stat-number">{{ host.ports_count }}</span>
<span class="ready-stat-label">Ports</span>
</div>
<div class="ready-stat">
<span class="ready-stat-number">{{ host.http_services_count }}</span>
<span class="ready-stat-label">HTTP</span>
</div>
<div class="ready-stat">
<span class="ready-stat-number">{{ host.action_outputs_count }}</span>
<span class="ready-stat-label">Actions</span>
</div>
</div>
<div class="ready-host-tags">
{% for tag in host.tags_with_source %}
{% if loop.index <= 6 %} <span class="ready-tag tag-{{ tag.source }}">{{ tag.name }}</span>
{% endif %}
{% endfor %}
{% if host.tags_with_source.len() > 6 %}
<span class="ready-tag-more">+{{ host.tags_with_source.len() - 6 }} more</span>
{% endif %}
</div>
<div class="ready-host-action">
<span class="ready-action-text">Analyze</span>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-ready-hosts hosts-ready-analysis">
<div class="empty-placeholder">
<h3>Scanning in progress</h3>
<p>Hosts will appear here once all scanning phases and workflow actions are complete.</p>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Nuclei Findings Section — always visible, updated by JS polling -->
<div class="section-card nuclei-findings-section">
<div class="section-header">
<h2 class="section-title">Nuclei Findings</h2>
<p class="section-subtitle">Hosts with nuclei vulnerability scan results ready for review</p>
</div>
<div class="section-content">
{% if stats.hosts_with_nuclei_findings.len() > 0 %}
<div class="ready-hosts-grid hosts-nuclei-findings">
{% for host in stats.hosts_with_nuclei_findings %}
<div class="ready-host-card" data-host-ip="{{ host.ip }}"
onclick="openNucleiHost('{{ host.ip }}')">
<div class="ready-host-header">
<h4>{{ host.ip }}</h4>
{% if host.hostname != "" %}
<span class="ready-host-hostname">{{ host.hostname }}</span>
{% endif %}
</div>
<div class="ready-host-stats nuclei-severity-stats">
{% if host.nuclei_severity_counts.critical > 0 %}
<div class="ready-stat severity-critical">
<span class="ready-stat-number">{{ host.nuclei_severity_counts.critical }}</span>
<span class="ready-stat-label">Critical</span>
</div>
{% endif %}
{% if host.nuclei_severity_counts.high > 0 %}
<div class="ready-stat severity-high">
<span class="ready-stat-number">{{ host.nuclei_severity_counts.high }}</span>
<span class="ready-stat-label">High</span>
</div>
{% endif %}
{% if host.nuclei_severity_counts.medium > 0 %}
<div class="ready-stat severity-medium">
<span class="ready-stat-number">{{ host.nuclei_severity_counts.medium }}</span>
<span class="ready-stat-label">Medium</span>
</div>
{% endif %}
{% if host.nuclei_severity_counts.low > 0 %}
<div class="ready-stat severity-low">
<span class="ready-stat-number">{{ host.nuclei_severity_counts.low }}</span>
<span class="ready-stat-label">Low</span>
</div>
{% endif %}
{% if host.nuclei_severity_counts.info > 0 %}
<div class="ready-stat severity-info">
<span class="ready-stat-number">{{ host.nuclei_severity_counts.info }}</span>
<span class="ready-stat-label">Info</span>
</div>
{% endif %}
{% if host.nuclei_severity_counts.unknown > 0 %}
<div class="ready-stat severity-unknown">
<span class="ready-stat-number">{{ host.nuclei_severity_counts.unknown }}</span>
<span class="ready-stat-label">Unknown</span>
</div>
{% endif %}
</div>
<div class="ready-host-action">
<span class="ready-action-text">View Findings</span>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-ready-hosts hosts-nuclei-findings">
<div class="empty-placeholder">
<h3>No Nuclei Findings</h3>
<p>Use the "Run Nuclei" button on any HTTP service to scan for vulnerabilities.</p>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Active Tasks Section -->
{% if stats.active_tasks.len() > 0 %}
<div class="section-card" id="active-tasks-section">
<div class="section-header">
<h2 class="section-title">
Active Tasks
</h2>
<p class="section-subtitle">Real-time monitoring of running reconnaissance tasks and their progress</p>
</div>
<div class="section-content">
<div class="tasks-table-container">
<table class="tasks-table" id="active-tasks-table">
<thead>
<tr>
<th>Task</th>
<th>Target</th>
<th>Duration</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for task in stats.active_tasks %}
<tr>
<td class="task-name">{{ task.name }}</td>
<td class="task-target">{{ task.target }}</td>
<td class="task-duration">{{ task.duration }}</td>
<td class="task-actions">
<a href="/task/{{ task.id }}" class="btn btn-detail btn-small">Detail</a>
<button class="btn btn-danger btn-small"
onclick="killTask('{{ task.id }}', '{{ task.name }}')">Kill</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
<!-- System Performance Section -->
<div class="section-card">
<div class="section-header">
<h2 class="section-title">
System Performance
</h2>
<p class="section-subtitle">Monitor system resources and task execution efficiency</p>
</div>
<div class="section-content">
<div class="performance-grid">
<div class="performance-card">
<h4>Task Queue</h4>
<div class="performance-info">
<span class="performance-value">{{ stats.active_tasks.len() }}/{{ stats.queued_tasks_count
}}</span>
<span class="performance-label">Active/Queued</span>
</div>
</div>
<div class="performance-card">
<h4>Discovery Progress</h4>
<div class="performance-info">
<span class="performance-value">{{ stats.hosts_with_open_ports }}/{{ stats.total_hosts }}</span>
<span class="performance-label">Hosts Scanned</span>
</div>
</div>
</div>
</div>
</div>
<!-- Concurrency Management Section -->
<div class="section-card">
<div class="section-header">
<h2 class="section-title">
Concurrency Status
</h2>
<p class="section-subtitle">Real-time semaphore usage and resource allocation</p>
</div>
<div class="section-content">
<div class="concurrency-grid">
{% for group in stats.concurrency_stats.display_groups %}
<div class="concurrency-card">
<h4>{{ group.name }}</h4>
<div class="concurrency-info">
<div class="concurrency-usage">
<span class="usage-fraction">{{ group.in_use }}/{{ group.total_limit }}</span>
<div class="usage-bar">
<div class="usage-fill"
style="width: {% if group.total_limit > 0 %}{{ (group.in_use * 100 / group.total_limit) }}{% else %}0{% endif %}%">
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<script>
// Kill task function
async function killTask(taskId, taskName) {
if (!confirm(`Kill task "${taskName}"?`)) {
return;
}
try {
const response = await fetch(`/api/kill-task/${taskId}`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
if (window.ROFMAD?.showNotification) {
window.ROFMAD.showNotification(data.message, 'success');
}
// Task will disappear on next refresh cycle
} else {
alert('Failed to kill task: ' + (data.message || 'Unknown error'));
}
} catch (error) {
console.error('Error killing task:', error);
alert('Failed to kill task: ' + error.message);
}
}
// Dashboard refresh functionality
async function updateDashboardContent() {
if (document.hidden || window.location.pathname !== '/') return;
try {
// Fetch all dashboard data
const [taskResponse, analysisResponse, concurrencyResponse, activeTasksResponse, discoveryResponse, nucleiResponse] = await Promise.all([
fetch('/api/task-status'),
fetch('/api/hosts-ready-analysis'),
fetch('/api/concurrency-status'),
fetch('/api/active-tasks'),
fetch('/api/discovery-stats'),
fetch('/api/nuclei-findings')
]);
if (taskResponse.ok) {
const taskData = await taskResponse.json();
updateTaskCounters(taskData);
}
if (analysisResponse.ok) {
const analysisData = await analysisResponse.json();
updateReadyForAnalysisSection(analysisData.hosts);
}
if (concurrencyResponse.ok) {
const concurrencyData = await concurrencyResponse.json();
updateConcurrencyDisplay(concurrencyData);
}
if (activeTasksResponse.ok) {
const activeTasksData = await activeTasksResponse.json();
updateActiveTasksTable(activeTasksData.tasks);
}
if (discoveryResponse.ok) {
const discoveryData = await discoveryResponse.json();
updateDiscoveryStatistics(discoveryData);
}
if (nucleiResponse.ok) {
const nucleiData = await nucleiResponse.json();
updateNucleiFindingsSection(nucleiData.hosts);
}
} catch (error) {
console.error('Dashboard update failed:', error);
}
}
function updateTaskCounters(data) {
const activeCountEl = document.getElementById('active-count');
const queuedCountEl = document.getElementById('queued-count');
if (activeCountEl) activeCountEl.textContent = data.active_count;
if (queuedCountEl) queuedCountEl.textContent = data.queued_count;
}
function updateReadyForAnalysisSection(hosts) {
const container = document.querySelector('.hosts-ready-analysis');
if (!container) return;
if (hosts.length === 0) {
container.innerHTML = `
<div class="empty-placeholder">
<h3>Scanning in Progress</h3>
<p>Hosts will appear here once all scanning phases and workflow actions are complete.</p>
</div>
`;
container.className = 'empty-ready-hosts hosts-ready-analysis';
return;
}
container.className = 'ready-hosts-grid hosts-ready-analysis';
// Skip full re-render if the same hosts are already displayed — prevents
// the hover transition from replaying every 2s due to innerHTML replacement.
const existingCards = container.querySelectorAll('.ready-host-card');
const existingIPs = new Set([...existingCards].map(c => c.dataset.hostIp));
const newIPs = new Set(hosts.map(h => h.ip));
if (existingIPs.size === newIPs.size && [...newIPs].every(ip => existingIPs.has(ip))) {
return;
}
container.innerHTML = hosts.map(host => `
<div class="ready-host-card" data-host-ip="${host.ip}" onclick="openHostForAnalysis('${host.ip}')">
<div class="ready-host-header">
<h4>${host.ip}</h4>
${host.hostname ? `<span class="ready-host-hostname">${host.hostname}</span>` : ''}
</div>
<div class="ready-host-stats">
<div class="ready-stat">
<span class="ready-stat-number">${host.ports_count}</span>
<span class="ready-stat-label">Ports</span>
</div>
<div class="ready-stat">
<span class="ready-stat-number">${host.http_services_count}</span>
<span class="ready-stat-label">HTTP</span>
</div>
<div class="ready-stat">
<span class="ready-stat-number">${host.action_outputs_count}</span>
<span class="ready-stat-label">Actions</span>
</div>
</div>
<div class="ready-host-tags">
${host.tags_with_source.slice(0, 6).map(tag =>
`<span class="ready-tag tag-${tag.source}">${tag.name}</span>`
).join('')}
${host.tags_with_source.length > 6 ?
`<span class="ready-tag-more">+${host.tags_with_source.length - 6} more</span>` : ''}
</div>
<div class="ready-host-action">
<span class="ready-action-text">Analyze</span>
</div>
</div>
`).join('');
}
function updateNucleiFindingsSection(hosts) {
const container = document.querySelector('.hosts-nuclei-findings');
if (!container) return;
if (hosts.length === 0) {
container.innerHTML = `
<div class="empty-placeholder">
<h3>No Nuclei Findings</h3>
<p>Use the "Run Nuclei" button on any HTTP service to scan for vulnerabilities.</p>
</div>`;
container.className = 'empty-ready-hosts hosts-nuclei-findings';
return;
}
container.className = 'ready-hosts-grid hosts-nuclei-findings';
// Skip re-render if same hosts already displayed — prevents hover transition flicker
const existingCards = container.querySelectorAll('.ready-host-card');
const existingIPs = new Set([...existingCards].map(c => c.dataset.hostIp));
const newIPs = new Set(hosts.map(h => h.ip));
if (existingIPs.size === newIPs.size && [...newIPs].every(ip => existingIPs.has(ip))) return;
container.innerHTML = hosts.map(host => `
<div class="ready-host-card" data-host-ip="${host.ip}"
onclick="openNucleiHost('${host.ip}')">
<div class="ready-host-header">
<h4>${host.ip}</h4>
${host.hostname ? `<span class="ready-host-hostname">${host.hostname}</span>` : ''}
</div>
<div class="ready-host-stats nuclei-severity-stats">
${['critical', 'high', 'medium', 'low', 'info', 'unknown']
.filter(sev => (host.nuclei_severity_counts?.[sev] || 0) > 0)
.map(sev => `
<div class="ready-stat severity-${sev}">
<span class="ready-stat-number">${host.nuclei_severity_counts[sev]}</span>
<span class="ready-stat-label">${sev.charAt(0).toUpperCase() + sev.slice(1)}</span>
</div>`).join('')}
</div>
<div class="ready-host-action"><span class="ready-action-text">View Findings</span></div>
</div>
`).join('');
}
function updateConcurrencyDisplay(data) {
// Update using existing DOM structure from dashboard.html
(data.display_groups || []).forEach(group => {
const cards = document.querySelectorAll('.concurrency-card');
cards.forEach(card => {
const h4 = card.querySelector('h4');
if (h4 && h4.textContent.trim() === group.name) {
const fraction = card.querySelector('.usage-fraction');
const fill = card.querySelector('.usage-fill');
if (fraction) fraction.textContent = `${group.in_use}/${group.total_limit}`;
if (fill) {
const percentage = group.total_limit > 0 ? (group.in_use * 100 / group.total_limit) : 0;
fill.style.width = `${percentage}%`;
}
}
});
});
}
function updateActiveTasksTable(tasks) {
const activeSection = document.getElementById('active-tasks-section');
if (!activeSection) return;
const tbody = activeSection.querySelector('tbody');
if (!tbody) return;
if (tasks.length > 0) {
// Show the section and update table
activeSection.style.display = 'block';
tbody.innerHTML = tasks.map(task => `
<tr>
<td class="task-name">${task.name}</td>
<td class="task-target">${task.target}</td>
<td class="task-duration">${task.duration}</td>
<td class="task-actions">
<a href="/task/${task.id}" class="btn btn-detail btn-small">Detail</a>
<button class="btn btn-danger btn-small" onclick="killTask('${task.id}', '${task.name}')">Kill</button>
</td>
</tr>
`).join('');
} else {
// Hide section when no active tasks (dashboard conditionally shows this section)
activeSection.style.display = 'none';
}
}
function updateDiscoveryStatistics(data) {
// Update Discovery Statistics section
const hostsWithPortsValue = document.querySelector('.discovery-card:nth-child(1) .discovery-value');
const hostsWithPortsDetail = document.querySelector('.discovery-card:nth-child(1) .discovery-detail');
const httpServicesValue = document.querySelector('.discovery-card:nth-child(2) .discovery-value');
const screenshotsValue = document.querySelector('.discovery-card:nth-child(3) .discovery-value');
if (hostsWithPortsValue) hostsWithPortsValue.textContent = data.hosts_with_open_ports;
if (hostsWithPortsDetail) hostsWithPortsDetail.textContent = `${data.total_open_ports} total ports discovered`;
if (httpServicesValue) httpServicesValue.textContent = data.hosts_with_http_services;
if (screenshotsValue) screenshotsValue.textContent = data.total_screenshots;
// Update placeholder stats if empty state is showing
const totalHostsStat = document.getElementById('total-hosts-stat');
const activeTasksStat = document.getElementById('active-tasks-stat');
if (totalHostsStat) totalHostsStat.textContent = data.total_hosts;
// Note: active-tasks-stat is updated by updateTaskCounters from /api/task-status
}
// Enhanced JavaScript functionality
function openHostForAnalysis(ip) {
const hostCard = event.currentTarget;
// Prevent double clicks
if (hostCard.disabled) return;
hostCard.disabled = true;
// Visual feedback
hostCard.style.opacity = '0.7';
hostCard.style.pointerEvents = 'none';
// Open host detail in new tab
window.open(`/hosts/${ip}`, '_blank');
// Mark this host as analyzed (remove from ready list)
fetch('/mark-analyzed', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `ip=${encodeURIComponent(ip)}&analyzed=true`
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Remove the card with animation
hostCard.style.transition = 'all 0.5s ease';
hostCard.style.transform = 'translateX(100%)';
hostCard.style.opacity = '0';
setTimeout(() => {
hostCard.remove();
}, 500);
if (window.ROFMAD?.showNotification) {
window.ROFMAD.showNotification(`Host ${ip} marked as analyzed`, 'success');
}
} else {
// Restore on failure
hostCard.disabled = false;
hostCard.style.opacity = '1';
hostCard.style.pointerEvents = 'auto';
}
})
.catch(error => {
console.error('Error marking host as analyzed:', error);
// Restore on error
hostCard.disabled = false;
hostCard.style.opacity = '1';
hostCard.style.pointerEvents = 'auto';
if (window.ROFMAD?.showNotification) {
window.ROFMAD.showNotification('Failed to mark host as analyzed', 'error');
}
});
}
function openNucleiHost(ip) {
const card = event.currentTarget;
if (card.disabled) return;
card.disabled = true;
card.style.opacity = '0.7';
card.style.pointerEvents = 'none';
fetch('/mark-nuclei-reviewed', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `ip=${encodeURIComponent(ip)}&nuclei_reviewed=true`
}).finally(() => {
window.open(`/hosts/${ip}`, '_blank');
});
}
let dashboardRefreshInterval;
function startDashboardRefresh() {
// Initial update
updateDashboardContent();
// Set up 2-second interval
dashboardRefreshInterval = setInterval(updateDashboardContent, 2000);
}
function stopDashboardRefresh() {
if (dashboardRefreshInterval) {
clearInterval(dashboardRefreshInterval);
}
}
document.addEventListener('DOMContentLoaded', function () {
// Enhanced statistics animation
const statNumbers = document.querySelectorAll('.stat-number');
statNumbers.forEach((stat, index) => {
const finalValue = parseInt(stat.textContent);
if (finalValue > 0) {
stat.textContent = '0';
setTimeout(() => {
let current = 0;
const increment = Math.max(1, finalValue / 20);
const timer = setInterval(() => {
current += increment;
if (current >= finalValue) {
stat.textContent = finalValue;
clearInterval(timer);
} else {
stat.textContent = Math.floor(current);
}
}, 50);
}, index * 200);
}
});
// Enhanced hover effects for discovery cards
// Discovery card hover is handled by CSS
// Add keyboard shortcuts
document.addEventListener('keydown', function (e) {
if (e.altKey) {
switch (e.key) {
case 'd':
e.preventDefault();
window.location.href = '/';
break;
case 'h':
e.preventDefault();
window.location.href = '/hosts';
break;
case 's':
e.preventDefault();
window.location.href = '/search';
break;
case 't':
e.preventDefault();
window.location.href = '/tags';
break;
}
}
});
// Add tooltips for stats
const statCards = document.querySelectorAll('.stat-card');
statCards.forEach(card => {
card.addEventListener('mouseenter', function () {
const tooltip = document.createElement('div');
tooltip.className = 'tooltip';
tooltip.style.cssText = `
position: absolute;
background: var(--gb-bg1);
color: var(--gb-fg);
padding: 0.4rem 0.75rem;
border-radius: 0;
font-size: 0.78rem;
font-family: 'JetBrains Mono', monospace;
z-index: 1000;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s;
border: 1px solid var(--gb-bg2);
`;
const title = this.querySelector('h3').textContent;
const value = this.querySelector('.stat-number').textContent;
const description = this.querySelector('.stat-description').textContent;
tooltip.innerHTML = `<strong>${title}:</strong> ${value}<br><em>${description}</em>`;
document.body.appendChild(tooltip);
const rect = this.getBoundingClientRect();
tooltip.style.left = (rect.left + rect.width / 2 - tooltip.offsetWidth / 2) + 'px';
tooltip.style.top = (rect.top - tooltip.offsetHeight - 10) + 'px';
setTimeout(() => tooltip.style.opacity = '1', 10);
this.addEventListener('mouseleave', () => {
tooltip.style.opacity = '0';
setTimeout(() => tooltip.remove(), 300);
}, { once: true });
});
});
// Ready host card hover is handled by CSS
// Start real-time updates
startDashboardRefresh();
});
// Stop refresh when page unloads
window.addEventListener('beforeunload', stopDashboardRefresh);
</script>
{% endblock %}