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:
Lorenzo Venerandi
2026-02-28 17:43:50 +01:00
parent ce713d8072
commit d9ae55c0aa
16 changed files with 1070 additions and 23 deletions

View File

@@ -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;
}

View File

@@ -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';

View File

@@ -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);