Merge branch 'dev' into feat/blocklist-api
This commit is contained in:
@@ -708,6 +708,330 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
||||
max-height: 400px;
|
||||
}}
|
||||
|
||||
/* Mobile Optimization - Tablets (768px and down) */
|
||||
@media (max-width: 768px) {{
|
||||
body {{
|
||||
padding: 12px;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 100%;
|
||||
}}
|
||||
h1 {{
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
}}
|
||||
.github-logo {{
|
||||
position: relative;
|
||||
top: auto;
|
||||
left: auto;
|
||||
margin-bottom: 15px;
|
||||
}}
|
||||
.download-section {{
|
||||
position: relative;
|
||||
top: auto;
|
||||
right: auto;
|
||||
margin-bottom: 20px;
|
||||
}}
|
||||
.stats-grid {{
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}}
|
||||
.stat-value {{
|
||||
font-size: 28px;
|
||||
}}
|
||||
.stat-card {{
|
||||
padding: 15px;
|
||||
}}
|
||||
.table-container {{
|
||||
padding: 12px;
|
||||
margin-bottom: 15px;
|
||||
overflow-x: auto;
|
||||
}}
|
||||
table {{
|
||||
font-size: 13px;
|
||||
}}
|
||||
th, td {{
|
||||
padding: 10px 6px;
|
||||
}}
|
||||
h2 {{
|
||||
font-size: 18px;
|
||||
}}
|
||||
.tabs-container {{
|
||||
gap: 0;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}}
|
||||
.tab-button {{
|
||||
padding: 10px 16px;
|
||||
font-size: 12px;
|
||||
}}
|
||||
.ip-stats-dropdown {{
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}}
|
||||
.stats-right {{
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
}}
|
||||
.radar-chart {{
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
}}
|
||||
.timeline-container {{
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
min-height: auto;
|
||||
}}
|
||||
.timeline-column {{
|
||||
flex: 1 !important;
|
||||
max-height: 300px;
|
||||
}}
|
||||
#attacker-map {{
|
||||
height: 350px !important;
|
||||
}}
|
||||
.leaflet-popup-content {{
|
||||
min-width: 200px !important;
|
||||
}}
|
||||
.ip-marker {{
|
||||
font-size: 8px;
|
||||
}}
|
||||
.ip-detail-content {{
|
||||
padding: 20px;
|
||||
max-width: 95%;
|
||||
max-height: 85vh;
|
||||
}}
|
||||
.download-btn {{
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}}
|
||||
}}
|
||||
|
||||
/* Mobile Optimization - Small phones (480px and down) */
|
||||
@media (max-width: 480px) {{
|
||||
body {{
|
||||
padding: 8px;
|
||||
}}
|
||||
h1 {{
|
||||
font-size: 20px;
|
||||
margin-bottom: 15px;
|
||||
}}
|
||||
.stats-grid {{
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
}}
|
||||
.stat-value {{
|
||||
font-size: 24px;
|
||||
}}
|
||||
.stat-card {{
|
||||
padding: 12px;
|
||||
}}
|
||||
.stat-label {{
|
||||
font-size: 12px;
|
||||
}}
|
||||
.table-container {{
|
||||
padding: 10px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
table {{
|
||||
font-size: 12px;
|
||||
}}
|
||||
th, td {{
|
||||
padding: 8px 4px;
|
||||
}}
|
||||
th {{
|
||||
position: relative;
|
||||
}}
|
||||
th.sortable::after {{
|
||||
right: 4px;
|
||||
font-size: 10px;
|
||||
}}
|
||||
h2 {{
|
||||
font-size: 16px;
|
||||
margin-bottom: 12px;
|
||||
}}
|
||||
.tabs-container {{
|
||||
gap: 0;
|
||||
}}
|
||||
.tab-button {{
|
||||
padding: 10px 12px;
|
||||
font-size: 11px;
|
||||
flex: 1;
|
||||
}}
|
||||
.ip-row {{
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
background: #1c2128;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
.ip-row td {{
|
||||
display: block;
|
||||
padding: 4px 0;
|
||||
border: none;
|
||||
}}
|
||||
.ip-row td::before {{
|
||||
content: attr(data-label);
|
||||
font-weight: bold;
|
||||
color: #8b949e;
|
||||
margin-right: 8px;
|
||||
}}
|
||||
.ip-clickable {{
|
||||
display: inline-block;
|
||||
}}
|
||||
.ip-stats-dropdown {{
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
}}
|
||||
.stats-left {{
|
||||
flex: 1;
|
||||
}}
|
||||
.stats-right {{
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
}}
|
||||
.radar-chart {{
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
}}
|
||||
.radar-legend {{
|
||||
margin-top: 8px;
|
||||
font-size: 10px;
|
||||
}}
|
||||
.stat-row {{
|
||||
padding: 4px 0;
|
||||
}}
|
||||
.stat-label-sm {{
|
||||
font-size: 12px;
|
||||
}}
|
||||
.stat-value-sm {{
|
||||
font-size: 13px;
|
||||
}}
|
||||
.category-badge {{
|
||||
padding: 3px 6px;
|
||||
font-size: 10px;
|
||||
}}
|
||||
.timeline-container {{
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-height: auto;
|
||||
}}
|
||||
.timeline-column {{
|
||||
flex: 1 !important;
|
||||
max-height: 250px;
|
||||
font-size: 11px;
|
||||
}}
|
||||
.timeline-header {{
|
||||
font-size: 12px;
|
||||
margin-bottom: 8px;
|
||||
}}
|
||||
.timeline-item {{
|
||||
padding-bottom: 10px;
|
||||
font-size: 11px;
|
||||
}}
|
||||
.timeline-marker {{
|
||||
left: -19px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}}
|
||||
.reputation-badge {{
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
margin-right: 0;
|
||||
font-size: 10px;
|
||||
}}
|
||||
#attacker-map {{
|
||||
height: 300px !important;
|
||||
}}
|
||||
.leaflet-popup-content {{
|
||||
min-width: 150px !important;
|
||||
}}
|
||||
.ip-marker {{
|
||||
font-size: 7px;
|
||||
}}
|
||||
.ip-detail-modal {{
|
||||
justify-content: flex-end;
|
||||
align-items: flex-end;
|
||||
}}
|
||||
.ip-detail-content {{
|
||||
padding: 15px;
|
||||
max-width: 100%;
|
||||
max-height: 90vh;
|
||||
border-radius: 8px 8px 0 0;
|
||||
width: 100%;
|
||||
}}
|
||||
.download-btn {{
|
||||
padding: 6px 10px;
|
||||
font-size: 11px;
|
||||
}}
|
||||
.github-logo {{
|
||||
font-size: 12px;
|
||||
}}
|
||||
.github-logo svg {{
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}}
|
||||
}}
|
||||
|
||||
/* Landscape mode optimization */
|
||||
@media (max-height: 600px) and (orientation: landscape) {{
|
||||
body {{
|
||||
padding: 8px;
|
||||
}}
|
||||
h1 {{
|
||||
margin-bottom: 10px;
|
||||
font-size: 18px;
|
||||
}}
|
||||
.stats-grid {{
|
||||
margin-bottom: 10px;
|
||||
gap: 8px;
|
||||
}}
|
||||
.stat-value {{
|
||||
font-size: 20px;
|
||||
}}
|
||||
.stat-card {{
|
||||
padding: 8px;
|
||||
}}
|
||||
#attacker-map {{
|
||||
height: 250px !important;
|
||||
}}
|
||||
.ip-stats-dropdown {{
|
||||
gap: 10px;
|
||||
}}
|
||||
.radar-chart {{
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}}
|
||||
}}
|
||||
|
||||
/* Touch-friendly optimizations */
|
||||
@media (hover: none) and (pointer: coarse) {{
|
||||
.ip-clickable {{
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: rgba(88, 166, 255, 0.2);
|
||||
}}
|
||||
.tab-button {{
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: rgba(88, 166, 255, 0.2);
|
||||
padding: 14px 18px;
|
||||
}}
|
||||
.download-btn {{
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: rgba(36, 134, 54, 0.3);
|
||||
}}
|
||||
input[type="checkbox"] {{
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}}
|
||||
}}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -1191,6 +1515,20 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
||||
html += '</div>';
|
||||
}}
|
||||
|
||||
if (stats.country) {{
|
||||
html += '<div class="stat-row">';
|
||||
html += '<span class="stat-label-sm">Country:</span>';
|
||||
html += `<span class="stat-value-sm">${{stats.country}}</span>`;
|
||||
html += '</div>';
|
||||
}}
|
||||
|
||||
if (stats.reverse) {{
|
||||
html += '<div class="stat-row">';
|
||||
html += '<span class="stat-label-sm">Reverse DNS:</span>';
|
||||
html += `<span class="stat-value-sm">${{stats.reverse}}</span>`;
|
||||
html += '</div>';
|
||||
}}
|
||||
|
||||
if (stats.asn_org) {{
|
||||
html += '<div class="stat-row">';
|
||||
html += '<span class="stat-label-sm">ASN Org:</span>';
|
||||
@@ -1198,6 +1536,19 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
||||
html += '</div>';
|
||||
}}
|
||||
|
||||
if (stats.is_proxy !== undefined || stats.is_hosting !== undefined) {{
|
||||
const flags = [];
|
||||
if (stats.is_proxy) flags.push('Proxy');
|
||||
if (stats.is_hosting) flags.push('Hosting');
|
||||
|
||||
if (flags.length > 0) {{
|
||||
html += '<div class="stat-row">';
|
||||
html += '<span class="stat-label-sm">Flags: <span title="Proxy: IP is using a proxy service. Hosting: IP is from a hosting/cloud provider" style="cursor: help; color: #58a6ff; font-weight: bold;">ⓘ</span></span>';
|
||||
html += `<span class="stat-value-sm">${{flags.join(', ')}}</span>`;
|
||||
html += '</div>';
|
||||
}}
|
||||
}}
|
||||
|
||||
if (stats.reputation_score !== null && stats.reputation_score !== undefined) {{
|
||||
html += '<div class="stat-row">';
|
||||
html += '<span class="stat-label-sm">Reputation Score:</span>';
|
||||
@@ -1235,8 +1586,6 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
||||
if (change.old_category) {{
|
||||
html += `<span class="category-badge ${{oldClass}}">${{change.old_category}}</span>`;
|
||||
html += '<span style="color: #8b949e; margin: 0 4px;">→</span>';
|
||||
}} else {{
|
||||
html += '<span style="color: #8b949e;">Initial:</span>';
|
||||
}}
|
||||
|
||||
html += `<span class="category-badge ${{newClass}}">${{change.new_category}}</span>`;
|
||||
@@ -1252,16 +1601,35 @@ def generate_dashboard(stats: dict, dashboard_path: str = "") -> str:
|
||||
html += '<div class="timeline-column">';
|
||||
|
||||
if (stats.list_on && Object.keys(stats.list_on).length > 0) {{
|
||||
html += '<div class="timeline-header">Listed On</div>';
|
||||
const sortedSources = Object.entries(stats.list_on).sort((a, b) => a[0].localeCompare(b[0]));
|
||||
|
||||
sortedSources.forEach(([source, url]) => {{
|
||||
if (url && url !== 'N/A') {{
|
||||
html += `<a href="${{url}}" target="_blank" rel="noopener noreferrer" class="reputation-badge" title="${{source}}">${{source}}</a>`;
|
||||
}} else {{
|
||||
html += `<span class="reputation-badge">${{source}}</span>`;
|
||||
}}
|
||||
}});
|
||||
// Filter out is_hosting and is_proxy from the displayed list
|
||||
const filteredList = Object.entries(stats.list_on).filter(([source, data]) =>
|
||||
source !== 'is_hosting' && source !== 'is_proxy'
|
||||
);
|
||||
|
||||
if (filteredList.length > 0) {{
|
||||
html += '<div class="timeline-header">Listed On</div>';
|
||||
const sortedSources = filteredList.sort((a, b) => a[0].localeCompare(b[0]));
|
||||
|
||||
sortedSources.forEach(([source, data]) => {{
|
||||
// Handle both string URLs and nested object data
|
||||
if (typeof data === 'string' && data !== 'N/A') {{
|
||||
html += `<a href="${{data}}" target="_blank" rel="noopener noreferrer" class="reputation-badge" title="${{source}}">${{source}}</a>`;
|
||||
}} else if (typeof data === 'object' && data !== null) {{
|
||||
// For nested blocklist data, extract source_link if available
|
||||
const sourceLink = data['__source_link'] || data.source_link;
|
||||
if (sourceLink) {{
|
||||
html += `<a href="${{sourceLink}}" target="_blank" rel="noopener noreferrer" class="reputation-badge" title="${{source}}">${{source}}</a>`;
|
||||
}} else {{
|
||||
html += `<span class="reputation-badge">${{source}}</span>`;
|
||||
}}
|
||||
}} else {{
|
||||
html += `<span class="reputation-badge">${{source}}</span>`;
|
||||
}}
|
||||
}});
|
||||
}} else if (stats.country_code || stats.asn) {{
|
||||
html += '<div class="timeline-header">Reputation</div>';
|
||||
html += '<span class="reputation-clean" title="Not found on public blacklists">✓ Clean</span>';
|
||||
}}
|
||||
}} else if (stats.country_code || stats.asn) {{
|
||||
html += '<div class="timeline-header">Reputation</div>';
|
||||
html += '<span class="reputation-clean" title="Not found on public blacklists">✓ Clean</span>';
|
||||
|
||||
Reference in New Issue
Block a user