From d18d4f40f4f6e3573d35e214a240b720f0e28ee9 Mon Sep 17 00:00:00 2001 From: Tour Date: Sun, 7 Dec 2025 11:41:22 +0100 Subject: [PATCH] Features Former-commit-id: ca19649b6a901b458908a741667f9712b23fd393 --- .../java/auctiora/AuctionMonitorResource.java | 107 ++++++++++++++++-- .../java/auctiora/ObjectDetectionService.java | 23 +++- 2 files changed, 115 insertions(+), 15 deletions(-) diff --git a/src/main/java/auctiora/AuctionMonitorResource.java b/src/main/java/auctiora/AuctionMonitorResource.java index 7c2fce3..088528c 100644 --- a/src/main/java/auctiora/AuctionMonitorResource.java +++ b/src/main/java/auctiora/AuctionMonitorResource.java @@ -59,11 +59,14 @@ public class AuctionMonitorResource { status.put("lots", db.getAllLots().size()); status.put("images", db.getImageCount()); - // Count closing soon + // Count closing soon (within 30 minutes, excluding already-closed) var closingSoon = 0; for (var lot : db.getAllLots()) { - if (lot.closingTime() != null && lot.minutesUntilClose() < 30) { - closingSoon++; + if (lot.closingTime() != null) { + long minutes = lot.minutesUntilClose(); + if (minutes > 0 && minutes < 30) { + closingSoon++; + } } } status.put("closingSoon", closingSoon); @@ -99,22 +102,68 @@ public class AuctionMonitorResource { var activeLots = 0; var lotsWithBids = 0; double totalBids = 0; - + var hotLots = 0; + var sleeperLots = 0; + var bargainLots = 0; + var lotsClosing1h = 0; + var lotsClosing6h = 0; + double totalBidVelocity = 0; + int velocityCount = 0; + for (var lot : lots) { - if (lot.closingTime() != null && lot.minutesUntilClose() > 0) { + long minutesLeft = lot.closingTime() != null ? lot.minutesUntilClose() : Long.MAX_VALUE; + + if (lot.closingTime() != null && minutesLeft > 0) { activeLots++; + + // Time-based counts + if (minutesLeft < 60) lotsClosing1h++; + if (minutesLeft < 360) lotsClosing6h++; } + if (lot.currentBid() > 0) { lotsWithBids++; totalBids += lot.currentBid(); } + + // Intelligence metrics (require GraphQL enrichment) + if (lot.followersCount() != null && lot.followersCount() > 20) { + hotLots++; + } + if (lot.isSleeperLot()) { + sleeperLots++; + } + if (lot.isBelowEstimate()) { + bargainLots++; + } + + // Bid velocity + if (lot.bidVelocity() != null && lot.bidVelocity() > 0) { + totalBidVelocity += lot.bidVelocity(); + velocityCount++; + } } - + + // Calculate bids per hour (average velocity across all lots with velocity data) + double bidsPerHour = velocityCount > 0 ? totalBidVelocity / velocityCount : 0; + stats.put("activeLots", activeLots); stats.put("lotsWithBids", lotsWithBids); stats.put("totalBidValue", String.format("€%.2f", totalBids)); stats.put("averageBid", lotsWithBids > 0 ? String.format("€%.2f", totalBids / lotsWithBids) : "€0.00"); - + + // Bidding intelligence + stats.put("bidsPerHour", String.format("%.1f", bidsPerHour)); + stats.put("hotLots", hotLots); + stats.put("sleeperLots", sleeperLots); + stats.put("bargainLots", bargainLots); + stats.put("lotsClosing1h", lotsClosing1h); + stats.put("lotsClosing6h", lotsClosing6h); + + // Conversion rate + double conversionRate = activeLots > 0 ? (lotsWithBids * 100.0 / activeLots) : 0; + stats.put("conversionRate", String.format("%.1f%%", conversionRate)); + return Response.ok(stats).build(); } catch (Exception e) { @@ -319,9 +368,14 @@ public class AuctionMonitorResource { try { var allLots = db.getActiveLots(); var closingSoon = allLots.stream() - .filter(lot -> lot.closingTime() != null && lot.minutesUntilClose() < minutes) + .filter(lot -> lot.closingTime() != null) + .filter(lot -> { + long minutesLeft = lot.minutesUntilClose(); + return minutesLeft > 0 && minutesLeft < minutes; + }) + .sorted((a, b) -> Long.compare(a.minutesUntilClose(), b.minutesUntilClose())) .toList(); - + return Response.ok(closingSoon).build(); } catch (Exception e) { LOG.error("Failed to get closing lots", e); @@ -469,21 +523,50 @@ public class AuctionMonitorResource { /** * GET /api/monitor/charts/category-distribution - * Returns dynamic category distribution for charts + * Returns dynamic category distribution with intelligence for charts */ @GET @Path("/charts/category-distribution") public Response getCategoryDistribution() { try { var lots = db.getAllLots(); + + // Category distribution Map distribution = lots.stream() .filter(l -> l.category() != null && !l.category().isEmpty()) .collect(Collectors.groupingBy( l -> l.category().length() > 20 ? l.category().substring(0, 20) + "..." : l.category(), Collectors.counting() )); - - return Response.ok(distribution).build(); + + // Find top category by count + var topCategory = distribution.entrySet().stream() + .max(Map.Entry.comparingByValue()) + .map(Map.Entry::getKey) + .orElse("N/A"); + + // Calculate average bids per category + Map avgBidsByCategory = lots.stream() + .filter(l -> l.category() != null && !l.category().isEmpty() && l.currentBid() > 0) + .collect(Collectors.groupingBy( + l -> l.category().length() > 20 ? l.category().substring(0, 20) + "..." : l.category(), + Collectors.averagingDouble(Lot::currentBid) + )); + + double overallAvgBid = lots.stream() + .filter(l -> l.currentBid() > 0) + .mapToDouble(Lot::currentBid) + .average() + .orElse(0.0); + + Map response = new HashMap<>(); + response.put("distribution", distribution); + response.put("topCategory", topCategory); + response.put("categoryCount", distribution.size()); + response.put("averageBidOverall", String.format("€%.2f", overallAvgBid)); + response.put("avgBidsByCategory", avgBidsByCategory); + + return Response.ok(response).build(); } catch (Exception e) { LOG.error("Failed to get category distribution", e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) diff --git a/src/main/java/auctiora/ObjectDetectionService.java b/src/main/java/auctiora/ObjectDetectionService.java index 8d0d111..093a0ce 100644 --- a/src/main/java/auctiora/ObjectDetectionService.java +++ b/src/main/java/auctiora/ObjectDetectionService.java @@ -62,12 +62,29 @@ public class ObjectDetectionService { try { // Load network this.net = Dnn.readNetFromDarknet(cfgPath, weightsPath); - this.net.setPreferableBackend(DNN_BACKEND_OPENCV); - this.net.setPreferableTarget(DNN_TARGET_CPU); + + // Try to use GPU/CUDA if available, fallback to CPU + try { + this.net.setPreferableBackend(Dnn.DNN_BACKEND_CUDA); + this.net.setPreferableTarget(Dnn.DNN_TARGET_CUDA); + log.info("✓ Object detection enabled with YOLO (CUDA/GPU acceleration)"); + } catch (Exception e) { + // CUDA not available, try Vulkan for AMD GPUs + try { + this.net.setPreferableBackend(Dnn.DNN_BACKEND_VKCOM); + this.net.setPreferableTarget(Dnn.DNN_TARGET_VULKAN); + log.info("✓ Object detection enabled with YOLO (Vulkan/GPU acceleration)"); + } catch (Exception e2) { + // GPU not available, fallback to CPU + this.net.setPreferableBackend(DNN_BACKEND_OPENCV); + this.net.setPreferableTarget(DNN_TARGET_CPU); + log.info("✓ Object detection enabled with YOLO (CPU only)"); + } + } + // Load class names (one per line) this.classNames = Files.readAllLines(classNamesFile); this.enabled = true; - log.info("✓ Object detection enabled with YOLO"); } catch (UnsatisfiedLinkError e) { System.err.println("⚠️ Object detection disabled: OpenCV native libraries not loaded"); throw new IOException("Failed to initialize object detection: OpenCV native libraries not loaded", e);