Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,10 @@ public ResponseEntity<List<StatsService.LocationStatsResponse>> getLocationStats
List<StatsService.LocationStatsResponse> locationStats = statsService.getLocationStats();
return ResponseEntity.ok(locationStats);
}

@GetMapping("/api/query-summary")
@ResponseBody
public ResponseEntity<StatsService.SummaryStatsResponse> getQuerySummary() {
return ResponseEntity.ok(statsService.getSummaryStats());
}
}
48 changes: 48 additions & 0 deletions src/main/java/com/dedicatedcode/paikka/service/StatsService.java
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,49 @@ SELECT date_only, hour_bucket, COUNT(*) as query_count,
return results;
}

public SummaryStatsResponse getSummaryStats() {
LocalDate today = LocalDate.now();
String sql = """
SELECT COUNT(*) AS total_queries,
SUM(CASE WHEN date_only = ? THEN 1 ELSE 0 END) AS queries_today,
SUM(CASE WHEN date_only >= ? THEN 1 ELSE 0 END) AS queries_last_7_days,
SUM(CASE WHEN date_only >= ? THEN 1 ELSE 0 END) AS queries_last_30_days,
COUNT(DISTINCT CASE WHEN date_only >= ? THEN date_only END) AS active_days_last_30,
AVG(response_time_ms) AS avg_response_time,
SUM(CASE WHEN status_code >= 200 AND status_code < 300 THEN 1 ELSE 0 END) AS success_count,
SUM(CASE WHEN status_code >= 400 THEN 1 ELSE 0 END) AS error_count
FROM query_stats
""";

try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
pstmt.setString(1, today.toString());
pstmt.setString(2, today.minusDays(6).toString());
pstmt.setString(3, today.minusDays(29).toString());
pstmt.setString(4, today.minusDays(29).toString());

try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
long queriesLast30Days = rs.getLong("queries_last_30_days");
long activeDays = rs.getLong("active_days_last_30");
double avgQueriesPerDay = activeDays > 0 ? (double) queriesLast30Days / activeDays : 0.0;
return new SummaryStatsResponse(
rs.getLong("total_queries"),
rs.getLong("queries_today"),
rs.getLong("queries_last_7_days"),
avgQueriesPerDay,
rs.getLong("success_count"),
rs.getLong("error_count"),
rs.getDouble("avg_response_time")
);
}
}
} catch (SQLException e) {
logger.error("Failed to retrieve summary stats", e);
}

return new SummaryStatsResponse(0L, 0L, 0L, 0.0, 0L, 0L, 0.0);
}

public List<String> getAvailableEndpoints() {
String sql = "SELECT DISTINCT endpoint FROM query_stats ORDER BY endpoint";
List<String> endpoints = new ArrayList<>();
Expand Down Expand Up @@ -422,6 +465,11 @@ void clearDatabase() {
public record LocationStatsResponse(double lat, double lon, int queryCount, String lastQueried) {
}

public record SummaryStatsResponse(long totalQueries, long queriesToday, long queriesLast7Days,
double avgQueriesPerDay, long successCount, long errorCount,
double avgResponseTimeMs) {
}

private record StatsRecord(String endpoint, String parametersJson, long responseTimeMs, int resultCount,
LocalDate dateOnly, int hourBucket, int statusCode) {
}
Expand Down
56 changes: 41 additions & 15 deletions src/main/resources/static/css/dark-theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ body {
}

.container {
position: relative;
max-width: 1400px;
margin: 0 auto;
background: var(--bg-card);
Expand Down Expand Up @@ -154,39 +155,53 @@ body {
}

.map-info {
padding: 30px;
background: var(--bg-secondary);
border-top: 1px solid var(--border-color);
position: absolute;
left: 15px;
right: 15px;
bottom: 32px;
padding: 8px 16px;
background: rgba(37, 37, 37, 0.88);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: 0 4px 15px var(--shadow);
z-index: 900;
}

.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 25px;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 4px 24px;
}

.info-item {
text-align: center;
padding: 20px;
background: var(--bg-tertiary);
border-radius: 8px;
border: 1px solid var(--border-color);
padding: 0;
background: transparent;
border: none;
min-width: 72px;
}

.info-value {
font-size: 2rem;
font-size: 1.05rem;
font-weight: bold;
color: var(--accent-primary);
display: block;
margin-bottom: 8px;
margin-bottom: 0;
line-height: 1.2;
white-space: nowrap;
}

.info-label {
font-size: 0.9rem;
font-size: 0.6rem;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 500;
white-space: nowrap;
}

.legend {
Expand Down Expand Up @@ -271,8 +286,19 @@ body {
}

.info-grid {
grid-template-columns: repeat(2, 1fr);
gap: 15px;
gap: 4px 14px;
justify-content: flex-start;
}

.info-item {
min-width: 58px;
}

.map-info {
left: 10px;
right: 10px;
bottom: 40px;
padding: 6px 10px;
}

.legend {
Expand Down
67 changes: 49 additions & 18 deletions src/main/resources/templates/map.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
<title>Paikka Query Locations</title>
<link rel="stylesheet" href="/css/dark-theme.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link rel="stylesheet" th:href="@{/css/main.css}" />
</head>
<body>
<div class="container">
Expand Down Expand Up @@ -34,15 +33,39 @@ <h4>Query Activity</h4>
<div class="info-grid">
<div class="info-item">
<span class="info-value" id="total-locations">-</span>
<div class="info-label">Unique Locations</div>
<div class="info-label">Locations</div>
</div>
<div class="info-item">
<span class="info-value" id="total-queries">-</span>
<div class="info-label">Total Queries</div>
<div class="info-label">Total Requests</div>
</div>
<div class="info-item">
<span class="info-value" id="queries-today">-</span>
<div class="info-label">Today</div>
</div>
<div class="info-item">
<span class="info-value" id="queries-last-7-days">-</span>
<div class="info-label">Last 7 Days</div>
</div>
<div class="info-item">
<span class="info-value" id="avg-per-day">-</span>
<div class="info-label">Avg / Day (30d)</div>
</div>
<div class="info-item">
<span class="info-value" id="success-rate">-</span>
<div class="info-label">Success Rate</div>
</div>
<div class="info-item">
<span class="info-value" id="error-count">-</span>
<div class="info-label">Errors</div>
</div>
<div class="info-item">
<span class="info-value" id="avg-response-time">-</span>
<div class="info-label">Avg Response</div>
</div>
<div class="info-item">
<span class="info-value" id="avg-queries">-</span>
<div class="info-label">Avg Queries/Location</div>
<div class="info-label">Avg / Location</div>
</div>
<div class="info-item">
<span class="info-value" id="last-updated">-</span>
Expand All @@ -58,18 +81,10 @@ <h4>Query Activity</h4>
const map = L.map('map').setView([50.0, 10.0], 4);

// Add dark tile layer as base
const darkLayer = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '© OpenStreetMap contributors, © CARTO',
subdomains: 'abcd',
maxZoom: 19
}).addTo(map);

// Add light tile layer for uncovering effect
const lightLayer = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
attribution: '© OpenStreetMap contributors, © CARTO',
subdomains: 'abcd',
const darkLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Dark_Gray_Base/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles © Esri — Source: Esri, HERE, Garmin, © OpenStreetMap contributors',
maxZoom: 19,
opacity: 0
maxNativeZoom: 16
}).addTo(map);

