feat: add IP insight feature with detailed view and actions
- Updated various tables to include "Actions" column with inspect buttons for IP insights. - Created a new IP insight template for displaying detailed information about an IP address. - Implemented JavaScript functions to handle opening the IP insight view and loading data via HTMX. - Enhanced map markers to include inspect buttons for quick access to IP insights. - Added styles for the new IP insight page and buttons to maintain UI consistency.
This commit is contained in:
@@ -474,6 +474,15 @@ tbody {
|
||||
color: #58a6ff;
|
||||
border-bottom-color: #58a6ff;
|
||||
}
|
||||
.tab-button.disabled {
|
||||
color: #484f58;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.tab-button.disabled:hover {
|
||||
color: #484f58;
|
||||
background: transparent;
|
||||
}
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
@@ -1253,3 +1262,340 @@ tbody {
|
||||
[x-cloak] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Single IP Page Styles
|
||||
======================================== */
|
||||
|
||||
.ip-page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.ip-page-header h1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ip-address-title {
|
||||
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
||||
font-size: 32px;
|
||||
color: #58a6ff;
|
||||
}
|
||||
|
||||
.ip-location-subtitle {
|
||||
color: #8b949e;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ip-page-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ip-page-left,
|
||||
.ip-page-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.ip-info-card h2 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ip-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.ip-info-section {
|
||||
background: #0d1117;
|
||||
border: 1px solid #21262d;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.ip-info-section h3 {
|
||||
color: #58a6ff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
margin: 0 0 12px 0;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #21262d;
|
||||
}
|
||||
|
||||
.ip-flag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background: #f0883e1a;
|
||||
color: #f0883e;
|
||||
border: 1px solid #f0883e4d;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.reputation-score {
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.reputation-score.bad {
|
||||
background: #f851491a;
|
||||
color: #f85149;
|
||||
}
|
||||
|
||||
.reputation-score.medium {
|
||||
background: #f0883e1a;
|
||||
color: #f0883e;
|
||||
}
|
||||
|
||||
.reputation-score.good {
|
||||
background: #3fb9501a;
|
||||
color: #3fb950;
|
||||
}
|
||||
|
||||
.radar-chart-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
/* Single IP page: radar chart with legend on the right */
|
||||
.ip-page-right .radar-chart-container {
|
||||
padding: 10px 0;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* Target the wrapper div injected by generateRadarChart inside radar chart containers */
|
||||
.ip-page-right #ip-radar-chart > div,
|
||||
.ip-page-right #insight-radar-chart > div {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
align-items: center !important;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.ip-page-right #ip-radar-chart > div > svg,
|
||||
.ip-page-right #insight-radar-chart > div > svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ip-page-right #ip-radar-chart .radar-legend,
|
||||
.ip-page-right #insight-radar-chart .radar-legend {
|
||||
margin-top: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Single IP page: limit timeline height to match map */
|
||||
.ip-page-right .timeline {
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
/* Dark theme scrollbar for timeline */
|
||||
.ip-page-right .timeline::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.ip-page-right .timeline::-webkit-scrollbar-track {
|
||||
background: #21262d;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.ip-page-right .timeline::-webkit-scrollbar-thumb {
|
||||
background: #484f58;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.ip-page-right .timeline::-webkit-scrollbar-thumb:hover {
|
||||
background: #6e7681;
|
||||
}
|
||||
|
||||
.single-ip-marker {
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* Mobile responsiveness for IP page */
|
||||
@media (max-width: 1024px) {
|
||||
.ip-page-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.ip-page-right {
|
||||
order: -1;
|
||||
}
|
||||
|
||||
.ip-info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
/* On mobile, stack legend below chart again */
|
||||
.ip-page-right #ip-radar-chart > div,
|
||||
.ip-page-right #insight-radar-chart > div {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.ip-page-right #ip-radar-chart .radar-legend,
|
||||
.ip-page-right #insight-radar-chart .radar-legend {
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* On mobile, remove timeline height limit */
|
||||
.ip-page-right .timeline {
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ip-address-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.ip-page-header h1 {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ip-info-section {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.ip-info-section h3 {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.ip-address-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.ip-location-subtitle {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
IP Lookup Panel
|
||||
======================================== */
|
||||
|
||||
.ip-lookup-panel {
|
||||
background: #161b22;
|
||||
border: 1px solid #30363d;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ip-lookup-form {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.ip-lookup-input {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
background: #0d1117;
|
||||
border: 1px solid #30363d;
|
||||
border-radius: 6px;
|
||||
color: #c9d1d9;
|
||||
font-size: 14px;
|
||||
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
||||
outline: none;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.ip-lookup-input:focus {
|
||||
border-color: #58a6ff;
|
||||
box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.15);
|
||||
}
|
||||
|
||||
.ip-lookup-input::placeholder {
|
||||
color: #6e7681;
|
||||
}
|
||||
|
||||
.ip-lookup-btn {
|
||||
padding: 12px 24px;
|
||||
background: #238636;
|
||||
color: #ffffff;
|
||||
border: 1px solid #2ea043;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ip-lookup-btn:hover {
|
||||
background: #2ea043;
|
||||
}
|
||||
|
||||
.ip-lookup-btn:active {
|
||||
background: #1f7a2f;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.ip-lookup-form {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ip-lookup-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Inspect Button
|
||||
======================================== */
|
||||
|
||||
.inspect-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
background: #21262d;
|
||||
color: #58a6ff;
|
||||
border: 1px solid #30363d;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.inspect-btn:hover {
|
||||
background: #30363d;
|
||||
border-color: #58a6ff;
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
.inspect-btn svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
@@ -17,19 +17,26 @@ document.addEventListener('alpine:init', () => {
|
||||
// Chart state
|
||||
chartLoaded: false,
|
||||
|
||||
// IP Insight state
|
||||
insightIp: null,
|
||||
|
||||
init() {
|
||||
// Handle hash-based tab routing
|
||||
const hash = window.location.hash.slice(1);
|
||||
if (hash === 'ip-stats' || hash === 'attacks') {
|
||||
this.switchToAttacks();
|
||||
}
|
||||
// ip-insight tab is only accessible via lens buttons, not direct hash navigation
|
||||
|
||||
window.addEventListener('hashchange', () => {
|
||||
const h = window.location.hash.slice(1);
|
||||
if (h === 'ip-stats' || h === 'attacks') {
|
||||
this.switchToAttacks();
|
||||
} else {
|
||||
this.switchToOverview();
|
||||
} else if (h !== 'ip-insight') {
|
||||
// Don't switch away from ip-insight via hash if already there
|
||||
if (this.tab !== 'ip-insight') {
|
||||
this.switchToOverview();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -60,6 +67,31 @@ document.addEventListener('alpine:init', () => {
|
||||
window.location.hash = '#overview';
|
||||
},
|
||||
|
||||
switchToIpInsight() {
|
||||
// Only allow switching if an IP is selected
|
||||
if (!this.insightIp) return;
|
||||
this.tab = 'ip-insight';
|
||||
window.location.hash = '#ip-insight';
|
||||
},
|
||||
|
||||
openIpInsight(ip) {
|
||||
// Set the IP and load the insight content
|
||||
this.insightIp = ip;
|
||||
this.tab = 'ip-insight';
|
||||
window.location.hash = '#ip-insight';
|
||||
|
||||
// Load IP insight content via HTMX
|
||||
this.$nextTick(() => {
|
||||
const container = document.getElementById('ip-insight-htmx-container');
|
||||
if (container && typeof htmx !== 'undefined') {
|
||||
htmx.ajax('GET', `${this.dashboardPath}/htmx/ip-insight/${encodeURIComponent(ip)}`, {
|
||||
target: '#ip-insight-htmx-container',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async viewRawRequest(logId) {
|
||||
try {
|
||||
const resp = await fetch(
|
||||
@@ -110,6 +142,19 @@ document.addEventListener('alpine:init', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
// Global function for opening IP Insight (used by map popups)
|
||||
window.openIpInsight = function(ip) {
|
||||
// Find the Alpine component and call openIpInsight
|
||||
const container = document.querySelector('[x-data="dashboardApp()"]');
|
||||
if (container) {
|
||||
// Try Alpine 3.x API first, then fall back to older API
|
||||
const data = Alpine.$data ? Alpine.$data(container) : (container._x_dataStack && container._x_dataStack[0]);
|
||||
if (data && typeof data.openIpInsight === 'function') {
|
||||
data.openIpInsight(ip);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Utility function for formatting timestamps (used by map popups)
|
||||
function formatTimestamp(isoTimestamp) {
|
||||
if (!isoTimestamp) return 'N/A';
|
||||
|
||||
@@ -340,6 +340,15 @@ function buildMapMarkers(ips) {
|
||||
`;
|
||||
}
|
||||
|
||||
// Add inspect button
|
||||
popupContent += `
|
||||
<div style="margin-top: 12px; border-top: 1px solid #30363d; padding-top: 12px; text-align: center;">
|
||||
<button onclick="window.openIpInsight('${ip.ip}')" class="inspect-btn" style="display: inline-flex; align-items: center; gap: 4px; padding: 6px 12px; background: #21262d; color: #58a6ff; border: 1px solid #30363d; border-radius: 4px; font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.2s;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
popupContent += '</div>';
|
||||
marker.setPopupContent(popupContent);
|
||||
} catch (err) {
|
||||
@@ -363,6 +372,11 @@ function buildMapMarkers(ips) {
|
||||
<div style="margin-top: 12px; border-top: 1px solid #30363d; padding-top: 12px; text-align: center; color: #f85149; font-size: 11px;">
|
||||
Failed to load chart: ${err.message}
|
||||
</div>
|
||||
<div style="margin-top: 12px; text-align: center;">
|
||||
<button onclick="window.openIpInsight('${ip.ip}')" class="inspect-btn" style="display: inline-flex; align-items: center; gap: 4px; padding: 6px 12px; background: #21262d; color: #58a6ff; border: 1px solid #30363d; border-radius: 4px; font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.2s;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
marker.setPopupContent(errorPopup);
|
||||
|
||||
Reference in New Issue
Block a user