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

1265 lines
54 KiB
HTML
Executable File

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ROFMAD - {% block title %}{% endblock %}</title>
<style>
{% include "styles.html" %}
</style>
{% block extra_head %}{% endblock %}
</head>
<body>
<!-- Navigation Bar -->
<nav class="navbar">
<div class="nav-brand">
<h1>ROFMAD</h1>
</div>
<div class="nav-links">
<a href="/">Dashboard</a>
<a href="/scanners" data-admin-only="true">Scanners</a>
<a href="/hosts">Hosts</a>
<a href="/actions/builder" data-admin-only="true">Actions</a>
<a href="/tags">Tags</a>
<a href="/queue">Queue</a>
<a href="/logs">Logs</a>
<a href="/settings" data-admin-only="true">Settings</a>
</div>
</nav>
<!-- Main Content -->
<main class="container">
{% block content %}{% endblock %}
</main>
<script>
class TerminalModal {
constructor() {
this.modal = null;
this.isOpen = false;
this.taskId = null;
this.pollInterval = null;
}
open(title, taskId) {
if (this.isOpen) this.close();
this.taskId = taskId;
this.createModal(title);
this.startPolling();
}
createModal(title) {
this.modal = document.createElement('div');
this.modal.className = 'terminal-modal-overlay';
this.modal.innerHTML = `
<div class="terminal-modal">
<div class="terminal-header">
<h3 class="terminal-title">${title}</h3>
<div class="terminal-controls">
<button class="terminal-btn" onclick="terminalModal.copyAll()">Copy All</button>
<button class="terminal-btn terminal-close" onclick="terminalModal.close()">Close</button>
</div>
</div>
<div class="terminal-body">
<div class="terminal-output" id="terminalOutput">Loading...</div>
<div class="terminal-status" id="terminalStatus">Connecting...</div>
</div>
</div>
`;
document.body.appendChild(this.modal);
this.modal.style.display = 'flex';
this.isOpen = true;
// Close handlers
document.addEventListener('keydown', this.handleKeydown.bind(this));
this.modal.addEventListener('click', (e) => {
if (e.target === this.modal) this.close();
});
}
startPolling() {
this.pollInterval = setInterval(async () => {
try {
const response = await fetch(`/api/task/${this.taskId}/output`);
const data = await response.json();
this.updateOutput(data.output);
if (data.output.includes('complete') || data.output.includes('stopped')) {
document.getElementById('terminalStatus').textContent = 'Complete';
}
} catch (error) {
console.error('Failed to fetch output:', error);
}
}, 1000);
}
updateOutput(output) {
const outputEl = document.getElementById('terminalOutput');
if (outputEl) {
outputEl.textContent = output;
outputEl.scrollTop = outputEl.scrollHeight;
}
}
copyAll() {
const output = document.getElementById('terminalOutput').textContent;
if (window.ROFMAD) {
window.ROFMAD.copyToClipboard(output);
}
}
close() {
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = null;
}
if (this.modal) {
document.body.removeChild(this.modal);
this.modal = null;
}
this.isOpen = false;
this.taskId = null;
document.removeEventListener('keydown', this.handleKeydown.bind(this));
}
handleKeydown(e) {
if (e.key === 'Escape' && this.isOpen) {
this.close();
}
}
}
class DiscoveryResultsModal {
constructor() {
this.modal = null;
this.isOpen = false;
}
showDiscoveryResults(results, toolType) {
if (this.isOpen) this.close();
const title = toolType === 'dns-search' ? 'DNS Discovery Results' : 'Interface Discovery Results';
this.createModal(title);
this.displayResults(results);
}
createModal(title) {
this.modal = document.createElement('div');
this.modal.className = 'discovery-modal-overlay';
this.modal.innerHTML = `
<div class="discovery-modal">
<div class="discovery-modal-header">
<h3 class="discovery-modal-title">${title}</h3>
<button class="discovery-modal-close" onclick="discoveryModal.close()">Close</button>
</div>
<div class="discovery-modal-body" id="discoveryModalBody">
<!-- Content will be populated -->
</div>
</div>
`;
document.body.appendChild(this.modal);
this.modal.style.display = 'flex';
this.isOpen = true;
// Close handlers
document.addEventListener('keydown', this.handleKeydown.bind(this));
this.modal.addEventListener('click', (e) => {
if (e.target === this.modal) this.close();
});
}
displayResults(results) {
const body = document.getElementById('discoveryModalBody');
let content = '';
// Only show Hosts table if we have IPs
if (results.ips && results.ips.length > 0) {
content += `
<div class="discovery-section">
<h3>Discovered Hosts (${results.ips.length})</h3>
<table class="discovery-table">
<thead>
<tr>
<th>IP Address</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${results.ips.map(ip => `
<tr data-target="${ip}">
<td><code>${ip}</code></td>
<td>
<div class="discovery-actions">
<button class="discovery-btn discovery-btn-primary" onclick="discoveryModal.addToTargets('${ip}')">
Add to Targets
</button>
<button class="discovery-btn discovery-btn-danger" onclick="discoveryModal.blacklist('${ip}')">
Blacklist
</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
}
// Only show Subnets table if we have subnets
if (results.subnets && results.subnets.length > 0) {
content += `
<div class="discovery-section">
<h3>Discovered Subnets (${results.subnets.length})</h3>
<table class="discovery-table">
<thead>
<tr>
<th>Subnet</th>
<th>Size</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${results.subnets.map(subnet => {
const prefix = parseInt(subnet.split('/')[1]);
const sizeInfo = this.getSubnetSizeInfo(prefix);
const actions = this.getSubnetActionButtons(subnet, prefix);
return `
<tr data-target="${subnet}">
<td><code>${subnet}</code></td>
<td>${sizeInfo}</td>
<td>
<div class="discovery-actions">
${actions}
<button class="discovery-btn discovery-btn-danger" onclick="discoveryModal.blacklist('${subnet}')">
Blacklist
</button>
</div>
</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
`;
}
// If no results at all
if (!content) {
content = `
<div class="discovery-empty">
<p>No new targets discovered</p>
<p>All discovered items may already be known or blacklisted.</p>
</div>
`;
}
body.innerHTML = content;
}
getSubnetSizeInfo(prefix) {
if (prefix === 24) {
return '/24 (256 hosts)';
} else if (prefix > 24) {
const hosts = Math.pow(2, 32 - prefix);
return `/${prefix} (${hosts} hosts - smaller)`;
} else {
const hosts = Math.pow(2, 32 - prefix);
return `/${prefix} (${hosts.toLocaleString()} hosts - larger)`;
}
}
getSubnetActionButtons(subnet, prefix) {
const baseIp = subnet.split('/')[0];
if (prefix === 24) {
// /24 networks: Add subnet | Blacklist
return `
<button class="discovery-btn discovery-btn-primary" onclick="discoveryModal.addSubnet('${subnet}')">
Add Subnet
</button>
`;
} else if (prefix > 24) {
// Smaller than /24: Add as /24 | Blacklist
return `
<button class="discovery-btn discovery-btn-secondary" onclick="discoveryModal.addSubnet('${baseIp}/24')">
Add as /24
</button>
`;
} else {
// Larger than /24: Launch Masscan | Add as /24 | Blacklist
return `
<button class="discovery-btn discovery-btn-warning" onclick="discoveryModal.launchMasscan('${subnet}')">
Launch Masscan
</button>
<button class="discovery-btn discovery-btn-secondary" onclick="discoveryModal.addSubnet('${baseIp}/24')">
Add as /24
</button>
`;
}
}
// Action handlers
async addToTargets(ip) {
await this.performAction('/add-target', `target=${encodeURIComponent(ip)}`, `Added ${ip} to targets`);
}
async addSubnet(subnet) {
await this.performAction('/add-subnet', `subnet=${encodeURIComponent(subnet)}`, `Added ${subnet} for scanning`);
}
async blacklist(target) {
await this.performAction('/add-blacklist', `target=${encodeURIComponent(target)}`, `Blacklisted ${target}`);
}
async launchMasscan(subnet) {
await this.performAction('/add-subnet', `subnet=${encodeURIComponent(subnet)}`, `Launched Masscan on ${subnet}`);
}
async performAction(endpoint, body, successMessage) {
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body
});
const data = await response.json();
if (data.success) {
ROFMAD.showNotification(successMessage, 'success');
// Remove the row from display
const target = body.split('=')[1];
const decodedTarget = decodeURIComponent(target);
this.removeRowByTarget(decodedTarget);
} else {
ROFMAD.showNotification(`Action failed: ${data.message}`, 'error');
}
} catch (error) {
ROFMAD.showNotification(`Action error: ${error.message}`, 'error');
}
}
removeRowByTarget(target) {
const row = document.querySelector(`tr[data-target="${target}"]`);
if (row) {
row.style.transition = 'opacity 0.3s ease';
row.style.opacity = '0';
setTimeout(() => {
row.remove();
this.checkIfTablesEmpty();
}, 300);
}
}
checkIfTablesEmpty() {
const sections = document.querySelectorAll('.discovery-section');
sections.forEach(section => {
const rows = section.querySelectorAll('tbody tr');
if (rows.length === 0) {
section.style.transition = 'opacity 0.3s ease';
section.style.opacity = '0';
setTimeout(() => section.remove(), 300);
}
});
// If no sections left, show empty message
if (document.querySelectorAll('.discovery-section').length === 0) {
document.getElementById('discoveryModalBody').innerHTML = `
<div class="discovery-empty">
<p>All actions completed!</p>
<p>No more items to process.</p>
</div>
`;
}
}
close() {
if (this.modal) {
document.body.removeChild(this.modal);
this.modal = null;
}
this.isOpen = false;
document.removeEventListener('keydown', this.handleKeydown.bind(this));
}
handleKeydown(e) {
if (e.key === 'Escape' && this.isOpen) {
this.close();
}
}
}
// Global instances
window.terminalModal = new TerminalModal();
window.discoveryModal = new DiscoveryResultsModal();
// Selective refresh - only update counters and analysis list
const selectiveRefresh = {
intervals: {},
startCounterRefresh: () => {
selectiveRefresh.intervals.counters = setInterval(async () => {
try {
const response = await fetch('/api/task-status');
if (response.ok) {
const data = await response.json();
selectiveRefresh.updateCounters(data);
}
} catch (error) {
// Silent fail
}
}, 3000);
},
startAnalysisRefresh: () => {
selectiveRefresh.intervals.analysis = setInterval(async () => {
try {
const response = await fetch('/api/hosts-ready-analysis');
if (response.ok) {
const data = await response.json();
selectiveRefresh.updateAnalysisList(data);
}
} catch (error) {
// Silent fail
}
}, 15000);
},
updateCounters: (data) => {
const updates = {
'queued-count': data.queued_count,
'active-count': data.active_count
};
Object.entries(updates).forEach(([id, value]) => {
const element = document.getElementById(id);
if (element) {
element.textContent = value;
}
});
},
updateAnalysisList: (data) => {
const container = document.querySelector('.hosts-ready-analysis');
if (!container) return;
const isEmptyState = container.querySelector('.empty-placeholder');
if (data.hosts.length === 0 && !isEmptyState) {
container.innerHTML = `
<div class="empty-placeholder">
<h3>All Hosts Analyzed</h3>
<p>No more hosts ready for analysis at this time.</p>
</div>
`;
container.className = 'empty-ready-hosts hosts-ready-analysis'; // FIX: Update class
return;
}
if (data.hosts.length > 0 && isEmptyState) {
container.innerHTML = '';
container.className = 'ready-hosts-grid hosts-ready-analysis';
}
if (data.hosts.length > 0) {
const currentIPs = new Set(
Array.from(container.querySelectorAll('[data-host-ip]'))
.map(el => el.getAttribute('data-host-ip'))
);
const newIPs = new Set(data.hosts.map(h => h.ip));
// FIX: Always update if different
if (currentIPs.size !== newIPs.size || ![...currentIPs].every(ip => newIPs.has(ip))) {
container.className = 'ready-hosts-grid hosts-ready-analysis';
container.innerHTML = data.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('');
}
}
},
stop: () => {
Object.values(selectiveRefresh.intervals).forEach(clearInterval);
selectiveRefresh.intervals = {};
}
};
// Tab switching functionality
function initTabs() {
const tabButtons = document.querySelectorAll('.tab-btn');
const tabContents = document.querySelectorAll('.tab-content');
tabButtons.forEach(button => {
button.addEventListener('click', () => {
const targetTab = button.getAttribute('data-tab');
// Remove active classes
tabButtons.forEach(btn => btn.classList.remove('active'));
tabContents.forEach(content => content.classList.remove('active'));
// Add active classes
button.classList.add('active');
const targetContent = document.getElementById(targetTab);
if (targetContent) {
targetContent.classList.add('active');
}
});
});
}
// Status indicator tooltips
function initTooltips() {
const statusIndicators = document.querySelectorAll('.status-indicator');
statusIndicators.forEach(indicator => {
const title = indicator.getAttribute('title');
if (title) {
indicator.addEventListener('mouseenter', (e) => {
const tooltip = document.createElement('div');
tooltip.className = 'tooltip';
tooltip.textContent = title;
tooltip.style.cssText = `
position: absolute;
background: #1f1f1f;
color: #f8fafc;
padding: 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
border: 1px solid #374151;
z-index: 1000;
pointer-events: none;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.4);
`;
const rect = e.target.getBoundingClientRect();
tooltip.style.left = (rect.left + rect.width / 2) + 'px';
tooltip.style.top = (rect.top - 40) + 'px';
tooltip.style.transform = 'translateX(-50%)';
document.body.appendChild(tooltip);
e.target.addEventListener('mouseleave', () => {
if (tooltip.parentNode) {
tooltip.parentNode.removeChild(tooltip);
}
}, { once: true });
});
}
});
}
// Progress bar animations
function animateProgressBars() {
const progressBars = document.querySelectorAll('.progress-fill');
progressBars.forEach(bar => {
const width = bar.style.width;
bar.style.width = '0%';
setTimeout(() => {
bar.style.width = width;
}, 100);
});
}
// Enhanced search functionality with tag preview
function initSearch() {
const searchInput = document.getElementById('search-input');
if (searchInput) {
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
const value = searchInput.value.trim();
if (value) {
window.location.href = `/search?tag=${encodeURIComponent(value)}`;
}
}
});
// Add dynamic tag suggestions if available
searchInput.addEventListener('input', (e) => {
const value = e.target.value.trim();
if (value.length >= 2) {
// Could add dynamic tag suggestions here
// For now, just show a helpful tooltip
if (!document.getElementById('search-tooltip')) {
const tooltip = document.createElement('div');
tooltip.id = 'search-tooltip';
tooltip.style.cssText = `
position: absolute;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 0.5rem;
font-size: 0.8rem;
color: var(--text-muted);
z-index: 1000;
margin-top: 2px;
max-width: 300px;
`;
tooltip.textContent = 'Press Enter to search, or visit Tags page for all available tags';
searchInput.parentNode.appendChild(tooltip);
setTimeout(() => {
const existingTooltip = document.getElementById('search-tooltip');
if (existingTooltip) {
existingTooltip.remove();
}
}, 3000);
}
}
});
}
}
// Table sorting (basic client-side)
function initTableSorting() {
const tables = document.querySelectorAll('.hosts-table, .tasks-table');
tables.forEach(table => {
const headers = table.querySelectorAll('th');
headers.forEach((header, index) => {
if (header.textContent.trim() && !header.classList.contains('no-sort')) {
header.style.cursor = 'pointer';
header.addEventListener('click', () => {
sortTable(table, index);
});
}
});
});
}
function sortTable(table, columnIndex) {
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
const isNumeric = rows.length > 0 && !isNaN(rows[0].cells[columnIndex]?.textContent.trim());
rows.sort((a, b) => {
const aVal = a.cells[columnIndex]?.textContent.trim() || '';
const bVal = b.cells[columnIndex]?.textContent.trim() || '';
if (isNumeric) {
return parseFloat(aVal) - parseFloat(bVal);
} else {
return aVal.localeCompare(bVal);
}
});
rows.forEach(row => tbody.appendChild(row));
}
// Enhanced keyboard shortcuts
function initKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
// Alt + D = Dashboard
if (e.altKey && e.key === 'd') {
e.preventDefault();
window.location.href = '/';
}
// Alt + H = Hosts
if (e.altKey && e.key === 'h') {
e.preventDefault();
window.location.href = '/hosts';
}
// Alt + T = Tags
if (e.altKey && e.key === 't') {
e.preventDefault();
window.location.href = '/tags';
}
// Alt + L = Logs
if (e.altKey && e.key === 'l') {
e.preventDefault();
window.location.href = '/logs';
}
// Alt + A = Actions
if (e.altKey && e.key === 'a') {
e.preventDefault();
window.location.href = '/actions/builder';
}
// Escape = Close modals/go back
if (e.key === 'Escape') {
const activeTab = document.querySelector('.tab-content.active');
if (activeTab && window.history.length > 1) {
window.history.back();
}
}
});
}
// Enhanced tag interaction functionality
function initTagInteractions() {
// Add click-to-search functionality for tag elements
document.addEventListener('click', (e) => {
if (e.target.classList.contains('tag') || e.target.classList.contains('tag-item')) {
e.preventDefault();
const tagText = e.target.textContent.trim();
if (tagText) {
window.location.href = `/tags?tag=${encodeURIComponent(tagText)}`;
}
}
});
// Categorize and style tags based on likely source
document.querySelectorAll('.tag').forEach(tag => {
const tagText = tag.textContent.trim().toLowerCase();
// Action-based tags (from specific_actions) - typically more specific/complex
if (isLikelyActionTag(tagText)) {
tag.classList.add('action-tag');
}
tag.style.cursor = 'pointer';
tag.title = 'Click to search for this tag';
});
}
// Determine if a tag is likely from action output vs automatic detection
function isLikelyActionTag(tagText) {
// Action tags are typically more specific, longer, or contain certain patterns
const actionIndicators = [
// Complex/specific strings that unlikely come from nmap
'domain-controller', 'file-server', 'mail-server', 'web-app-',
'vulnerable-', 'exploit-', 'cve-', 'admin-panel', 'backup-',
'config-', 'default-creds', 'weak-auth', 'anonymous-',
// Length-based heuristic (action tags tend to be longer/more descriptive)
];
// Check for action-specific patterns
if (actionIndicators.some(indicator => tagText.includes(indicator))) {
return true;
}
// Length and complexity heuristic
if (tagText.length > 15 || tagText.includes('-') && tagText.split('-').length > 2) {
return true;
}
return false;
}
// Analysis workflow functions for pentester workflow
function toggleAnalysisStatus(ip, analyzed) {
const row = document.querySelector(`tr[data-ip="${ip}"]`);
const statusContainer = row?.querySelector('.analysis-status-container');
if (!statusContainer) return;
// Add loading animation
statusContainer.classList.add('analysis-updating');
// Make the API call
fetch('/mark-analyzed', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `ip=${encodeURIComponent(ip)}&analyzed=${analyzed}`
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Update the row appearance
if (analyzed) {
row.classList.add('analyzed-host');
statusContainer.innerHTML = `
<div class="analysis-complete" onclick="toggleAnalysisStatus('${ip}', false)">
<span class="analysis-text">Analysis Complete</span>
</div>
`;
ROFMAD.showNotification(`Host ${ip} marked as analyzed`, 'success');
} else {
row.classList.remove('analyzed-host');
statusContainer.innerHTML = `
<div class="analysis-pending" onclick="toggleAnalysisStatus('${ip}', true)">
<span class="analysis-text">User Analysis Complete</span>
</div>
`;
ROFMAD.showNotification(`Host ${ip} marked as pending analysis`, 'info');
}
} else {
ROFMAD.showNotification('Failed to update analysis status', 'error');
}
})
.catch(error => {
console.error('Error updating analysis status:', error);
ROFMAD.showNotification('Failed to update analysis status', 'error');
})
.finally(() => {
// Remove loading animation
statusContainer.classList.remove('analysis-updating');
});
}
function toggleInterestingStatus(ip, interesting) {
fetch('/mark-interesting', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `ip=${encodeURIComponent(ip)}&interesting=${interesting}`
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
ROFMAD.showNotification(`Host ${ip} ${interesting ? 'bookmarked' : 'unbookmarked'}`, 'success');
}
})
.catch(error => {
ROFMAD.showNotification('Failed to update bookmark status', 'error');
});
}
// Function for opening host for analysis from dashboard
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';
// Mark as analyzed with retry logic
const markAnalyzed = async (retryCount = 0) => {
try {
const response = await fetch('/mark-analyzed', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `ip=${encodeURIComponent(ip)}&analyzed=true`,
signal: AbortSignal.timeout(5000) // 5 second timeout
});
const data = await response.json();
if (data.success) {
// Success - remove card
hostCard.style.transition = 'all 0.5s ease';
hostCard.style.transform = 'translateX(100%)';
hostCard.style.opacity = '0';
setTimeout(() => {
hostCard.remove();
// Check if this was the last host card
const remainingCards = document.querySelectorAll('.ready-host-card');
if (remainingCards.length === 0) {
const container = document.querySelector('.hosts-ready-analysis');
if (container) {
container.innerHTML = `
<div class="empty-placeholder">
<h3>All Hosts Analyzed</h3>
<p>No more hosts ready for analysis at this time.</p>
</div>
`;
}
}
}, 500);
// Open host detail in new tab
window.open(`/hosts/${ip}`, '_blank');
if (window.ROFMAD?.showNotification) {
window.ROFMAD.showNotification(`Host ${ip} marked as analyzed`, 'success');
}
} else {
throw new Error('Server returned failure');
}
} catch (error) {
console.error('Error marking host as analyzed:', error);
// Retry logic
if (retryCount < 2) {
setTimeout(() => markAnalyzed(retryCount + 1), 1000);
return;
}
// Final failure - restore card
hostCard.disabled = false;
hostCard.style.opacity = '1';
hostCard.style.pointerEvents = 'auto';
if (window.ROFMAD?.showNotification) {
window.ROFMAD.showNotification(`Failed to mark host ${ip} as analyzed. Please try again.`, 'error');
}
}
};
markAnalyzed();
}
// Enhanced image modal for screenshots
function openImageModal(filename, url) {
const modal = document.getElementById('imageModal');
const modalImg = document.getElementById('modalImage');
const captionText = document.getElementById('modalCaption');
if (modal && modalImg && captionText) {
modal.style.display = 'block';
modalImg.src = '/screenshots/' + filename;
captionText.innerHTML = url;
}
}
function closeImageModal() {
const modal = document.getElementById('imageModal');
if (modal) {
modal.style.display = 'none';
}
}
// Search by tag functionality
function searchTag(tag) {
window.location.href = `/tags?tag=${encodeURIComponent(tag)}`;
}
// Role-based UI management - hide admin-only elements from player accounts
function initRoleBasedUI() {
// Test if user is admin by trying to access a known admin endpoint
fetch('/api/user-role-check', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'test=role'
})
.then(response => response.json())
.then(data => {
if (!data.success || data.message === 'Admin privileges required') {
// User is not admin - hide admin-only elements
hideAdminOnlyElements();
}
})
.catch(() => {
// If request fails, assume not admin and hide elements
hideAdminOnlyElements();
});
}
function hideAdminOnlyElements() {
// Hide navigation links marked with data-admin-only
document.querySelectorAll('[data-admin-only="true"]').forEach(element => {
element.style.display = 'none';
});
}
// Initialize all functionality when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
initTabs();
initTooltips();
animateProgressBars();
initSearch();
initTableSorting();
initKeyboardShortcuts();
initTagInteractions();
initRoleBasedUI();
// Start selective refresh only on dashboard
if (window.location.pathname === '/') {
selectiveRefresh.startCounterRefresh();
selectiveRefresh.startAnalysisRefresh();
}
// Close modal with Escape key
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
closeImageModal();
}
});
// Add smooth scrolling for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
// Add loading states for forms
document.querySelectorAll('form').forEach(form => {
form.addEventListener('submit', function() {
const submitBtn = form.querySelector('button[type="submit"], input[type="submit"]');
if (submitBtn) {
const originalText = submitBtn.textContent || submitBtn.value;
submitBtn.disabled = true;
if (submitBtn.textContent !== undefined) {
submitBtn.textContent = 'Loading...';
} else {
submitBtn.value = 'Loading...';
}
// Restore original state after 5 seconds (fallback)
setTimeout(() => {
submitBtn.disabled = false;
if (submitBtn.textContent !== undefined) {
submitBtn.textContent = originalText;
} else {
submitBtn.value = originalText;
}
}, 5000);
}
});
});
// Highlight current page in navigation
const currentPath = window.location.pathname;
document.querySelectorAll('.nav-links a').forEach(link => {
if (link.getAttribute('href') === currentPath) {
link.style.borderBottomColor = 'var(--gb-blue)';
link.style.color = 'var(--gb-blue-b)';
}
});
// Initialize copy functionality for code blocks
document.querySelectorAll('pre code').forEach(block => {
block.addEventListener('dblclick', function() {
ROFMAD.copyToClipboard(this.textContent);
});
block.title = 'Double-click to copy';
block.style.cursor = 'pointer';
});
// Add expand/collapse for large action outputs
document.querySelectorAll('.action-content pre').forEach(pre => {
if (pre.textContent.length > 2000) {
const lines = pre.textContent.split('\n');
if (lines.length > 50) {
const shortContent = lines.slice(0, 50).join('\n') + '\n\n... (truncated, click to expand)';
const fullContent = pre.textContent;
pre.textContent = shortContent;
pre.style.cursor = 'pointer';
let expanded = false;
pre.addEventListener('click', function() {
if (!expanded) {
this.textContent = fullContent;
expanded = true;
}
});
}
}
});
// Add service URL click functionality
document.querySelectorAll('.service-header h4').forEach(header => {
header.style.cursor = 'pointer';
header.addEventListener('click', function() {
const url = this.textContent.trim();
if (url.startsWith('http')) {
window.open(url, '_blank');
}
});
header.title = 'Click to open in new tab';
});
});
// Stop refresh when page unloads
window.addEventListener('beforeunload', () => {
selectiveRefresh.stop();
});
// Enhanced utility functions available globally
window.ROFMAD = {
// Format numbers with commas
formatNumber: (num) => {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
},
// Format bytes to human readable
formatBytes: (bytes) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
},
// Copy text to clipboard with enhanced error handling
copyToClipboard: (text) => {
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(() => {
ROFMAD.showNotification('Copied to clipboard', 'success');
}).catch((err) => {
console.error('Clipboard API failed:', err);
ROFMAD.fallbackCopyToClipboard(text);
});
} else {
ROFMAD.fallbackCopyToClipboard(text);
}
},
// Fallback copy method for older browsers
fallbackCopyToClipboard: (text) => {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
ROFMAD.showNotification('Copied to clipboard', 'success');
} catch (err) {
console.error('Fallback copy failed:', err);
ROFMAD.showNotification('Failed to copy to clipboard', 'error');
}
document.body.removeChild(textArea);
},
// Enhanced notification system
showNotification: (message, type = 'info') => {
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 1rem 1.5rem;
border-radius: 8px;
color: white;
font-weight: 500;
z-index: 10000;
animation: slideIn 0.3s ease;
max-width: 300px;
word-wrap: break-word;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 255, 255, 0.1);
`;
const colors = {
info: '#3b82f6',
success: '#10b981',
warning: '#f59e0b',
error: '#ef4444'
};
notification.style.backgroundColor = colors[type] || colors.info;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}, 3000);
}
};
// Make analysis functions globally available
window.toggleAnalysisStatus = toggleAnalysisStatus;
window.toggleInterestingStatus = toggleInterestingStatus;
window.openHostForAnalysis = openHostForAnalysis;
window.openImageModal = openImageModal;
window.closeImageModal = closeImageModal;
window.searchTag = searchTag;
// Add CSS animations
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
.tooltip {
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateX(-50%) translateY(5px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
/* Enhanced analysis status styling */
.analysis-updating {
animation: analysisUpdate 0.3s ease;
}
@keyframes analysisUpdate {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
/* Copy button hover effects */
.copy-btn {
transition: background-color 0.1s, color 0.1s;
}
`;
document.head.appendChild(style);
</script>
{% block extra_js %}{% endblock %}
</body>
</html>