// Function to get square opacity based on query count (for uncovering effect)
Expand Down Expand Up @@ -155,14 +170,13 @@ <h4>Query Activity</h4>

// Update info panel
const totalLocations = data.length;
const totalQueries = data.reduce((sum, loc) => sum + loc.queryCount, 0);
const avgQueries = totalLocations > 0 ? (totalQueries / totalLocations).toFixed(1) : 0;
const locationQueries = data.reduce((sum, loc) => sum + loc.queryCount, 0);
const avgQueries = totalLocations > 0 ? (locationQueries / totalLocations).toFixed(1) : 0;
const lastUpdated = data.length > 0 ?
new Date(Math.max(...data.map(d => new Date(d.lastQueried)))).toLocaleDateString() :
'No data';

document.getElementById('total-locations').textContent = totalLocations;
document.getElementById('total-queries').textContent = totalQueries;
document.getElementById('avg-queries').textContent = avgQueries;
document.getElementById('last-updated').textContent = lastUpdated;

Expand All @@ -178,6 +192,23 @@ <h4>Query Activity</h4>
console.error('Error loading location data:', error);
document.querySelector('.loading').textContent = 'Error loading location data';
});

// Load global query summary stats
fetch('/admin/api/query-summary')
.then(response => response.json())
.then(summary => {
document.getElementById('total-queries').textContent = summary.totalQueries;
document.getElementById('queries-today').textContent = summary.queriesToday;
document.getElementById('queries-last-7-days').textContent = summary.queriesLast7Days;
document.getElementById('avg-per-day').textContent = summary.avgQueriesPerDay.toFixed(1);
const total = summary.successCount + summary.errorCount;
document.getElementById('success-rate').textContent =
total > 0 ? ((summary.successCount / total) * 100).toFixed(1) + '%' : '-';
document.getElementById('error-count').textContent = summary.errorCount;
document.getElementById('avg-response-time').textContent =
Math.round(summary.avgResponseTimeMs) + ' ms';
})
.catch(error => console.error('Error loading query summary:', error));
</script>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,27 @@ void testStatsAggregationWithEndpointFilter() {
assertThat(reverseStats).isNotEmpty();
assertThat(reverseStats.get(0).getQueryCount()).isEqualTo(1);
}

@Test
void testGetSummaryStats() {
statsService.recordQuery("/api/v1/search", Map.of("q", "test1"), 100L, 5, 200);
statsService.recordQuery("/api/v1/search", Map.of("q", "test2"), 200L, 5, 404);

await().atMost(15, TimeUnit.SECONDS)
.pollInterval(1, TimeUnit.SECONDS)
.until(() -> {
statsService.flushPendingStats();
return statsService.getSummaryStats().totalQueries() >= 2;
});

StatsService.SummaryStatsResponse summary = statsService.getSummaryStats();

assertThat(summary.totalQueries()).isEqualTo(2);
assertThat(summary.queriesToday()).isEqualTo(2);
assertThat(summary.queriesLast7Days()).isEqualTo(2);
assertThat(summary.avgQueriesPerDay()).isEqualTo(2.0);
assertThat(summary.successCount()).isEqualTo(1);
assertThat(summary.errorCount()).isEqualTo(1);
assertThat(summary.avgResponseTimeMs()).isBetween(100.0, 200.0);
}
}
Loading