Features
This commit is contained in:
@@ -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<String, Long> 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<String, Double> 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<String, Object> 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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user