Fix mock tests
This commit is contained in:
@@ -7,13 +7,13 @@ import java.time.LocalDateTime;
|
|||||||
* Data typically populated by the external scraper process
|
* Data typically populated by the external scraper process
|
||||||
*/
|
*/
|
||||||
public record AuctionInfo(
|
public record AuctionInfo(
|
||||||
int auctionId, // Unique auction ID (from URL)
|
long auctionId, // Unique auction ID (from URL)
|
||||||
String title, // Auction title
|
String title, // Auction title
|
||||||
String location, // Location (e.g., "Amsterdam, NL")
|
String location, // Location (e.g., "Amsterdam, NL")
|
||||||
String city, // City name
|
String city, // City name
|
||||||
String country, // Country code (e.g., "NL")
|
String country, // Country code (e.g., "NL")
|
||||||
String url, // Full auction URL
|
String url, // Full auction URL
|
||||||
String typePrefix, // Auction type (A1 or A7)
|
String typePrefix, // Auction type (A1 or A7)
|
||||||
int lotCount, // Number of lots/kavels
|
int lotCount, // Number of lots/kavels
|
||||||
LocalDateTime firstLotClosingTime // Closing time if available
|
LocalDateTime firstLotClosingTime // Closing time if available
|
||||||
) { }
|
) { }
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public class DatabaseService {
|
|||||||
// Auctions table (populated by external scraper)
|
// Auctions table (populated by external scraper)
|
||||||
stmt.execute("""
|
stmt.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS auctions (
|
CREATE TABLE IF NOT EXISTS auctions (
|
||||||
auction_id INTEGER PRIMARY KEY,
|
auction_id BIGINT PRIMARY KEY,
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
location TEXT,
|
location TEXT,
|
||||||
city TEXT,
|
city TEXT,
|
||||||
@@ -51,8 +51,8 @@ public class DatabaseService {
|
|||||||
// Lots table (populated by external scraper)
|
// Lots table (populated by external scraper)
|
||||||
stmt.execute("""
|
stmt.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS lots (
|
CREATE TABLE IF NOT EXISTS lots (
|
||||||
lot_id INTEGER PRIMARY KEY,
|
lot_id BIGINT PRIMARY KEY,
|
||||||
sale_id INTEGER,
|
sale_id BIGINT,
|
||||||
title TEXT,
|
title TEXT,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
manufacturer TEXT,
|
manufacturer TEXT,
|
||||||
@@ -94,35 +94,94 @@ public class DatabaseService {
|
|||||||
* SQLite doesn't support DROP COLUMN, so we add columns with ALTER TABLE ADD COLUMN.
|
* SQLite doesn't support DROP COLUMN, so we add columns with ALTER TABLE ADD COLUMN.
|
||||||
*/
|
*/
|
||||||
private void migrateSchema(java.sql.Statement stmt) throws SQLException {
|
private void migrateSchema(java.sql.Statement stmt) throws SQLException {
|
||||||
// Check if country column exists in auctions table
|
// Check auctions table for missing columns
|
||||||
try (var rs = stmt.executeQuery("PRAGMA table_info(auctions)")) {
|
try (var rs = stmt.executeQuery("PRAGMA table_info(auctions)")) {
|
||||||
boolean hasCountry = false;
|
var hasLocation = false;
|
||||||
|
var hasCity = false;
|
||||||
|
var hasCountry = false;
|
||||||
|
var hasType = false;
|
||||||
|
var hasLotCount = false;
|
||||||
|
var hasClosingTime = false;
|
||||||
|
var hasDiscoveredAt = false;
|
||||||
|
|
||||||
while (rs.next()) {
|
while (rs.next()) {
|
||||||
if ("country".equals(rs.getString("name"))) {
|
var colName = rs.getString("name");
|
||||||
hasCountry = true;
|
switch (colName) {
|
||||||
break;
|
case "location" -> hasLocation = true;
|
||||||
|
case "city" -> hasCity = true;
|
||||||
|
case "country" -> hasCountry = true;
|
||||||
|
case "type" -> hasType = true;
|
||||||
|
case "lot_count" -> hasLotCount = true;
|
||||||
|
case "closing_time" -> hasClosingTime = true;
|
||||||
|
case "discovered_at" -> hasDiscoveredAt = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!hasLocation) {
|
||||||
|
log.info("Migrating schema: Adding 'location' column to auctions table");
|
||||||
|
stmt.execute("ALTER TABLE auctions ADD COLUMN location TEXT");
|
||||||
|
}
|
||||||
|
if (!hasCity) {
|
||||||
|
log.info("Migrating schema: Adding 'city' column to auctions table");
|
||||||
|
stmt.execute("ALTER TABLE auctions ADD COLUMN city TEXT");
|
||||||
|
}
|
||||||
if (!hasCountry) {
|
if (!hasCountry) {
|
||||||
log.info("Migrating schema: Adding 'country' column to auctions table");
|
log.info("Migrating schema: Adding 'country' column to auctions table");
|
||||||
stmt.execute("ALTER TABLE auctions ADD COLUMN country TEXT");
|
stmt.execute("ALTER TABLE auctions ADD COLUMN country TEXT");
|
||||||
}
|
}
|
||||||
|
if (!hasType) {
|
||||||
|
log.info("Migrating schema: Adding 'type' column to auctions table");
|
||||||
|
stmt.execute("ALTER TABLE auctions ADD COLUMN type TEXT");
|
||||||
|
}
|
||||||
|
if (!hasLotCount) {
|
||||||
|
log.info("Migrating schema: Adding 'lot_count' column to auctions table");
|
||||||
|
stmt.execute("ALTER TABLE auctions ADD COLUMN lot_count INTEGER DEFAULT 0");
|
||||||
|
}
|
||||||
|
if (!hasClosingTime) {
|
||||||
|
log.info("Migrating schema: Adding 'closing_time' column to auctions table");
|
||||||
|
stmt.execute("ALTER TABLE auctions ADD COLUMN closing_time TEXT");
|
||||||
|
}
|
||||||
|
if (!hasDiscoveredAt) {
|
||||||
|
log.info("Migrating schema: Adding 'discovered_at' column to auctions table");
|
||||||
|
stmt.execute("ALTER TABLE auctions ADD COLUMN discovered_at INTEGER");
|
||||||
|
}
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
// Table might not exist yet, which is fine
|
// Table might not exist yet, which is fine
|
||||||
log.debug("Could not check auctions table schema: " + e.getMessage());
|
log.debug("Could not check auctions table schema: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if sale_id column exists in lots table (old schema used auction_id)
|
// Check lots table for missing columns and migrate
|
||||||
try (var rs = stmt.executeQuery("PRAGMA table_info(lots)")) {
|
try (var rs = stmt.executeQuery("PRAGMA table_info(lots)")) {
|
||||||
boolean hasSaleId = false;
|
var hasSaleId = false;
|
||||||
boolean hasAuctionId = false;
|
var hasAuctionId = false;
|
||||||
|
var hasTitle = false;
|
||||||
|
var hasDescription = false;
|
||||||
|
var hasManufacturer = false;
|
||||||
|
var hasType = false;
|
||||||
|
var hasYear = false;
|
||||||
|
var hasCategory = false;
|
||||||
|
var hasCurrentBid = false;
|
||||||
|
var hasCurrency = false;
|
||||||
|
var hasUrl = false;
|
||||||
|
var hasClosingTime = false;
|
||||||
|
var hasClosingNotified = false;
|
||||||
|
|
||||||
while (rs.next()) {
|
while (rs.next()) {
|
||||||
String colName = rs.getString("name");
|
var colName = rs.getString("name");
|
||||||
if ("sale_id".equals(colName)) {
|
switch (colName) {
|
||||||
hasSaleId = true;
|
case "sale_id" -> hasSaleId = true;
|
||||||
}
|
case "auction_id" -> hasAuctionId = true;
|
||||||
if ("auction_id".equals(colName)) {
|
case "title" -> hasTitle = true;
|
||||||
hasAuctionId = true;
|
case "description" -> hasDescription = true;
|
||||||
|
case "manufacturer" -> hasManufacturer = true;
|
||||||
|
case "type" -> hasType = true;
|
||||||
|
case "year" -> hasYear = true;
|
||||||
|
case "category" -> hasCategory = true;
|
||||||
|
case "current_bid" -> hasCurrentBid = true;
|
||||||
|
case "currency" -> hasCurrency = true;
|
||||||
|
case "url" -> hasUrl = true;
|
||||||
|
case "closing_time" -> hasClosingTime = true;
|
||||||
|
case "closing_notified" -> hasClosingNotified = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +196,52 @@ public class DatabaseService {
|
|||||||
log.info("Migrating schema: Adding 'sale_id' column to new lots table");
|
log.info("Migrating schema: Adding 'sale_id' column to new lots table");
|
||||||
stmt.execute("ALTER TABLE lots ADD COLUMN sale_id INTEGER");
|
stmt.execute("ALTER TABLE lots ADD COLUMN sale_id INTEGER");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add missing columns for lot details
|
||||||
|
if (!hasTitle) {
|
||||||
|
log.info("Migrating schema: Adding 'title' column to lots table");
|
||||||
|
stmt.execute("ALTER TABLE lots ADD COLUMN title TEXT");
|
||||||
|
}
|
||||||
|
if (!hasDescription) {
|
||||||
|
log.info("Migrating schema: Adding 'description' column to lots table");
|
||||||
|
stmt.execute("ALTER TABLE lots ADD COLUMN description TEXT");
|
||||||
|
}
|
||||||
|
if (!hasManufacturer) {
|
||||||
|
log.info("Migrating schema: Adding 'manufacturer' column to lots table");
|
||||||
|
stmt.execute("ALTER TABLE lots ADD COLUMN manufacturer TEXT");
|
||||||
|
}
|
||||||
|
if (!hasType) {
|
||||||
|
log.info("Migrating schema: Adding 'type' column to lots table");
|
||||||
|
stmt.execute("ALTER TABLE lots ADD COLUMN type TEXT");
|
||||||
|
}
|
||||||
|
if (!hasYear) {
|
||||||
|
log.info("Migrating schema: Adding 'year' column to lots table");
|
||||||
|
stmt.execute("ALTER TABLE lots ADD COLUMN year INTEGER");
|
||||||
|
}
|
||||||
|
if (!hasCategory) {
|
||||||
|
log.info("Migrating schema: Adding 'category' column to lots table");
|
||||||
|
stmt.execute("ALTER TABLE lots ADD COLUMN category TEXT");
|
||||||
|
}
|
||||||
|
if (!hasCurrentBid) {
|
||||||
|
log.info("Migrating schema: Adding 'current_bid' column to lots table");
|
||||||
|
stmt.execute("ALTER TABLE lots ADD COLUMN current_bid REAL");
|
||||||
|
}
|
||||||
|
if (!hasCurrency) {
|
||||||
|
log.info("Migrating schema: Adding 'currency' column to lots table");
|
||||||
|
stmt.execute("ALTER TABLE lots ADD COLUMN currency TEXT DEFAULT 'EUR'");
|
||||||
|
}
|
||||||
|
if (!hasUrl) {
|
||||||
|
log.info("Migrating schema: Adding 'url' column to lots table");
|
||||||
|
stmt.execute("ALTER TABLE lots ADD COLUMN url TEXT");
|
||||||
|
}
|
||||||
|
if (!hasClosingTime) {
|
||||||
|
log.info("Migrating schema: Adding 'closing_time' column to lots table");
|
||||||
|
stmt.execute("ALTER TABLE lots ADD COLUMN closing_time TEXT");
|
||||||
|
}
|
||||||
|
if (!hasClosingNotified) {
|
||||||
|
log.info("Migrating schema: Adding 'closing_notified' column to lots table");
|
||||||
|
stmt.execute("ALTER TABLE lots ADD COLUMN closing_notified INTEGER DEFAULT 0");
|
||||||
|
}
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
// Table might not exist yet, which is fine
|
// Table might not exist yet, which is fine
|
||||||
log.debug("Could not check lots table schema: " + e.getMessage());
|
log.debug("Could not check lots table schema: " + e.getMessage());
|
||||||
@@ -162,7 +267,7 @@ public class DatabaseService {
|
|||||||
""";
|
""";
|
||||||
|
|
||||||
try (var conn = DriverManager.getConnection(url); var ps = conn.prepareStatement(sql)) {
|
try (var conn = DriverManager.getConnection(url); var ps = conn.prepareStatement(sql)) {
|
||||||
ps.setInt(1, auction.auctionId());
|
ps.setLong(1, auction.auctionId());
|
||||||
ps.setString(2, auction.title());
|
ps.setString(2, auction.title());
|
||||||
ps.setString(3, auction.location());
|
ps.setString(3, auction.location());
|
||||||
ps.setString(4, auction.city());
|
ps.setString(4, auction.city());
|
||||||
@@ -182,15 +287,22 @@ public class DatabaseService {
|
|||||||
synchronized List<AuctionInfo> getAllAuctions() throws SQLException {
|
synchronized List<AuctionInfo> getAllAuctions() throws SQLException {
|
||||||
List<AuctionInfo> auctions = new ArrayList<>();
|
List<AuctionInfo> auctions = new ArrayList<>();
|
||||||
var sql = "SELECT auction_id, title, location, city, country, url, type, lot_count, closing_time FROM auctions";
|
var sql = "SELECT auction_id, title, location, city, country, url, type, lot_count, closing_time FROM auctions";
|
||||||
|
|
||||||
try (var conn = DriverManager.getConnection(url); var stmt = conn.createStatement()) {
|
try (var conn = DriverManager.getConnection(url); var stmt = conn.createStatement()) {
|
||||||
var rs = stmt.executeQuery(sql);
|
var rs = stmt.executeQuery(sql);
|
||||||
while (rs.next()) {
|
while (rs.next()) {
|
||||||
var closingStr = rs.getString("closing_time");
|
var closingStr = rs.getString("closing_time");
|
||||||
var closing = closingStr != null ? LocalDateTime.parse(closingStr) : null;
|
LocalDateTime closing = null;
|
||||||
|
if (closingStr != null && !closingStr.isBlank()) {
|
||||||
|
try {
|
||||||
|
closing = LocalDateTime.parse(closingStr);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Invalid closing_time format for auction {}: {}", rs.getLong("auction_id"), closingStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
auctions.add(new AuctionInfo(
|
auctions.add(new AuctionInfo(
|
||||||
rs.getInt("auction_id"),
|
rs.getLong("auction_id"),
|
||||||
rs.getString("title"),
|
rs.getString("title"),
|
||||||
rs.getString("location"),
|
rs.getString("location"),
|
||||||
rs.getString("city"),
|
rs.getString("city"),
|
||||||
@@ -212,16 +324,23 @@ public class DatabaseService {
|
|||||||
List<AuctionInfo> auctions = new ArrayList<>();
|
List<AuctionInfo> auctions = new ArrayList<>();
|
||||||
var sql = "SELECT auction_id, title, location, city, country, url, type, lot_count, closing_time "
|
var sql = "SELECT auction_id, title, location, city, country, url, type, lot_count, closing_time "
|
||||||
+ "FROM auctions WHERE country = ?";
|
+ "FROM auctions WHERE country = ?";
|
||||||
|
|
||||||
try (var conn = DriverManager.getConnection(url); var ps = conn.prepareStatement(sql)) {
|
try (var conn = DriverManager.getConnection(url); var ps = conn.prepareStatement(sql)) {
|
||||||
ps.setString(1, countryCode);
|
ps.setString(1, countryCode);
|
||||||
var rs = ps.executeQuery();
|
var rs = ps.executeQuery();
|
||||||
while (rs.next()) {
|
while (rs.next()) {
|
||||||
var closingStr = rs.getString("closing_time");
|
var closingStr = rs.getString("closing_time");
|
||||||
var closing = closingStr != null ? LocalDateTime.parse(closingStr) : null;
|
LocalDateTime closing = null;
|
||||||
|
if (closingStr != null && !closingStr.isBlank()) {
|
||||||
|
try {
|
||||||
|
closing = LocalDateTime.parse(closingStr);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Invalid closing_time format for auction {}: {}", rs.getLong("auction_id"), closingStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
auctions.add(new AuctionInfo(
|
auctions.add(new AuctionInfo(
|
||||||
rs.getInt("auction_id"),
|
rs.getLong("auction_id"),
|
||||||
rs.getString("title"),
|
rs.getString("title"),
|
||||||
rs.getString("location"),
|
rs.getString("location"),
|
||||||
rs.getString("city"),
|
rs.getString("city"),
|
||||||
@@ -258,8 +377,8 @@ public class DatabaseService {
|
|||||||
""";
|
""";
|
||||||
|
|
||||||
try (var conn = DriverManager.getConnection(url); var ps = conn.prepareStatement(sql)) {
|
try (var conn = DriverManager.getConnection(url); var ps = conn.prepareStatement(sql)) {
|
||||||
ps.setInt(1, lot.lotId());
|
ps.setLong(1, lot.lotId());
|
||||||
ps.setInt(2, lot.saleId());
|
ps.setLong(2, lot.saleId());
|
||||||
ps.setString(3, lot.title());
|
ps.setString(3, lot.title());
|
||||||
ps.setString(4, lot.description());
|
ps.setString(4, lot.description());
|
||||||
ps.setString(5, lot.manufacturer());
|
ps.setString(5, lot.manufacturer());
|
||||||
@@ -278,10 +397,10 @@ public class DatabaseService {
|
|||||||
/**
|
/**
|
||||||
* Inserts a new image record with object detection labels
|
* Inserts a new image record with object detection labels
|
||||||
*/
|
*/
|
||||||
synchronized void insertImage(int lotId, String url, String filePath, List<String> labels) throws SQLException {
|
synchronized void insertImage(long lotId, String url, String filePath, List<String> labels) throws SQLException {
|
||||||
var sql = "INSERT INTO images (lot_id, url, file_path, labels, processed_at) VALUES (?, ?, ?, ?, ?)";
|
var sql = "INSERT INTO images (lot_id, url, file_path, labels, processed_at) VALUES (?, ?, ?, ?, ?)";
|
||||||
try (var conn = DriverManager.getConnection(this.url); var ps = conn.prepareStatement(sql)) {
|
try (var conn = DriverManager.getConnection(this.url); var ps = conn.prepareStatement(sql)) {
|
||||||
ps.setInt(1, lotId);
|
ps.setLong(1, lotId);
|
||||||
ps.setString(2, url);
|
ps.setString(2, url);
|
||||||
ps.setString(3, filePath);
|
ps.setString(3, filePath);
|
||||||
ps.setString(4, String.join(",", labels));
|
ps.setString(4, String.join(",", labels));
|
||||||
@@ -293,17 +412,17 @@ public class DatabaseService {
|
|||||||
/**
|
/**
|
||||||
* Retrieves images for a specific lot
|
* Retrieves images for a specific lot
|
||||||
*/
|
*/
|
||||||
synchronized List<ImageRecord> getImagesForLot(int lotId) throws SQLException {
|
synchronized List<ImageRecord> getImagesForLot(long lotId) throws SQLException {
|
||||||
List<ImageRecord> images = new ArrayList<>();
|
List<ImageRecord> images = new ArrayList<>();
|
||||||
var sql = "SELECT id, lot_id, url, file_path, labels FROM images WHERE lot_id = ?";
|
var sql = "SELECT id, lot_id, url, file_path, labels FROM images WHERE lot_id = ?";
|
||||||
|
|
||||||
try (var conn = DriverManager.getConnection(url); var ps = conn.prepareStatement(sql)) {
|
try (var conn = DriverManager.getConnection(url); var ps = conn.prepareStatement(sql)) {
|
||||||
ps.setInt(1, lotId);
|
ps.setLong(1, lotId);
|
||||||
var rs = ps.executeQuery();
|
var rs = ps.executeQuery();
|
||||||
while (rs.next()) {
|
while (rs.next()) {
|
||||||
images.add(new ImageRecord(
|
images.add(new ImageRecord(
|
||||||
rs.getInt("id"),
|
rs.getInt("id"),
|
||||||
rs.getInt("lot_id"),
|
rs.getLong("lot_id"),
|
||||||
rs.getString("url"),
|
rs.getString("url"),
|
||||||
rs.getString("file_path"),
|
rs.getString("file_path"),
|
||||||
rs.getString("labels")
|
rs.getString("labels")
|
||||||
@@ -320,16 +439,23 @@ public class DatabaseService {
|
|||||||
List<Lot> list = new ArrayList<>();
|
List<Lot> list = new ArrayList<>();
|
||||||
var sql = "SELECT lot_id, sale_id, title, description, manufacturer, type, year, category, " +
|
var sql = "SELECT lot_id, sale_id, title, description, manufacturer, type, year, category, " +
|
||||||
"current_bid, currency, url, closing_time, closing_notified FROM lots";
|
"current_bid, currency, url, closing_time, closing_notified FROM lots";
|
||||||
|
|
||||||
try (var conn = DriverManager.getConnection(url); var stmt = conn.createStatement()) {
|
try (var conn = DriverManager.getConnection(url); var stmt = conn.createStatement()) {
|
||||||
var rs = stmt.executeQuery(sql);
|
var rs = stmt.executeQuery(sql);
|
||||||
while (rs.next()) {
|
while (rs.next()) {
|
||||||
var closingStr = rs.getString("closing_time");
|
var closingStr = rs.getString("closing_time");
|
||||||
var closing = closingStr != null ? LocalDateTime.parse(closingStr) : null;
|
LocalDateTime closing = null;
|
||||||
|
if (closingStr != null && !closingStr.isBlank()) {
|
||||||
|
try {
|
||||||
|
closing = LocalDateTime.parse(closingStr);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Invalid closing_time format for lot {}: {}", rs.getLong("lot_id"), closingStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
list.add(new Lot(
|
list.add(new Lot(
|
||||||
rs.getInt("sale_id"),
|
rs.getLong("sale_id"),
|
||||||
rs.getInt("lot_id"),
|
rs.getLong("lot_id"),
|
||||||
rs.getString("title"),
|
rs.getString("title"),
|
||||||
rs.getString("description"),
|
rs.getString("description"),
|
||||||
rs.getString("manufacturer"),
|
rs.getString("manufacturer"),
|
||||||
@@ -375,7 +501,7 @@ public class DatabaseService {
|
|||||||
try (var conn = DriverManager.getConnection(url);
|
try (var conn = DriverManager.getConnection(url);
|
||||||
var ps = conn.prepareStatement("UPDATE lots SET current_bid = ? WHERE lot_id = ?")) {
|
var ps = conn.prepareStatement("UPDATE lots SET current_bid = ? WHERE lot_id = ?")) {
|
||||||
ps.setDouble(1, lot.currentBid());
|
ps.setDouble(1, lot.currentBid());
|
||||||
ps.setInt(2, lot.lotId());
|
ps.setLong(2, lot.lotId());
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -387,7 +513,7 @@ public class DatabaseService {
|
|||||||
try (var conn = DriverManager.getConnection(url);
|
try (var conn = DriverManager.getConnection(url);
|
||||||
var ps = conn.prepareStatement("UPDATE lots SET closing_notified = ? WHERE lot_id = ?")) {
|
var ps = conn.prepareStatement("UPDATE lots SET closing_notified = ? WHERE lot_id = ?")) {
|
||||||
ps.setInt(1, lot.closingNotified() ? 1 : 0);
|
ps.setInt(1, lot.closingNotified() ? 1 : 0);
|
||||||
ps.setInt(2, lot.lotId());
|
ps.setLong(2, lot.lotId());
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -472,11 +598,11 @@ public class DatabaseService {
|
|||||||
try (var conn = DriverManager.getConnection(url); var stmt = conn.createStatement()) {
|
try (var conn = DriverManager.getConnection(url); var stmt = conn.createStatement()) {
|
||||||
var rs = stmt.executeQuery(sql);
|
var rs = stmt.executeQuery(sql);
|
||||||
while (rs.next()) {
|
while (rs.next()) {
|
||||||
String lotIdStr = rs.getString("lot_id");
|
var lotIdStr = rs.getString("lot_id");
|
||||||
String auctionIdStr = rs.getString("auction_id");
|
var auctionIdStr = rs.getString("auction_id");
|
||||||
|
|
||||||
int lotId = ScraperDataAdapter.extractNumericId(lotIdStr);
|
var lotId = ScraperDataAdapter.extractNumericId(lotIdStr);
|
||||||
int saleId = ScraperDataAdapter.extractNumericId(auctionIdStr);
|
var saleId = ScraperDataAdapter.extractNumericId(auctionIdStr);
|
||||||
|
|
||||||
images.add(new ImageImportRecord(
|
images.add(new ImageImportRecord(
|
||||||
lotId,
|
lotId,
|
||||||
@@ -494,10 +620,10 @@ public class DatabaseService {
|
|||||||
/**
|
/**
|
||||||
* Simple record for image data from database
|
* Simple record for image data from database
|
||||||
*/
|
*/
|
||||||
record ImageRecord(int id, int lotId, String url, String filePath, String labels) { }
|
record ImageRecord(int id, long lotId, String url, String filePath, String labels) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record for importing images from scraper format
|
* Record for importing images from scraper format
|
||||||
*/
|
*/
|
||||||
record ImageImportRecord(int lotId, int saleId, String url) { }
|
record ImageImportRecord(long lotId, long saleId, String url) { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class ImageProcessingService {
|
|||||||
* @param lotId lot identifier
|
* @param lotId lot identifier
|
||||||
* @return absolute path to saved file or null on failure
|
* @return absolute path to saved file or null on failure
|
||||||
*/
|
*/
|
||||||
String downloadImage(String imageUrl, int saleId, int lotId) {
|
String downloadImage(String imageUrl, long saleId, long lotId) {
|
||||||
try {
|
try {
|
||||||
var response = httpClient.sendGetBytes(imageUrl);
|
var response = httpClient.sendGetBytes(imageUrl);
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ class ImageProcessingService {
|
|||||||
* @param saleId sale identifier
|
* @param saleId sale identifier
|
||||||
* @param imageUrls list of image URLs to process
|
* @param imageUrls list of image URLs to process
|
||||||
*/
|
*/
|
||||||
void processImagesForLot(int lotId, int saleId, List<String> imageUrls) {
|
void processImagesForLot(long lotId, long saleId, List<String> imageUrls) {
|
||||||
log.info(" Processing {} images for lot {}", imageUrls.size(), lotId);
|
log.info(" Processing {} images for lot {}", imageUrls.size(), lotId);
|
||||||
|
|
||||||
for (var imgUrl : imageUrls) {
|
for (var imgUrl : imageUrls) {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import java.time.LocalDateTime;
|
|||||||
*/
|
*/
|
||||||
@With
|
@With
|
||||||
record Lot(
|
record Lot(
|
||||||
int saleId,
|
long saleId,
|
||||||
int lotId,
|
long lotId,
|
||||||
String title,
|
String title,
|
||||||
String description,
|
String description,
|
||||||
String manufacturer,
|
String manufacturer,
|
||||||
|
|||||||
@@ -71,14 +71,27 @@ public class ScraperDataAdapter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int extractNumericId(String id) {
|
public static long extractNumericId(String id) {
|
||||||
if (id == null || id.isBlank()) return 0;
|
if (id == null || id.isBlank()) return 0L;
|
||||||
// Remove the type prefix (e.g., "A7-") first, then extract all digits
|
// Remove the type prefix (e.g., "A7-") first, then extract all digits
|
||||||
// "A7-39813" → "39813" → 39813
|
// "A7-39813" → "39813" → 39813
|
||||||
// "A1-28505-5" → "28505-5" → "285055"
|
// "A1-28505-5" → "28505-5" → "285055"
|
||||||
var afterPrefix = id.indexOf('-') >= 0 ? id.substring(id.indexOf('-') + 1) : id;
|
var afterPrefix = id.indexOf('-') >= 0 ? id.substring(id.indexOf('-') + 1) : id;
|
||||||
var digits = afterPrefix.replaceAll("\\D+", "");
|
var digits = afterPrefix.replaceAll("\\D+", "");
|
||||||
return digits.isEmpty() ? 0 : Integer.parseInt(digits);
|
if (digits.isEmpty()) return 0L;
|
||||||
|
|
||||||
|
// Check if number is too large for long (> 19 digits or value > Long.MAX_VALUE)
|
||||||
|
if (digits.length() > 19) {
|
||||||
|
log.debug("ID too large for long, skipping: {}", id);
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Long.parseLong(digits);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.debug("Invalid numeric ID, skipping: {}", id);
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String extractTypePrefix(String id) {
|
private static String extractTypePrefix(String id) {
|
||||||
@@ -115,12 +128,21 @@ public class ScraperDataAdapter {
|
|||||||
|
|
||||||
private static LocalDateTime parseTimestamp(String ts) {
|
private static LocalDateTime parseTimestamp(String ts) {
|
||||||
if (ts == null || ts.isBlank()) return null;
|
if (ts == null || ts.isBlank()) return null;
|
||||||
|
|
||||||
|
// Filter out known invalid values
|
||||||
|
String tsLower = ts.toLowerCase().trim();
|
||||||
|
if (tsLower.equals("gap") || tsLower.equals("null") || tsLower.equals("n/a") ||
|
||||||
|
tsLower.equals("unknown") || tsLower.equals("tbd") || tsLower.length() < 8) {
|
||||||
|
log.debug("Skipping invalid timestamp value: {}", ts);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
for (var fmt : TIMESTAMP_FORMATS) {
|
for (var fmt : TIMESTAMP_FORMATS) {
|
||||||
try {
|
try {
|
||||||
return LocalDateTime.parse(ts, fmt);
|
return LocalDateTime.parse(ts, fmt);
|
||||||
} catch (DateTimeParseException ignored) { }
|
} catch (DateTimeParseException ignored) { }
|
||||||
}
|
}
|
||||||
log.info("Unable to parse timestamp: {}", ts);
|
log.debug("Unable to parse timestamp: {}", ts);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ public class WorkflowOrchestrator {
|
|||||||
|
|
||||||
this.notifier = new NotificationService(notificationConfig);
|
this.notifier = new NotificationService(notificationConfig);
|
||||||
this.detector = new ObjectDetectionService(yoloCfg, yoloWeights, yoloClasses);
|
this.detector = new ObjectDetectionService(yoloCfg, yoloWeights, yoloClasses);
|
||||||
RateLimitedHttpClient httpClient = new RateLimitedHttpClient();
|
var httpClient = new RateLimitedHttpClient();
|
||||||
this.imageProcessor = new ImageProcessingService(db, detector, httpClient);
|
this.imageProcessor = new ImageProcessingService(db, detector, httpClient);
|
||||||
|
|
||||||
this.monitor = new TroostwijkMonitor(databasePath, notificationConfig,
|
this.monitor = new TroostwijkMonitor(databasePath, notificationConfig,
|
||||||
@@ -90,7 +90,7 @@ public class WorkflowOrchestrator {
|
|||||||
scheduler.scheduleAtFixedRate(() -> {
|
scheduler.scheduleAtFixedRate(() -> {
|
||||||
try {
|
try {
|
||||||
log.info("📥 [WORKFLOW 1] Importing scraper data...");
|
log.info("📥 [WORKFLOW 1] Importing scraper data...");
|
||||||
long start = System.currentTimeMillis();
|
var start = System.currentTimeMillis();
|
||||||
|
|
||||||
// Import auctions
|
// Import auctions
|
||||||
var auctions = db.importAuctionsFromScraper();
|
var auctions = db.importAuctionsFromScraper();
|
||||||
@@ -104,7 +104,7 @@ public class WorkflowOrchestrator {
|
|||||||
var images = db.getUnprocessedImagesFromScraper();
|
var images = db.getUnprocessedImagesFromScraper();
|
||||||
log.info(" → Found {} unprocessed images", images.size());
|
log.info(" → Found {} unprocessed images", images.size());
|
||||||
|
|
||||||
long duration = System.currentTimeMillis() - start;
|
var duration = System.currentTimeMillis() - start;
|
||||||
log.info(" ✓ Scraper import completed in {}ms\n", duration);
|
log.info(" ✓ Scraper import completed in {}ms\n", duration);
|
||||||
|
|
||||||
// Trigger notification if significant data imported
|
// Trigger notification if significant data imported
|
||||||
@@ -133,7 +133,7 @@ public class WorkflowOrchestrator {
|
|||||||
scheduler.scheduleAtFixedRate(() -> {
|
scheduler.scheduleAtFixedRate(() -> {
|
||||||
try {
|
try {
|
||||||
log.info("🖼️ [WORKFLOW 2] Processing pending images...");
|
log.info("🖼️ [WORKFLOW 2] Processing pending images...");
|
||||||
long start = System.currentTimeMillis();
|
var start = System.currentTimeMillis();
|
||||||
|
|
||||||
// Get unprocessed images
|
// Get unprocessed images
|
||||||
var unprocessedImages = db.getUnprocessedImagesFromScraper();
|
var unprocessedImages = db.getUnprocessedImagesFromScraper();
|
||||||
@@ -145,17 +145,17 @@ public class WorkflowOrchestrator {
|
|||||||
|
|
||||||
log.info(" → Processing {} images", unprocessedImages.size());
|
log.info(" → Processing {} images", unprocessedImages.size());
|
||||||
|
|
||||||
int processed = 0;
|
var processed = 0;
|
||||||
int detected = 0;
|
var detected = 0;
|
||||||
|
|
||||||
for (var imageRecord : unprocessedImages) {
|
for (var imageRecord : unprocessedImages) {
|
||||||
try {
|
try {
|
||||||
// Download image
|
// Download image
|
||||||
String filePath = imageProcessor.downloadImage(
|
var filePath = imageProcessor.downloadImage(
|
||||||
imageRecord.url(),
|
imageRecord.url(),
|
||||||
imageRecord.saleId(),
|
imageRecord.saleId(),
|
||||||
imageRecord.lotId()
|
imageRecord.lotId()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (filePath != null) {
|
if (filePath != null) {
|
||||||
// Run object detection
|
// Run object detection
|
||||||
@@ -190,7 +190,7 @@ public class WorkflowOrchestrator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
long duration = System.currentTimeMillis() - start;
|
var duration = System.currentTimeMillis() - start;
|
||||||
log.info(String.format(" ✓ Processed %d images, detected objects in %d (%.1fs)\n",
|
log.info(String.format(" ✓ Processed %d images, detected objects in %d (%.1fs)\n",
|
||||||
processed, detected, duration / 1000.0));
|
processed, detected, duration / 1000.0));
|
||||||
|
|
||||||
@@ -211,12 +211,12 @@ public class WorkflowOrchestrator {
|
|||||||
scheduler.scheduleAtFixedRate(() -> {
|
scheduler.scheduleAtFixedRate(() -> {
|
||||||
try {
|
try {
|
||||||
log.info("💰 [WORKFLOW 3] Monitoring bids...");
|
log.info("💰 [WORKFLOW 3] Monitoring bids...");
|
||||||
long start = System.currentTimeMillis();
|
var start = System.currentTimeMillis();
|
||||||
|
|
||||||
var activeLots = db.getActiveLots();
|
var activeLots = db.getActiveLots();
|
||||||
log.info(" → Checking {} active lots", activeLots.size());
|
log.info(" → Checking {} active lots", activeLots.size());
|
||||||
|
|
||||||
int bidChanges = 0;
|
var bidChanges = 0;
|
||||||
|
|
||||||
for (var lot : activeLots) {
|
for (var lot : activeLots) {
|
||||||
// Note: In production, this would call Troostwijk API
|
// Note: In production, this would call Troostwijk API
|
||||||
@@ -224,7 +224,7 @@ public class WorkflowOrchestrator {
|
|||||||
// The external scraper updates bids, we just notify
|
// The external scraper updates bids, we just notify
|
||||||
}
|
}
|
||||||
|
|
||||||
long duration = System.currentTimeMillis() - start;
|
var duration = System.currentTimeMillis() - start;
|
||||||
log.info(String.format(" ✓ Bid monitoring completed in %dms\n", duration));
|
log.info(String.format(" ✓ Bid monitoring completed in %dms\n", duration));
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -244,20 +244,20 @@ public class WorkflowOrchestrator {
|
|||||||
scheduler.scheduleAtFixedRate(() -> {
|
scheduler.scheduleAtFixedRate(() -> {
|
||||||
try {
|
try {
|
||||||
log.info("⏰ [WORKFLOW 4] Checking closing times...");
|
log.info("⏰ [WORKFLOW 4] Checking closing times...");
|
||||||
long start = System.currentTimeMillis();
|
var start = System.currentTimeMillis();
|
||||||
|
|
||||||
var activeLots = db.getActiveLots();
|
var activeLots = db.getActiveLots();
|
||||||
int alertsSent = 0;
|
var alertsSent = 0;
|
||||||
|
|
||||||
for (var lot : activeLots) {
|
for (var lot : activeLots) {
|
||||||
if (lot.closingTime() == null) continue;
|
if (lot.closingTime() == null) continue;
|
||||||
|
|
||||||
long minutesLeft = lot.minutesUntilClose();
|
var minutesLeft = lot.minutesUntilClose();
|
||||||
|
|
||||||
// Alert for lots closing in 5 minutes
|
// Alert for lots closing in 5 minutes
|
||||||
if (minutesLeft <= 5 && minutesLeft > 0 && !lot.closingNotified()) {
|
if (minutesLeft <= 5 && minutesLeft > 0 && !lot.closingNotified()) {
|
||||||
String message = String.format("Kavel %d sluit binnen %d min.",
|
var message = String.format("Kavel %d sluit binnen %d min.",
|
||||||
lot.lotId(), minutesLeft);
|
lot.lotId(), minutesLeft);
|
||||||
|
|
||||||
notifier.sendNotification(message, "Lot Closing Soon", 1);
|
notifier.sendNotification(message, "Lot Closing Soon", 1);
|
||||||
|
|
||||||
@@ -274,7 +274,7 @@ public class WorkflowOrchestrator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
long duration = System.currentTimeMillis() - start;
|
var duration = System.currentTimeMillis() - start;
|
||||||
log.info(String.format(" → Sent %d closing alerts in %dms\n",
|
log.info(String.format(" → Sent %d closing alerts in %dms\n",
|
||||||
alertsSent, duration));
|
alertsSent, duration));
|
||||||
|
|
||||||
@@ -312,7 +312,7 @@ public class WorkflowOrchestrator {
|
|||||||
|
|
||||||
// Step 4: Check closing times
|
// Step 4: Check closing times
|
||||||
log.info("[4/4] Checking closing times...");
|
log.info("[4/4] Checking closing times...");
|
||||||
int closingSoon = 0;
|
var closingSoon = 0;
|
||||||
for (var lot : activeLots) {
|
for (var lot : activeLots) {
|
||||||
if (lot.closingTime() != null && lot.minutesUntilClose() < 30) {
|
if (lot.closingTime() != null && lot.minutesUntilClose() < 30) {
|
||||||
closingSoon++;
|
closingSoon++;
|
||||||
@@ -399,15 +399,15 @@ public class WorkflowOrchestrator {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
var auctions = db.getAllAuctions();
|
var auctions = db.getAllAuctions();
|
||||||
var lots = db.getAllLots();
|
var lots = db.getAllLots();
|
||||||
int images = db.getImageCount();
|
var images = db.getImageCount();
|
||||||
|
|
||||||
log.info(" Auctions: {}", auctions.size());
|
log.info(" Auctions: {}", auctions.size());
|
||||||
log.info(" Lots: {}", lots.size());
|
log.info(" Lots: {}", lots.size());
|
||||||
log.info(" Images: {}", images);
|
log.info(" Images: {}", images);
|
||||||
|
|
||||||
// Count closing soon
|
// Count closing soon
|
||||||
int closingSoon = 0;
|
var closingSoon = 0;
|
||||||
for (var lot : lots) {
|
for (var lot : lots) {
|
||||||
if (lot.closingTime() != null && lot.minutesUntilClose() < 30) {
|
if (lot.closingTime() != null && lot.minutesUntilClose() < 30) {
|
||||||
closingSoon++;
|
closingSoon++;
|
||||||
|
|||||||
224
src/main/resources/META-INF/resources/index.html
Normal file
224
src/main/resources/META-INF/resources/index.html
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Scrape-UI 1 - Enterprise</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<style>
|
||||||
|
.gradient-bg {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hover {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hover:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-50 min-h-screen">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="gradient-bg text-white py-8">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<h1 class="text-4xl font-bold mb-2">Scrape-UI Enterprise</h1>
|
||||||
|
<p class="text-xl opacity-90">Powered by Quarkus + Modern Frontend</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="container mx-auto px-4 py-8">
|
||||||
|
<!-- API Status Card -->
|
||||||
|
<!-- API & Build Status Card -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 mb-8 card-hover">
|
||||||
|
<h2 class="text-2xl font-bold mb-4 text-gray-800">Build & Runtime Status</h2>
|
||||||
|
<div id="api-status" class="space-y-4">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<!-- Build Information -->
|
||||||
|
<div class="bg-blue-50 p-4 rounded-lg">
|
||||||
|
<h3 class="font-semibold text-blue-800 mb-2">📦 Maven Build</h3>
|
||||||
|
<div class="space-y-2 text-sm">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600">Group:</span>
|
||||||
|
<span class="font-mono font-medium" id="build-group">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600">Artifact:</span>
|
||||||
|
<span class="font-mono font-medium" id="build-artifact">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600">Version:</span>
|
||||||
|
<span class="font-mono font-medium px-2 py-1 bg-blue-100 rounded" id="build-version">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Runtime Information -->
|
||||||
|
<div class="bg-green-50 p-4 rounded-lg">
|
||||||
|
<h3 class="font-semibold text-green-800 mb-2">🚀 Runtime</h3>
|
||||||
|
<div class="space-y-2 text-sm">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600">Status:</span>
|
||||||
|
<span class="px-2 py-1 rounded-full text-xs font-semibold bg-green-100 text-green-800" id="runtime-status">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600">Java:</span>
|
||||||
|
<span class="font-mono" id="java-version">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600">Platform:</span>
|
||||||
|
<span class="font-mono" id="runtime-os">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Timestamp & Additional Info -->
|
||||||
|
<div class="pt-4 border-t">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-gray-500">Last Updated</p>
|
||||||
|
<p class="font-medium" id="last-updated">-</p>
|
||||||
|
</div>
|
||||||
|
<button onclick="fetchStatus()" class="bg-gray-100 hover:bg-gray-200 text-gray-700 px-4 py-2 rounded-lg text-sm transition-colors">
|
||||||
|
🔄 Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- API Response Card -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 card-hover">
|
||||||
|
<h2 class="text-2xl font-bold mb-4 text-gray-800">API Test</h2>
|
||||||
|
<button id="test-api" class="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition-colors mb-4">
|
||||||
|
Test Greeting API
|
||||||
|
</button>
|
||||||
|
<div id="api-response" class="bg-gray-100 p-4 rounded-lg">
|
||||||
|
<pre class="text-sm text-gray-700">Click the button to test the API</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Features Grid -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mt-8">
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 card-hover">
|
||||||
|
<h3 class="text-xl font-semibold mb-2 text-gray-800">⚡ Quarkus Backend</h3>
|
||||||
|
<p class="text-gray-600">Fast startup, low memory footprint, optimized for containers</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 card-hover">
|
||||||
|
<h3 class="text-xl font-semibold mb-2 text-gray-800">🚀 REST API</h3>
|
||||||
|
<p class="text-gray-600">RESTful endpoints with JSON responses</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 card-hover">
|
||||||
|
<h3 class="text-xl font-semibold mb-2 text-gray-800">🎨 Modern UI</h3>
|
||||||
|
<p class="text-gray-600">Responsive design with Tailwind CSS</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Fetch API status on load
|
||||||
|
async function fetchStatus() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/status')
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${ response.status }: ${ response.statusText }`)
|
||||||
|
}
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
// Update Build Information
|
||||||
|
document.getElementById('build-group').textContent = data.groupId || 'N/A'
|
||||||
|
document.getElementById('build-artifact').textContent = data.artifactId || data.name || 'N/A'
|
||||||
|
document.getElementById('build-version').textContent = data.version || 'N/A'
|
||||||
|
|
||||||
|
// Update Runtime Information
|
||||||
|
document.getElementById('runtime-status').textContent = data.status || 'unknown'
|
||||||
|
document.getElementById('java-version').textContent = data.javaVersion || System.getProperty?.('java.version') || 'N/A'
|
||||||
|
document.getElementById('runtime-os').textContent = data.os || 'N/A'
|
||||||
|
|
||||||
|
// Update Timestamp
|
||||||
|
const timestamp = data.timestamp ? new Date(data.timestamp).toLocaleString() : 'N/A'
|
||||||
|
document.getElementById('last-updated').textContent = timestamp
|
||||||
|
|
||||||
|
// Update status badge color based on status
|
||||||
|
const statusBadge = document.getElementById('runtime-status')
|
||||||
|
if (data.status?.toLowerCase() === 'running') {
|
||||||
|
statusBadge.className = 'px-2 py-1 rounded-full text-xs font-semibold bg-green-100 text-green-800'
|
||||||
|
} else {
|
||||||
|
statusBadge.className = 'px-2 py-1 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800'
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching status:', error)
|
||||||
|
document.getElementById('api-status').innerHTML = `
|
||||||
|
<div class="bg-red-50 border-l-4 border-red-500 p-4">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<p class="text-sm text-red-700">Failed to load status: ${ error.message }</p>
|
||||||
|
<button onclick="fetchStatus()" class="mt-2 text-sm text-red-700 hover:text-red-600 font-medium">
|
||||||
|
Retry →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch API status on load
|
||||||
|
async function fetchStatus3() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/status')
|
||||||
|
const data = await response.json()
|
||||||
|
document.getElementById('api-status').innerHTML = `
|
||||||
|
<p><strong>Application:</strong> ${ data.application }</p>
|
||||||
|
<p><strong>Status:</strong> <span class="text-green-600 font-semibold">${ data.status }</span></p>
|
||||||
|
<p><strong>Version:</strong> ${ data.version }</p>
|
||||||
|
<p><strong>Timestamp:</strong> ${ data.timestamp }</p>
|
||||||
|
`
|
||||||
|
} catch (error) {
|
||||||
|
document.getElementById('api-status').innerHTML = `
|
||||||
|
<p class="text-red-600">Error loading status: ${ error.message }</p>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test greeting API
|
||||||
|
document.getElementById('test-api').addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/hello')
|
||||||
|
const data = await response.json()
|
||||||
|
document.getElementById('api-response').innerHTML = `
|
||||||
|
<pre class="text-sm text-gray-700">${ JSON.stringify(data, null, 2) }</pre>
|
||||||
|
`
|
||||||
|
} catch (error) {
|
||||||
|
document.getElementById('api-response').innerHTML = `
|
||||||
|
<pre class="text-sm text-red-600">Error: ${ error.message }</pre>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Auto-refresh every 30 seconds
|
||||||
|
let refreshInterval = setInterval(fetchStatus, 30000);
|
||||||
|
|
||||||
|
// Stop auto-refresh when page loses focus (optional)
|
||||||
|
document.addEventListener('visibilitychange', function() {
|
||||||
|
if (document.hidden) {
|
||||||
|
clearInterval(refreshInterval);
|
||||||
|
} else {
|
||||||
|
refreshInterval = setInterval(fetchStatus, 30000);
|
||||||
|
fetchStatus(); // Refresh immediately when returning to tab
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Load status on page load
|
||||||
|
fetchStatus()
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
572
src/main/resources/META-INF/resources/praetium.html
Normal file
572
src/main/resources/META-INF/resources/praetium.html
Normal file
@@ -0,0 +1,572 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Auctiora - Monitoring Dashboard</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/plotly.js/3.0.3/plotly.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
.gradient-bg {
|
||||||
|
background: linear-gradient(135deg, #1e3a8a 0%, #3b82f6 50%, #60a5fa 100%);
|
||||||
|
}
|
||||||
|
.card-hover {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.card-hover:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
.status-online {
|
||||||
|
animation: pulse-green 2s infinite;
|
||||||
|
}
|
||||||
|
@keyframes pulse-green {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
.workflow-card {
|
||||||
|
background: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%);
|
||||||
|
border-left: 4px solid #3b82f6;
|
||||||
|
}
|
||||||
|
.alert-badge {
|
||||||
|
animation: pulse-red 1.5s infinite;
|
||||||
|
}
|
||||||
|
@keyframes pulse-red {
|
||||||
|
0%, 100% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.1); }
|
||||||
|
}
|
||||||
|
.metric-card {
|
||||||
|
background: linear-gradient(145deg, #ffffff 0%, #fafbfc 100%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-50 min-h-screen">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="gradient-bg text-white py-6 shadow-lg">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-4xl font-bold mb-2">
|
||||||
|
<i class="fas fa-cube mr-3"></i>
|
||||||
|
Auctiora Monitoring Dashboard
|
||||||
|
</h1>
|
||||||
|
<p class="text-lg opacity-90">Real-time Auction Data Processing & Monitoring</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span class="status-online inline-block w-3 h-3 bg-green-400 rounded-full"></span>
|
||||||
|
<span class="text-sm font-semibold">System Online</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs opacity-75 mt-1" id="last-update">Last updated: --:--</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="container mx-auto px-4 py-8">
|
||||||
|
|
||||||
|
<!-- System Status Cards -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4 mb-8">
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 card-hover metric-card">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-gray-600 text-sm font-semibold uppercase">Auctions</div>
|
||||||
|
<div class="text-3xl font-bold text-blue-600 mt-2" id="total-auctions">--</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 rounded-full bg-blue-100">
|
||||||
|
<i class="fas fa-gavel text-2xl text-blue-600"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 card-hover metric-card">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-gray-600 text-sm font-semibold uppercase">Total Lots</div>
|
||||||
|
<div class="text-3xl font-bold text-green-600 mt-2" id="total-lots">--</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 rounded-full bg-green-100">
|
||||||
|
<i class="fas fa-boxes text-2xl text-green-600"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 card-hover metric-card">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-gray-600 text-sm font-semibold uppercase">Images</div>
|
||||||
|
<div class="text-3xl font-bold text-purple-600 mt-2" id="total-images">--</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 rounded-full bg-purple-100">
|
||||||
|
<i class="fas fa-images text-2xl text-purple-600"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 card-hover metric-card">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-gray-600 text-sm font-semibold uppercase">Active Lots</div>
|
||||||
|
<div class="text-3xl font-bold text-yellow-600 mt-2" id="active-lots">--</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 rounded-full bg-yellow-100">
|
||||||
|
<i class="fas fa-clock text-2xl text-yellow-600"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 card-hover metric-card">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-gray-600 text-sm font-semibold uppercase">Closing Soon</div>
|
||||||
|
<div class="text-3xl font-bold text-red-600 mt-2" id="closing-soon">--</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 rounded-full bg-red-100">
|
||||||
|
<i class="fas fa-bell text-2xl text-red-600"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistics Overview -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 col-span-2">
|
||||||
|
<h3 class="text-xl font-semibold mb-4 text-gray-800">
|
||||||
|
<i class="fas fa-chart-line mr-2 text-blue-600"></i>
|
||||||
|
Bidding Statistics
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-3 gap-4">
|
||||||
|
<div class="text-center p-4 bg-blue-50 rounded-lg">
|
||||||
|
<div class="text-gray-600 text-sm font-semibold">Lots with Bids</div>
|
||||||
|
<div class="text-2xl font-bold text-blue-600 mt-2" id="lots-with-bids">--</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-4 bg-green-50 rounded-lg">
|
||||||
|
<div class="text-gray-600 text-sm font-semibold">Total Bid Value</div>
|
||||||
|
<div class="text-2xl font-bold text-green-600 mt-2" id="total-bid-value">--</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-4 bg-purple-50 rounded-lg">
|
||||||
|
<div class="text-gray-600 text-sm font-semibold">Average Bid</div>
|
||||||
|
<div class="text-2xl font-bold text-purple-600 mt-2" id="average-bid">--</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<h3 class="text-xl font-semibold mb-4 text-gray-800">
|
||||||
|
<i class="fas fa-tachometer-alt mr-2 text-green-600"></i>
|
||||||
|
Rate Limiting
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-gray-600 text-sm">Total Requests</span>
|
||||||
|
<span class="font-bold text-gray-800" id="rate-total-requests">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-gray-600 text-sm">Successful</span>
|
||||||
|
<span class="font-bold text-green-600" id="rate-successful">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-gray-600 text-sm">Failed</span>
|
||||||
|
<span class="font-bold text-red-600" id="rate-failed">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-gray-600 text-sm">Avg Duration</span>
|
||||||
|
<span class="font-bold text-blue-600" id="rate-avg-duration">--</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Workflow Controls -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
|
||||||
|
<h3 class="text-xl font-semibold mb-4 text-gray-800">
|
||||||
|
<i class="fas fa-play-circle mr-2 text-blue-600"></i>
|
||||||
|
Workflow Controls
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<button onclick="triggerWorkflow('scraper-import')"
|
||||||
|
class="workflow-card p-4 rounded-lg hover:shadow-lg transition-all">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<i class="fas fa-download text-blue-600 text-xl"></i>
|
||||||
|
<span class="text-xs text-gray-500" id="status-scraper">Ready</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm font-semibold text-gray-800">Import Scraper Data</div>
|
||||||
|
<div class="text-xs text-gray-600 mt-1">Load auction/lot data from external scraper</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onclick="triggerWorkflow('image-processing')"
|
||||||
|
class="workflow-card p-4 rounded-lg hover:shadow-lg transition-all">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<i class="fas fa-image text-purple-600 text-xl"></i>
|
||||||
|
<span class="text-xs text-gray-500" id="status-images">Ready</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm font-semibold text-gray-800">Process Images</div>
|
||||||
|
<div class="text-xs text-gray-600 mt-1">Download & analyze images with object detection</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onclick="triggerWorkflow('bid-monitoring')"
|
||||||
|
class="workflow-card p-4 rounded-lg hover:shadow-lg transition-all">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<i class="fas fa-chart-line text-green-600 text-xl"></i>
|
||||||
|
<span class="text-xs text-gray-500" id="status-bids">Ready</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm font-semibold text-gray-800">Monitor Bids</div>
|
||||||
|
<div class="text-xs text-gray-600 mt-1">Track bid changes and send notifications</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onclick="triggerWorkflow('closing-alerts')"
|
||||||
|
class="workflow-card p-4 rounded-lg hover:shadow-lg transition-all">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<i class="fas fa-bell text-red-600 text-xl"></i>
|
||||||
|
<span class="text-xs text-gray-500" id="status-alerts">Ready</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm font-semibold text-gray-800">Closing Alerts</div>
|
||||||
|
<div class="text-xs text-gray-600 mt-1">Alert for lots closing within 30 minutes</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Charts Section -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
||||||
|
<!-- Country Distribution -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<h3 class="text-xl font-semibold mb-4 text-gray-800">
|
||||||
|
<i class="fas fa-globe mr-2 text-blue-600"></i>
|
||||||
|
Auctions by Country
|
||||||
|
</h3>
|
||||||
|
<div id="country-chart" style="height: 300px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Category Distribution -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<h3 class="text-xl font-semibold mb-4 text-gray-800">
|
||||||
|
<i class="fas fa-tags mr-2 text-green-600"></i>
|
||||||
|
Lots by Category
|
||||||
|
</h3>
|
||||||
|
<div id="category-chart" style="height: 300px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Closing Soon Table -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h3 class="text-xl font-semibold text-gray-800">
|
||||||
|
<i class="fas fa-exclamation-triangle mr-2 text-red-600"></i>
|
||||||
|
Lots Closing Soon (< 30 min)
|
||||||
|
</h3>
|
||||||
|
<button onclick="fetchClosingSoon()" class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition-colors">
|
||||||
|
<i class="fas fa-sync mr-2"></i>Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Lot ID</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Title</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Current Bid</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Closing Time</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Minutes Left</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="closing-soon-table" class="bg-white divide-y divide-gray-200">
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="px-6 py-4 text-center text-gray-500">Loading...</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Activity Log -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<h3 class="text-xl font-semibold mb-4 text-gray-800">
|
||||||
|
<i class="fas fa-history mr-2 text-purple-600"></i>
|
||||||
|
Activity Log
|
||||||
|
</h3>
|
||||||
|
<div id="activity-log" class="space-y-2 max-h-64 overflow-y-auto">
|
||||||
|
<div class="text-sm text-gray-500">Monitoring system started...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="bg-gray-800 text-white py-6 mt-12">
|
||||||
|
<div class="container mx-auto px-4 text-center">
|
||||||
|
<p class="text-gray-300">
|
||||||
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
|
Auctiora - Auction Data Processing & Monitoring System
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-400 mt-2">
|
||||||
|
Architecture: Quarkus + SQLite + REST API | Auto-refresh: 15 seconds
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Dashboard state
|
||||||
|
let dashboardData = {
|
||||||
|
status: {},
|
||||||
|
statistics: {},
|
||||||
|
rateLimitStats: {},
|
||||||
|
closingSoon: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize dashboard
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
addLog('Dashboard initialized');
|
||||||
|
fetchAllData();
|
||||||
|
|
||||||
|
// Auto-refresh every 15 seconds
|
||||||
|
setInterval(fetchAllData, 15000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch all dashboard data
|
||||||
|
async function fetchAllData() {
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
fetchStatus(),
|
||||||
|
fetchStatistics(),
|
||||||
|
fetchRateLimitStats(),
|
||||||
|
fetchClosingSoon()
|
||||||
|
]);
|
||||||
|
updateLastUpdate();
|
||||||
|
addLog('Dashboard data refreshed');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching dashboard data:', error);
|
||||||
|
addLog('Error: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch system status
|
||||||
|
async function fetchStatus() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/monitor/status');
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch status');
|
||||||
|
|
||||||
|
dashboardData.status = await response.json();
|
||||||
|
|
||||||
|
document.getElementById('total-auctions').textContent = dashboardData.status.auctions || 0;
|
||||||
|
document.getElementById('total-lots').textContent = dashboardData.status.lots || 0;
|
||||||
|
document.getElementById('total-images').textContent = dashboardData.status.images || 0;
|
||||||
|
document.getElementById('closing-soon').textContent = dashboardData.status.closingSoon || 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching status:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch statistics
|
||||||
|
async function fetchStatistics() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/monitor/statistics');
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch statistics');
|
||||||
|
|
||||||
|
dashboardData.statistics = await response.json();
|
||||||
|
|
||||||
|
document.getElementById('active-lots').textContent = dashboardData.statistics.activeLots || 0;
|
||||||
|
document.getElementById('lots-with-bids').textContent = dashboardData.statistics.lotsWithBids || 0;
|
||||||
|
document.getElementById('total-bid-value').textContent = dashboardData.statistics.totalBidValue || '€0.00';
|
||||||
|
document.getElementById('average-bid').textContent = dashboardData.statistics.averageBid || '€0.00';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching statistics:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch rate limit stats
|
||||||
|
async function fetchRateLimitStats() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/monitor/rate-limit/stats');
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch rate limit stats');
|
||||||
|
|
||||||
|
dashboardData.rateLimitStats = await response.json();
|
||||||
|
|
||||||
|
// Aggregate stats from all hosts
|
||||||
|
let totalRequests = 0, successfulRequests = 0, failedRequests = 0, avgDuration = 0;
|
||||||
|
const stats = dashboardData.rateLimitStats.statistics || {};
|
||||||
|
const hostCount = Object.keys(stats).length;
|
||||||
|
|
||||||
|
for (const hostStats of Object.values(stats)) {
|
||||||
|
totalRequests += hostStats.totalRequests || 0;
|
||||||
|
successfulRequests += hostStats.successfulRequests || 0;
|
||||||
|
failedRequests += hostStats.failedRequests || 0;
|
||||||
|
avgDuration += hostStats.averageDurationMs || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('rate-total-requests').textContent = totalRequests;
|
||||||
|
document.getElementById('rate-successful').textContent = successfulRequests;
|
||||||
|
document.getElementById('rate-failed').textContent = failedRequests;
|
||||||
|
document.getElementById('rate-avg-duration').textContent =
|
||||||
|
hostCount > 0 ? (avgDuration / hostCount).toFixed(0) + ' ms' : '--';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching rate limit stats:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch closing soon lots
|
||||||
|
async function fetchClosingSoon() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/monitor/lots/closing-soon?minutes=30');
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch closing soon lots');
|
||||||
|
|
||||||
|
dashboardData.closingSoon = await response.json();
|
||||||
|
updateClosingSoonTable();
|
||||||
|
|
||||||
|
// Update charts (placeholder for now)
|
||||||
|
updateCharts();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching closing soon:', error);
|
||||||
|
document.getElementById('closing-soon-table').innerHTML =
|
||||||
|
'<tr><td colspan="6" class="px-6 py-4 text-center text-red-600">Error loading data</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update closing soon table
|
||||||
|
function updateClosingSoonTable() {
|
||||||
|
const tableBody = document.getElementById('closing-soon-table');
|
||||||
|
|
||||||
|
if (dashboardData.closingSoon.length === 0) {
|
||||||
|
tableBody.innerHTML = '<tr><td colspan="6" class="px-6 py-4 text-center text-gray-500">No lots closing soon</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tableBody.innerHTML = '';
|
||||||
|
dashboardData.closingSoon.forEach(lot => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.className = 'hover:bg-gray-50';
|
||||||
|
|
||||||
|
const minutesLeft = lot.minutesUntilClose || 0;
|
||||||
|
const badgeColor = minutesLeft < 10 ? 'bg-red-100 text-red-800' : 'bg-yellow-100 text-yellow-800';
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${lot.lotId || '--'}</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-gray-900">${lot.title || 'N/A'}</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-bold text-green-600">${lot.currency || ''}${lot.currentBid ? lot.currentBid.toFixed(2) : '0.00'}</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${lot.closingTime || 'N/A'}</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${badgeColor}">
|
||||||
|
${minutesLeft} min
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
|
<a href="${lot.url || '#'}" target="_blank" class="text-blue-600 hover:text-blue-900">
|
||||||
|
<i class="fas fa-external-link-alt"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tableBody.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update charts (placeholder)
|
||||||
|
function updateCharts() {
|
||||||
|
// Country distribution pie chart
|
||||||
|
const countryData = {
|
||||||
|
values: [5, 3, 2],
|
||||||
|
labels: ['NL', 'BE', 'DE'],
|
||||||
|
type: 'pie',
|
||||||
|
marker: { colors: ['#3b82f6', '#10b981', '#f59e0b'] }
|
||||||
|
};
|
||||||
|
|
||||||
|
Plotly.newPlot('country-chart', [countryData], {
|
||||||
|
showlegend: true,
|
||||||
|
margin: { t: 20, b: 20, l: 20, r: 20 }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Category distribution
|
||||||
|
const categoryData = {
|
||||||
|
values: [4, 3, 2, 1],
|
||||||
|
labels: ['Machinery', 'Material Handling', 'Power Generation', 'Furniture'],
|
||||||
|
type: 'pie',
|
||||||
|
marker: { colors: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444'] }
|
||||||
|
};
|
||||||
|
|
||||||
|
Plotly.newPlot('category-chart', [categoryData], {
|
||||||
|
showlegend: true,
|
||||||
|
margin: { t: 20, b: 20, l: 20, r: 20 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger workflow
|
||||||
|
async function triggerWorkflow(workflow) {
|
||||||
|
const statusId = 'status-' + workflow.split('-')[0];
|
||||||
|
const statusEl = document.getElementById(statusId);
|
||||||
|
|
||||||
|
statusEl.textContent = 'Running...';
|
||||||
|
statusEl.className = 'text-xs text-blue-600 font-semibold';
|
||||||
|
|
||||||
|
addLog(`Triggering workflow: ${workflow}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/monitor/trigger/${workflow}`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Workflow trigger failed');
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
addLog(`✓ ${result.message || 'Workflow completed'}`, 'success');
|
||||||
|
statusEl.textContent = 'Complete';
|
||||||
|
statusEl.className = 'text-xs text-green-600 font-semibold';
|
||||||
|
|
||||||
|
// Refresh data after workflow
|
||||||
|
setTimeout(fetchAllData, 2000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error triggering workflow:', error);
|
||||||
|
addLog(`✗ Workflow failed: ${error.message}`, 'error');
|
||||||
|
statusEl.textContent = 'Failed';
|
||||||
|
statusEl.className = 'text-xs text-red-600 font-semibold';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset status after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
statusEl.textContent = 'Ready';
|
||||||
|
statusEl.className = 'text-xs text-gray-500';
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add log entry
|
||||||
|
function addLog(message, type = 'info') {
|
||||||
|
const logContainer = document.getElementById('activity-log');
|
||||||
|
const logEntry = document.createElement('div');
|
||||||
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
|
|
||||||
|
let iconClass = 'fas fa-info-circle text-blue-600';
|
||||||
|
let textClass = 'text-gray-700';
|
||||||
|
|
||||||
|
if (type === 'success') {
|
||||||
|
iconClass = 'fas fa-check-circle text-green-600';
|
||||||
|
textClass = 'text-green-700';
|
||||||
|
} else if (type === 'error') {
|
||||||
|
iconClass = 'fas fa-exclamation-circle text-red-600';
|
||||||
|
textClass = 'text-red-700';
|
||||||
|
}
|
||||||
|
|
||||||
|
logEntry.className = `text-sm flex items-start space-x-2 ${textClass}`;
|
||||||
|
logEntry.innerHTML = `
|
||||||
|
<i class="${iconClass} mt-1"></i>
|
||||||
|
<span class="text-gray-500">${timestamp}</span>
|
||||||
|
<span>${message}</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
logContainer.insertBefore(logEntry, logContainer.firstChild);
|
||||||
|
|
||||||
|
// Keep only last 20 entries
|
||||||
|
while (logContainer.children.length > 20) {
|
||||||
|
logContainer.removeChild(logContainer.lastChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last update timestamp
|
||||||
|
function updateLastUpdate() {
|
||||||
|
const now = new Date();
|
||||||
|
document.getElementById('last-update').textContent =
|
||||||
|
`Last updated: ${now.toLocaleTimeString()}`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -3,13 +3,14 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Troostwijk Auctions - Kavel Data Dashboard</title>
|
<title>Auctiora - Monitoring Dashboard</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/plotly.js/3.0.3/plotly.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/plotly.js/3.0.3/plotly.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
.gradient-bg {
|
.gradient-bg {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #1e3a8a 0%, #3b82f6 50%, #60a5fa 100%);
|
||||||
}
|
}
|
||||||
.card-hover {
|
.card-hover {
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
@@ -18,33 +19,47 @@
|
|||||||
transform: translateY(-5px);
|
transform: translateY(-5px);
|
||||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
}
|
}
|
||||||
.kavel-card {
|
.status-online {
|
||||||
|
animation: pulse-green 2s infinite;
|
||||||
|
}
|
||||||
|
@keyframes pulse-green {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
.workflow-card {
|
||||||
background: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%);
|
background: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%);
|
||||||
border: 1px solid #e2e8f0;
|
border-left: 4px solid #3b82f6;
|
||||||
}
|
}
|
||||||
.bid-indicator {
|
.alert-badge {
|
||||||
background: linear-gradient(45deg, #10b981 0%, #059669 100%);
|
animation: pulse-red 1.5s infinite;
|
||||||
}
|
}
|
||||||
.price-indicator {
|
@keyframes pulse-red {
|
||||||
background: linear-gradient(45deg, #f59e0b 0%, #d97706 100%);
|
0%, 100% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.1); }
|
||||||
|
}
|
||||||
|
.metric-card {
|
||||||
|
background: linear-gradient(145deg, #ffffff 0%, #fafbfc 100%);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-50 min-h-screen">
|
<body class="bg-gray-50 min-h-screen">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="gradient-bg text-white py-8">
|
<header class="gradient-bg text-white py-6 shadow-lg">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-4xl font-bold mb-2">
|
<h1 class="text-4xl font-bold mb-2">
|
||||||
<i class="fas fa-gavel mr-3"></i>
|
<i class="fas fa-cube mr-3"></i>
|
||||||
Troostwijk Auctions
|
Auctiora Monitoring Dashboard
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-xl opacity-90">Kavel Data Extraction & Analysis Dashboard</p>
|
<p class="text-lg opacity-90">Real-time Auction Data Processing & Monitoring</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<div class="text-2xl font-bold" id="total-kavels">5</div>
|
<div class="flex items-center space-x-2">
|
||||||
<div class="text-sm opacity-80">Total Kavels</div>
|
<span class="status-online inline-block w-3 h-3 bg-green-400 rounded-full"></span>
|
||||||
|
<span class="text-sm font-semibold">System Online</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs opacity-75 mt-1" id="last-update">Last updated: --:--</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,422 +67,506 @@
|
|||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<main class="container mx-auto px-4 py-8">
|
<main class="container mx-auto px-4 py-8">
|
||||||
<!-- Summary Cards -->
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
<!-- System Status Cards -->
|
||||||
<div class="bg-white rounded-lg shadow-md p-6 card-hover">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4 mb-8">
|
||||||
<div class="flex items-center">
|
<div class="bg-white rounded-lg shadow-md p-6 card-hover metric-card">
|
||||||
<div class="p-3 rounded-full bg-blue-100 text-blue-600">
|
<div class="flex items-center justify-between">
|
||||||
<i class="fas fa-tags text-xl"></i>
|
<div>
|
||||||
|
<div class="text-gray-600 text-sm font-semibold uppercase">Auctions</div>
|
||||||
|
<div class="text-3xl font-bold text-blue-600 mt-2" id="total-auctions">--</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-4">
|
<div class="p-4 rounded-full bg-blue-100">
|
||||||
<div class="text-2xl font-bold text-gray-800" id="categories-count">5</div>
|
<i class="fas fa-gavel text-2xl text-blue-600"></i>
|
||||||
<div class="text-gray-600">Categories</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white rounded-lg shadow-md p-6 card-hover">
|
<div class="bg-white rounded-lg shadow-md p-6 card-hover metric-card">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center justify-between">
|
||||||
<div class="p-3 rounded-full bg-green-100 text-green-600">
|
<div>
|
||||||
<i class="fas fa-map-marker-alt text-xl"></i>
|
<div class="text-gray-600 text-sm font-semibold uppercase">Total Lots</div>
|
||||||
|
<div class="text-3xl font-bold text-green-600 mt-2" id="total-lots">--</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-4">
|
<div class="p-4 rounded-full bg-green-100">
|
||||||
<div class="text-2xl font-bold text-gray-800" id="locations-count">5</div>
|
<i class="fas fa-boxes text-2xl text-green-600"></i>
|
||||||
<div class="text-gray-600">Locations</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white rounded-lg shadow-md p-6 card-hover">
|
<div class="bg-white rounded-lg shadow-md p-6 card-hover metric-card">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center justify-between">
|
||||||
<div class="p-3 rounded-full bg-yellow-100 text-yellow-600">
|
<div>
|
||||||
<i class="fas fa-euro-sign text-xl"></i>
|
<div class="text-gray-600 text-sm font-semibold uppercase">Images</div>
|
||||||
|
<div class="text-3xl font-bold text-purple-600 mt-2" id="total-images">--</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-4">
|
<div class="p-4 rounded-full bg-purple-100">
|
||||||
<div class="text-2xl font-bold text-gray-800">€67,250</div>
|
<i class="fas fa-images text-2xl text-purple-600"></i>
|
||||||
<div class="text-gray-600">Total Value</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white rounded-lg shadow-md p-6 card-hover">
|
<div class="bg-white rounded-lg shadow-md p-6 card-hover metric-card">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center justify-between">
|
||||||
<div class="p-3 rounded-full bg-purple-100 text-purple-600">
|
<div>
|
||||||
<i class="fas fa-clock text-xl"></i>
|
<div class="text-gray-600 text-sm font-semibold uppercase">Active Lots</div>
|
||||||
|
<div class="text-3xl font-bold text-yellow-600 mt-2" id="active-lots">--</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-4">
|
<div class="p-4 rounded-full bg-yellow-100">
|
||||||
<div class="text-2xl font-bold text-gray-800" id="avg-bids">24</div>
|
<i class="fas fa-clock text-2xl text-yellow-600"></i>
|
||||||
<div class="text-gray-600">Avg Bids</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 card-hover metric-card">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-gray-600 text-sm font-semibold uppercase">Closing Soon</div>
|
||||||
|
<div class="text-3xl font-bold text-red-600 mt-2" id="closing-soon">--</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 rounded-full bg-red-100">
|
||||||
|
<i class="fas fa-bell text-2xl text-red-600"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistics Overview -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 col-span-2">
|
||||||
|
<h3 class="text-xl font-semibold mb-4 text-gray-800">
|
||||||
|
<i class="fas fa-chart-line mr-2 text-blue-600"></i>
|
||||||
|
Bidding Statistics
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-3 gap-4">
|
||||||
|
<div class="text-center p-4 bg-blue-50 rounded-lg">
|
||||||
|
<div class="text-gray-600 text-sm font-semibold">Lots with Bids</div>
|
||||||
|
<div class="text-2xl font-bold text-blue-600 mt-2" id="lots-with-bids">--</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-4 bg-green-50 rounded-lg">
|
||||||
|
<div class="text-gray-600 text-sm font-semibold">Total Bid Value</div>
|
||||||
|
<div class="text-2xl font-bold text-green-600 mt-2" id="total-bid-value">--</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-4 bg-purple-50 rounded-lg">
|
||||||
|
<div class="text-gray-600 text-sm font-semibold">Average Bid</div>
|
||||||
|
<div class="text-2xl font-bold text-purple-600 mt-2" id="average-bid">--</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<h3 class="text-xl font-semibold mb-4 text-gray-800">
|
||||||
|
<i class="fas fa-tachometer-alt mr-2 text-green-600"></i>
|
||||||
|
Rate Limiting
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-gray-600 text-sm">Total Requests</span>
|
||||||
|
<span class="font-bold text-gray-800" id="rate-total-requests">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-gray-600 text-sm">Successful</span>
|
||||||
|
<span class="font-bold text-green-600" id="rate-successful">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-gray-600 text-sm">Failed</span>
|
||||||
|
<span class="font-bold text-red-600" id="rate-failed">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-gray-600 text-sm">Avg Duration</span>
|
||||||
|
<span class="font-bold text-blue-600" id="rate-avg-duration">--</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Workflow Controls -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
|
||||||
|
<h3 class="text-xl font-semibold mb-4 text-gray-800">
|
||||||
|
<i class="fas fa-play-circle mr-2 text-blue-600"></i>
|
||||||
|
Workflow Controls
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<button onclick="triggerWorkflow('scraper-import')"
|
||||||
|
class="workflow-card p-4 rounded-lg hover:shadow-lg transition-all">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<i class="fas fa-download text-blue-600 text-xl"></i>
|
||||||
|
<span class="text-xs text-gray-500" id="status-scraper">Ready</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm font-semibold text-gray-800">Import Scraper Data</div>
|
||||||
|
<div class="text-xs text-gray-600 mt-1">Load auction/lot data from external scraper</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onclick="triggerWorkflow('image-processing')"
|
||||||
|
class="workflow-card p-4 rounded-lg hover:shadow-lg transition-all">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<i class="fas fa-image text-purple-600 text-xl"></i>
|
||||||
|
<span class="text-xs text-gray-500" id="status-images">Ready</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm font-semibold text-gray-800">Process Images</div>
|
||||||
|
<div class="text-xs text-gray-600 mt-1">Download & analyze images with object detection</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onclick="triggerWorkflow('bid-monitoring')"
|
||||||
|
class="workflow-card p-4 rounded-lg hover:shadow-lg transition-all">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<i class="fas fa-chart-line text-green-600 text-xl"></i>
|
||||||
|
<span class="text-xs text-gray-500" id="status-bids">Ready</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm font-semibold text-gray-800">Monitor Bids</div>
|
||||||
|
<div class="text-xs text-gray-600 mt-1">Track bid changes and send notifications</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onclick="triggerWorkflow('closing-alerts')"
|
||||||
|
class="workflow-card p-4 rounded-lg hover:shadow-lg transition-all">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<i class="fas fa-bell text-red-600 text-xl"></i>
|
||||||
|
<span class="text-xs text-gray-500" id="status-alerts">Ready</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm font-semibold text-gray-800">Closing Alerts</div>
|
||||||
|
<div class="text-xs text-gray-600 mt-1">Alert for lots closing within 30 minutes</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Charts Section -->
|
<!-- Charts Section -->
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
||||||
<!-- Categories Chart -->
|
<!-- Country Distribution -->
|
||||||
<div class="bg-white rounded-lg shadow-md p-6">
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
<h3 class="text-xl font-semibold mb-4 text-gray-800">
|
<h3 class="text-xl font-semibold mb-4 text-gray-800">
|
||||||
<i class="fas fa-chart-pie mr-2 text-blue-600"></i>
|
<i class="fas fa-globe mr-2 text-blue-600"></i>
|
||||||
Kavel Distribution by Category
|
Auctions by Country
|
||||||
</h3>
|
</h3>
|
||||||
<div id="categories-chart" style="height: 300px;"></div>
|
<div id="country-chart" style="height: 300px;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Price Ranges Chart -->
|
<!-- Category Distribution -->
|
||||||
<div class="bg-white rounded-lg shadow-md p-6">
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
<h3 class="text-xl font-semibold mb-4 text-gray-800">
|
<h3 class="text-xl font-semibold mb-4 text-gray-800">
|
||||||
<i class="fas fa-chart-bar mr-2 text-green-600"></i>
|
<i class="fas fa-tags mr-2 text-green-600"></i>
|
||||||
Price Distribution
|
Lots by Category
|
||||||
</h3>
|
</h3>
|
||||||
<div id="prices-chart" style="height: 300px;"></div>
|
<div id="category-chart" style="height: 300px;"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bid Activity Chart -->
|
<!-- Closing Soon Table -->
|
||||||
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
|
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
|
||||||
<h3 class="text-xl font-semibold mb-4 text-gray-800">
|
|
||||||
<i class="fas fa-chart-line mr-2 text-purple-600"></i>
|
|
||||||
Bidding Activity Analysis
|
|
||||||
</h3>
|
|
||||||
<div id="bids-chart" style="height: 400px;"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Kavel Details Table -->
|
|
||||||
<div class="bg-white rounded-lg shadow-md p-6">
|
|
||||||
<div class="flex justify-between items-center mb-6">
|
<div class="flex justify-between items-center mb-6">
|
||||||
<h3 class="text-xl font-semibold text-gray-800">
|
<h3 class="text-xl font-semibold text-gray-800">
|
||||||
<i class="fas fa-list mr-2 text-indigo-600"></i>
|
<i class="fas fa-exclamation-triangle mr-2 text-red-600"></i>
|
||||||
Kavel Details
|
Lots Closing Soon (< 30 min)
|
||||||
</h3>
|
</h3>
|
||||||
<div class="flex space-x-2">
|
<button onclick="fetchClosingSoon()" class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition-colors">
|
||||||
<button id="export-json" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors">
|
<i class="fas fa-sync mr-2"></i>Refresh
|
||||||
<i class="fas fa-download mr-2"></i>Export JSON
|
</button>
|
||||||
</button>
|
|
||||||
<button id="export-csv" class="bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition-colors">
|
|
||||||
<i class="fas fa-file-csv mr-2"></i>Export CSV
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="min-w-full divide-y divide-gray-200">
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
<thead class="bg-gray-50">
|
<thead class="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Kavel</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Lot ID</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Category</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Title</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Current Bid</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Current Bid</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Bids</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Closing Time</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Location</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Minutes Left</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">End Date</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="kavels-table" class="bg-white divide-y divide-gray-200">
|
<tbody id="closing-soon-table" class="bg-white divide-y divide-gray-200">
|
||||||
<!-- Table content will be populated by JavaScript -->
|
<tr>
|
||||||
|
<td colspan="6" class="px-6 py-4 text-center text-gray-500">Loading...</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Activity Log -->
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<h3 class="text-xl font-semibold mb-4 text-gray-800">
|
||||||
|
<i class="fas fa-history mr-2 text-purple-600"></i>
|
||||||
|
Activity Log
|
||||||
|
</h3>
|
||||||
|
<div id="activity-log" class="space-y-2 max-h-64 overflow-y-auto">
|
||||||
|
<div class="text-sm text-gray-500">Monitoring system started...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="bg-gray-800 text-white py-8 mt-12">
|
<footer class="bg-gray-800 text-white py-6 mt-12">
|
||||||
<div class="container mx-auto px-4 text-center">
|
<div class="container mx-auto px-4 text-center">
|
||||||
<p class="text-gray-300">
|
<p class="text-gray-300">
|
||||||
<i class="fas fa-info-circle mr-2"></i>
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
Data extracted from Troostwijk Auctions - Industrial Equipment & Machinery Auctions
|
Auctiora - Auction Data Processing & Monitoring System
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm text-gray-400 mt-2">
|
<p class="text-sm text-gray-400 mt-2">
|
||||||
Generated on: <span id="generation-date"></span>
|
Architecture: Quarkus + SQLite + REST API | Auto-refresh: 15 seconds
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Sample data (in real implementation, this would be loaded from the extracted files)
|
// Dashboard state
|
||||||
const kavelData = [
|
let dashboardData = {
|
||||||
{
|
status: {},
|
||||||
"id": "KAVEL_001",
|
statistics: {},
|
||||||
"title": "Industrial CNC Machine - Haas VF-2",
|
rateLimitStats: {},
|
||||||
"description": "Used Haas VF-2 vertical machining center, 30 taper, 10,000 RPM spindle",
|
closingSoon: []
|
||||||
"current_bid": "€12,500",
|
};
|
||||||
"bid_count": "23",
|
|
||||||
"end_date": "2025-11-28 14:00:00",
|
|
||||||
"location": "Amsterdam, Netherlands",
|
|
||||||
"auction_place": "Metalworking Equipment Auction",
|
|
||||||
"category": "Machinery",
|
|
||||||
"condition": "Used",
|
|
||||||
"year": "2018",
|
|
||||||
"images": ["https://example.com/image1.jpg", "https://example.com/image2.jpg"],
|
|
||||||
"specifications": {
|
|
||||||
"Spindle Speed": "10,000 RPM",
|
|
||||||
"Tool Capacity": "24 tools",
|
|
||||||
"Table Size": "914 x 356 mm",
|
|
||||||
"Travel X/Y/Z": "762/406/508 mm"
|
|
||||||
},
|
|
||||||
"url": "https://www.troostwijkauctions.com/lots/12345"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "KAVEL_002",
|
|
||||||
"title": "Forklift Truck - Linde E20",
|
|
||||||
"description": "Electric forklift, 2 ton capacity, including charger",
|
|
||||||
"current_bid": "€8,750",
|
|
||||||
"bid_count": "15",
|
|
||||||
"end_date": "2025-11-28 15:30:00",
|
|
||||||
"location": "Rotterdam, Netherlands",
|
|
||||||
"auction_place": "Warehouse Equipment Auction",
|
|
||||||
"category": "Material Handling",
|
|
||||||
"condition": "Good",
|
|
||||||
"year": "2020",
|
|
||||||
"images": ["https://example.com/forklift1.jpg"],
|
|
||||||
"specifications": {
|
|
||||||
"Capacity": "2000 kg",
|
|
||||||
"Lift Height": "4.5 meters",
|
|
||||||
"Battery": "80V lithium-ion",
|
|
||||||
"Hours": "1,250 hours"
|
|
||||||
},
|
|
||||||
"url": "https://www.troostwijkauctions.com/lots/12346"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "KAVEL_003",
|
|
||||||
"title": "Office Furniture Set - Complete",
|
|
||||||
"description": "Modern office furniture including desks, chairs, and storage units",
|
|
||||||
"current_bid": "€2,300",
|
|
||||||
"bid_count": "8",
|
|
||||||
"end_date": "2025-11-29 10:00:00",
|
|
||||||
"location": "Utrecht, Netherlands",
|
|
||||||
"auction_place": "Office Liquidation Auction",
|
|
||||||
"category": "Furniture",
|
|
||||||
"condition": "Excellent",
|
|
||||||
"year": "2023",
|
|
||||||
"images": ["https://example.com/office1.jpg", "https://example.com/office2.jpg"],
|
|
||||||
"specifications": {
|
|
||||||
"Desks": "6 executive desks",
|
|
||||||
"Chairs": "12 ergonomic office chairs",
|
|
||||||
"Storage": "4 filing cabinets",
|
|
||||||
"Conference Table": "1 large table"
|
|
||||||
},
|
|
||||||
"url": "https://www.troostwijkauctions.com/lots/12347"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "KAVEL_004",
|
|
||||||
"title": "Industrial Generator - 100kVA",
|
|
||||||
"description": "Cummins 100kVA diesel generator, low hours, recently serviced",
|
|
||||||
"current_bid": "€15,200",
|
|
||||||
"bid_count": "31",
|
|
||||||
"end_date": "2025-11-29 16:00:00",
|
|
||||||
"location": "Eindhoven, Netherlands",
|
|
||||||
"auction_place": "Power Equipment Auction",
|
|
||||||
"category": "Power Generation",
|
|
||||||
"condition": "Excellent",
|
|
||||||
"year": "2019",
|
|
||||||
"images": ["https://example.com/generator1.jpg"],
|
|
||||||
"specifications": {
|
|
||||||
"Power Output": "100 kVA",
|
|
||||||
"Fuel": "Diesel",
|
|
||||||
"Hours": "450 hours",
|
|
||||||
"Voltage": "400V 3-phase"
|
|
||||||
},
|
|
||||||
"url": "https://www.troostwijkauctions.com/lots/12348"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "KAVEL_005",
|
|
||||||
"title": "Laboratory Equipment Package",
|
|
||||||
"description": "Complete lab setup including microscopes, centrifuges, and analytical balances",
|
|
||||||
"current_bid": "€28,500",
|
|
||||||
"bid_count": "42",
|
|
||||||
"end_date": "2025-11-30 11:00:00",
|
|
||||||
"location": "Leiden, Netherlands",
|
|
||||||
"auction_place": "Medical Equipment Auction",
|
|
||||||
"category": "Laboratory",
|
|
||||||
"condition": "Good",
|
|
||||||
"year": "2021",
|
|
||||||
"images": ["https://example.com/lab1.jpg", "https://example.com/lab2.jpg"],
|
|
||||||
"specifications": {
|
|
||||||
"Microscopes": "3 digital microscopes",
|
|
||||||
"Centrifuges": "2 high-speed centrifuges",
|
|
||||||
"Balances": "5 analytical balances",
|
|
||||||
"Incubators": "2 temperature-controlled incubators"
|
|
||||||
},
|
|
||||||
"url": "https://www.troostwijkauctions.com/lots/12349"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// Initialize dashboard
|
// Initialize dashboard
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
populateTable();
|
addLog('Dashboard initialized');
|
||||||
createCharts();
|
fetchAllData();
|
||||||
setupExportButtons();
|
|
||||||
document.getElementById('generation-date').textContent = new Date().toLocaleString();
|
// Auto-refresh every 15 seconds
|
||||||
|
setInterval(fetchAllData, 15000);
|
||||||
});
|
});
|
||||||
|
|
||||||
function populateTable() {
|
// Fetch all dashboard data
|
||||||
const tableBody = document.getElementById('kavels-table');
|
async function fetchAllData() {
|
||||||
|
try {
|
||||||
kavelData.forEach(kavel => {
|
await Promise.all([
|
||||||
|
fetchStatus(),
|
||||||
|
fetchStatistics(),
|
||||||
|
fetchRateLimitStats(),
|
||||||
|
fetchClosingSoon()
|
||||||
|
]);
|
||||||
|
updateLastUpdate();
|
||||||
|
addLog('Dashboard data refreshed');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching dashboard data:', error);
|
||||||
|
addLog('Error: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch system status
|
||||||
|
async function fetchStatus() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/monitor/status');
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch status');
|
||||||
|
|
||||||
|
dashboardData.status = await response.json();
|
||||||
|
|
||||||
|
document.getElementById('total-auctions').textContent = dashboardData.status.auctions || 0;
|
||||||
|
document.getElementById('total-lots').textContent = dashboardData.status.lots || 0;
|
||||||
|
document.getElementById('total-images').textContent = dashboardData.status.images || 0;
|
||||||
|
document.getElementById('closing-soon').textContent = dashboardData.status.closingSoon || 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching status:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch statistics
|
||||||
|
async function fetchStatistics() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/monitor/statistics');
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch statistics');
|
||||||
|
|
||||||
|
dashboardData.statistics = await response.json();
|
||||||
|
|
||||||
|
document.getElementById('active-lots').textContent = dashboardData.statistics.activeLots || 0;
|
||||||
|
document.getElementById('lots-with-bids').textContent = dashboardData.statistics.lotsWithBids || 0;
|
||||||
|
document.getElementById('total-bid-value').textContent = dashboardData.statistics.totalBidValue || '€0.00';
|
||||||
|
document.getElementById('average-bid').textContent = dashboardData.statistics.averageBid || '€0.00';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching statistics:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch rate limit stats
|
||||||
|
async function fetchRateLimitStats() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/monitor/rate-limit/stats');
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch rate limit stats');
|
||||||
|
|
||||||
|
dashboardData.rateLimitStats = await response.json();
|
||||||
|
|
||||||
|
// Aggregate stats from all hosts
|
||||||
|
let totalRequests = 0, successfulRequests = 0, failedRequests = 0, avgDuration = 0;
|
||||||
|
const stats = dashboardData.rateLimitStats.statistics || {};
|
||||||
|
const hostCount = Object.keys(stats).length;
|
||||||
|
|
||||||
|
for (const hostStats of Object.values(stats)) {
|
||||||
|
totalRequests += hostStats.totalRequests || 0;
|
||||||
|
successfulRequests += hostStats.successfulRequests || 0;
|
||||||
|
failedRequests += hostStats.failedRequests || 0;
|
||||||
|
avgDuration += hostStats.averageDurationMs || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('rate-total-requests').textContent = totalRequests;
|
||||||
|
document.getElementById('rate-successful').textContent = successfulRequests;
|
||||||
|
document.getElementById('rate-failed').textContent = failedRequests;
|
||||||
|
document.getElementById('rate-avg-duration').textContent =
|
||||||
|
hostCount > 0 ? (avgDuration / hostCount).toFixed(0) + ' ms' : '--';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching rate limit stats:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch closing soon lots
|
||||||
|
async function fetchClosingSoon() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/monitor/lots/closing-soon?minutes=30');
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch closing soon lots');
|
||||||
|
|
||||||
|
dashboardData.closingSoon = await response.json();
|
||||||
|
updateClosingSoonTable();
|
||||||
|
|
||||||
|
// Update charts (placeholder for now)
|
||||||
|
updateCharts();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching closing soon:', error);
|
||||||
|
document.getElementById('closing-soon-table').innerHTML =
|
||||||
|
'<tr><td colspan="6" class="px-6 py-4 text-center text-red-600">Error loading data</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update closing soon table
|
||||||
|
function updateClosingSoonTable() {
|
||||||
|
const tableBody = document.getElementById('closing-soon-table');
|
||||||
|
|
||||||
|
if (dashboardData.closingSoon.length === 0) {
|
||||||
|
tableBody.innerHTML = '<tr><td colspan="6" class="px-6 py-4 text-center text-gray-500">No lots closing soon</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tableBody.innerHTML = '';
|
||||||
|
dashboardData.closingSoon.forEach(lot => {
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.className = 'hover:bg-gray-50';
|
row.className = 'hover:bg-gray-50';
|
||||||
|
|
||||||
|
const minutesLeft = lot.minutesUntilClose || 0;
|
||||||
|
const badgeColor = minutesLeft < 10 ? 'bg-red-100 text-red-800' : 'bg-yellow-100 text-yellow-800';
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${lot.lotId || '--'}</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-gray-900">${lot.title || 'N/A'}</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-bold text-green-600">${lot.currency || ''}${lot.currentBid ? lot.currentBid.toFixed(2) : '0.00'}</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${lot.closingTime || 'N/A'}</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<div class="flex items-center">
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${badgeColor}">
|
||||||
<div class="ml-4">
|
${minutesLeft} min
|
||||||
<div class="text-sm font-medium text-gray-900">${kavel.title}</div>
|
|
||||||
<div class="text-sm text-gray-500">${kavel.id}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
|
||||||
${kavel.category}
|
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
|
||||||
${kavel.current_bid}
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
||||||
${kavel.bid_count} bids
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
||||||
${kavel.location}
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
||||||
${new Date(kavel.end_date).toLocaleDateString()} ${new Date(kavel.end_date).toLocaleTimeString()}
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
<button onclick="showDetails('${kavel.id}')" class="text-indigo-600 hover:text-indigo-900 mr-3">
|
<a href="${lot.url || '#'}" target="_blank" class="text-blue-600 hover:text-blue-900">
|
||||||
<i class="fas fa-eye"></i>
|
|
||||||
</button>
|
|
||||||
<a href="${kavel.url}" target="_blank" class="text-green-600 hover:text-green-900">
|
|
||||||
<i class="fas fa-external-link-alt"></i>
|
<i class="fas fa-external-link-alt"></i>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
tableBody.appendChild(row);
|
tableBody.appendChild(row);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCharts() {
|
// Update charts (placeholder)
|
||||||
// Categories pie chart
|
function updateCharts() {
|
||||||
const categoriesData = kavelData.reduce((acc, kavel) => {
|
// Country distribution pie chart
|
||||||
acc[kavel.category] = (acc[kavel.category] || 0) + 1;
|
const countryData = {
|
||||||
return acc;
|
values: [5, 3, 2],
|
||||||
}, {});
|
labels: ['NL', 'BE', 'DE'],
|
||||||
|
|
||||||
const categoriesTrace = {
|
|
||||||
values: Object.values(categoriesData),
|
|
||||||
labels: Object.keys(categoriesData),
|
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
marker: {
|
marker: { colors: ['#3b82f6', '#10b981', '#f59e0b'] }
|
||||||
colors: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6']
|
|
||||||
},
|
|
||||||
textinfo: 'label+percent',
|
|
||||||
textposition: 'auto'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Plotly.newPlot('categories-chart', [categoriesTrace], {
|
Plotly.newPlot('country-chart', [countryData], {
|
||||||
showlegend: false,
|
showlegend: true,
|
||||||
margin: { t: 0, b: 0, l: 0, r: 0 }
|
margin: { t: 20, b: 20, l: 20, r: 20 }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Price ranges bar chart
|
// Category distribution
|
||||||
const priceRanges = kavelData.reduce((acc, kavel) => {
|
const categoryData = {
|
||||||
const price = parseInt(kavel.current_bid.replace(/[€,]/g, ''));
|
values: [4, 3, 2, 1],
|
||||||
let range;
|
labels: ['Machinery', 'Material Handling', 'Power Generation', 'Furniture'],
|
||||||
if (price < 5000) range = 'Under €5,000';
|
type: 'pie',
|
||||||
else if (price < 15000) range = '€5,000 - €15,000';
|
marker: { colors: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444'] }
|
||||||
else if (price < 25000) range = '€15,000 - €25,000';
|
|
||||||
else range = 'Over €25,000';
|
|
||||||
|
|
||||||
acc[range] = (acc[range] || 0) + 1;
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const pricesTrace = {
|
|
||||||
x: Object.keys(priceRanges),
|
|
||||||
y: Object.values(priceRanges),
|
|
||||||
type: 'bar',
|
|
||||||
marker: {
|
|
||||||
color: '#10b981'
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Plotly.newPlot('prices-chart', [pricesTrace], {
|
Plotly.newPlot('category-chart', [categoryData], {
|
||||||
xaxis: { title: 'Price Range' },
|
showlegend: true,
|
||||||
yaxis: { title: 'Number of Kavels' },
|
margin: { t: 20, b: 20, l: 20, r: 20 }
|
||||||
margin: { t: 20, b: 80, l: 60, r: 20 }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Bidding activity scatter plot
|
|
||||||
const bidsTrace = {
|
|
||||||
x: kavelData.map(k => k.title),
|
|
||||||
y: kavelData.map(k => parseInt(k.bid_count)),
|
|
||||||
mode: 'markers',
|
|
||||||
type: 'scatter',
|
|
||||||
marker: {
|
|
||||||
size: 12,
|
|
||||||
color: kavelData.map(k => parseInt(k.current_bid.replace(/[€,]/g, ''))),
|
|
||||||
colorscale: 'Viridis',
|
|
||||||
showscale: true,
|
|
||||||
colorbar: { title: 'Price (€)' }
|
|
||||||
},
|
|
||||||
text: kavelData.map(k => `${k.title}<br>Bids: ${k.bid_count}<br>Price: ${k.current_bid}`),
|
|
||||||
hovertemplate: '%{text}<extra></extra>'
|
|
||||||
};
|
|
||||||
|
|
||||||
Plotly.newPlot('bids-chart', [bidsTrace], {
|
|
||||||
xaxis: { title: 'Kavel', tickangle: -45 },
|
|
||||||
yaxis: { title: 'Number of Bids' },
|
|
||||||
margin: { t: 20, b: 150, l: 60, r: 80 }
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupExportButtons() {
|
// Trigger workflow
|
||||||
document.getElementById('export-json').addEventListener('click', function() {
|
async function triggerWorkflow(workflow) {
|
||||||
downloadData(kavelData, 'troostwijk_kavels.json', 'application/json');
|
const statusId = 'status-' + workflow.split('-')[0];
|
||||||
});
|
const statusEl = document.getElementById(statusId);
|
||||||
|
|
||||||
document.getElementById('export-csv').addEventListener('click', function() {
|
statusEl.textContent = 'Running...';
|
||||||
const csvData = convertToCSV(kavelData);
|
statusEl.className = 'text-xs text-blue-600 font-semibold';
|
||||||
downloadData(csvData, 'troostwijk_kavels.csv', 'text/csv');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadData(data, filename, mimeType) {
|
addLog(`Triggering workflow: ${workflow}`);
|
||||||
const blob = new Blob([typeof data === 'string' ? data : JSON.stringify(data, null, 2)],
|
|
||||||
{ type: mimeType });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = filename;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertToCSV(data) {
|
try {
|
||||||
const headers = ['ID', 'Title', 'Category', 'Current Bid', 'Bid Count', 'Location', 'End Date'];
|
const response = await fetch(`/api/monitor/trigger/${workflow}`, {
|
||||||
const csvContent = [
|
method: 'POST'
|
||||||
headers.join(','),
|
});
|
||||||
...data.map(row => [
|
|
||||||
row.id, row.title, row.category, row.current_bid,
|
|
||||||
row.bid_count, row.location, row.end_date
|
|
||||||
].join(','))
|
|
||||||
].join('\n');
|
|
||||||
return csvContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showDetails(kavelId) {
|
if (!response.ok) throw new Error('Workflow trigger failed');
|
||||||
const kavel = kavelData.find(k => k.id === kavelId);
|
|
||||||
if (kavel) {
|
const result = await response.json();
|
||||||
alert(`Details for ${kavel.title}:\n\nDescription: ${kavel.description}\nCondition: ${kavel.condition}\nYear: ${kavel.year}\nSpecifications: ${JSON.stringify(kavel.specifications, null, 2)}`);
|
addLog(`✓ ${result.message || 'Workflow completed'}`, 'success');
|
||||||
|
statusEl.textContent = 'Complete';
|
||||||
|
statusEl.className = 'text-xs text-green-600 font-semibold';
|
||||||
|
|
||||||
|
// Refresh data after workflow
|
||||||
|
setTimeout(fetchAllData, 2000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error triggering workflow:', error);
|
||||||
|
addLog(`✗ Workflow failed: ${error.message}`, 'error');
|
||||||
|
statusEl.textContent = 'Failed';
|
||||||
|
statusEl.className = 'text-xs text-red-600 font-semibold';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset status after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
statusEl.textContent = 'Ready';
|
||||||
|
statusEl.className = 'text-xs text-gray-500';
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add log entry
|
||||||
|
function addLog(message, type = 'info') {
|
||||||
|
const logContainer = document.getElementById('activity-log');
|
||||||
|
const logEntry = document.createElement('div');
|
||||||
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
|
|
||||||
|
let iconClass = 'fas fa-info-circle text-blue-600';
|
||||||
|
let textClass = 'text-gray-700';
|
||||||
|
|
||||||
|
if (type === 'success') {
|
||||||
|
iconClass = 'fas fa-check-circle text-green-600';
|
||||||
|
textClass = 'text-green-700';
|
||||||
|
} else if (type === 'error') {
|
||||||
|
iconClass = 'fas fa-exclamation-circle text-red-600';
|
||||||
|
textClass = 'text-red-700';
|
||||||
|
}
|
||||||
|
|
||||||
|
logEntry.className = `text-sm flex items-start space-x-2 ${textClass}`;
|
||||||
|
logEntry.innerHTML = `
|
||||||
|
<i class="${iconClass} mt-1"></i>
|
||||||
|
<span class="text-gray-500">${timestamp}</span>
|
||||||
|
<span>${message}</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
logContainer.insertBefore(logEntry, logContainer.firstChild);
|
||||||
|
|
||||||
|
// Keep only last 20 entries
|
||||||
|
while (logContainer.children.length > 20) {
|
||||||
|
logContainer.removeChild(logContainer.lastChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last update timestamp
|
||||||
|
function updateLastUpdate() {
|
||||||
|
const now = new Date();
|
||||||
|
document.getElementById('last-update').textContent =
|
||||||
|
`Last updated: ${now.toLocaleTimeString()}`;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -138,13 +138,13 @@ class ImageProcessingServiceTest {
|
|||||||
200.0, "EUR", "https://example.com", null, false)
|
200.0, "EUR", "https://example.com", null, false)
|
||||||
));
|
));
|
||||||
|
|
||||||
when(mockDb.getImagesForLot(anyInt())).thenReturn(List.of());
|
when(mockDb.getImagesForLot(anyLong())).thenReturn(List.of());
|
||||||
|
|
||||||
service.processPendingImages();
|
service.processPendingImages();
|
||||||
|
|
||||||
// Verify lots were queried
|
// Verify lots were queried
|
||||||
verify(mockDb, times(1)).getAllLots();
|
verify(mockDb, times(1)).getAllLots();
|
||||||
verify(mockDb, times(2)).getImagesForLot(anyInt());
|
verify(mockDb, times(2)).getImagesForLot(anyLong());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -1,589 +0,0 @@
|
|||||||
# ✅ Quarkus Integration Complete
|
|
||||||
|
|
||||||
## 🎯 Summary
|
|
||||||
|
|
||||||
The Troostwijk Auction Monitor is now **fully integrated with Quarkus Framework** with all components production-ready.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📦 What You Added
|
|
||||||
|
|
||||||
✅ **Quarkus BOM** in pom.xml (version 3.17.7)
|
|
||||||
✅ **application.properties** with configuration
|
|
||||||
✅ **Dockerfile** for Quarkus fast-jar
|
|
||||||
|
|
||||||
## 🚀 What I Added
|
|
||||||
|
|
||||||
✅ **Quarkus Dependencies** - scheduler, health, rest
|
|
||||||
✅ **QuarkusWorkflowScheduler** - @Scheduled workflows
|
|
||||||
✅ **AuctionMonitorProducer** - CDI service producers
|
|
||||||
✅ **AuctionMonitorResource** - Complete REST API
|
|
||||||
✅ **AuctionMonitorHealthCheck** - Health probes
|
|
||||||
✅ **docker-compose.yml** - Easy local deployment
|
|
||||||
✅ **k8s/deployment.yaml** - Kubernetes manifests
|
|
||||||
✅ **Complete Documentation** - 3 comprehensive guides
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏗️ Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
┌────────────────────────────────────────────────────────┐
|
|
||||||
│ QUARKUS APPLICATION │
|
|
||||||
├────────────────────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ HTTP Server (Port 8081) │
|
|
||||||
│ ├─ /api/monitor/* → REST API endpoints │
|
|
||||||
│ ├─ /health/* → Health check probes │
|
|
||||||
│ └─ /q/dev/* → Dev UI (dev mode only) │
|
|
||||||
│ │
|
|
||||||
│ Scheduler (Quarkus @Scheduled) │
|
|
||||||
│ ├─ Scraper Import → Every 30 minutes │
|
|
||||||
│ ├─ Image Processing → Every 1 hour │
|
|
||||||
│ ├─ Bid Monitoring → Every 15 minutes │
|
|
||||||
│ └─ Closing Alerts → Every 5 minutes │
|
|
||||||
│ │
|
|
||||||
│ CDI Container (Dependency Injection) │
|
|
||||||
│ ├─ DatabaseService (@Singleton) │
|
|
||||||
│ ├─ NotificationService (@Singleton) │
|
|
||||||
│ ├─ ObjectDetectionService (@Singleton) │
|
|
||||||
│ └─ ImageProcessingService (@Singleton) │
|
|
||||||
│ │
|
|
||||||
└────────────────────────────────────────────────────────┘
|
|
||||||
│ │ │
|
|
||||||
▼ ▼ ▼
|
|
||||||
[SQLite DB] [Desktop/Email] [YOLO Models]
|
|
||||||
cache.db Notifications Object Detection
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 How to Run
|
|
||||||
|
|
||||||
### 1. Development Mode (with Live Reload)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mvn quarkus:dev
|
|
||||||
|
|
||||||
# Features:
|
|
||||||
# ✓ Live reload (no restart needed)
|
|
||||||
# ✓ Dev UI: http://localhost:8081/q/dev/
|
|
||||||
# ✓ Continuous testing
|
|
||||||
# ✓ Debug on port 5005
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Production JAR
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build
|
|
||||||
mvn clean package
|
|
||||||
|
|
||||||
# Run
|
|
||||||
java -jar target/quarkus-app/quarkus-run.jar
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Docker
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build
|
|
||||||
docker build -t auction-monitor .
|
|
||||||
|
|
||||||
# Run
|
|
||||||
docker run -p 8081:8081 \
|
|
||||||
-v $(pwd)/data:/mnt/okcomputer/output \
|
|
||||||
auction-monitor
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Docker Compose (Recommended)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
docker-compose logs -f
|
|
||||||
|
|
||||||
# Stop
|
|
||||||
docker-compose down
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Kubernetes
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Deploy
|
|
||||||
kubectl apply -f k8s/deployment.yaml
|
|
||||||
|
|
||||||
# Port forward
|
|
||||||
kubectl port-forward svc/auction-monitor 8081:8081 -n auction-monitor
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📡 API Endpoints
|
|
||||||
|
|
||||||
Base URL: `http://localhost:8081`
|
|
||||||
|
|
||||||
### Status & Statistics
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Get status
|
|
||||||
curl http://localhost:8081/api/monitor/status
|
|
||||||
|
|
||||||
# Get statistics
|
|
||||||
curl http://localhost:8081/api/monitor/statistics
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manual Triggers
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Trigger scraper import
|
|
||||||
curl -X POST http://localhost:8081/api/monitor/trigger/scraper-import
|
|
||||||
|
|
||||||
# Trigger image processing
|
|
||||||
curl -X POST http://localhost:8081/api/monitor/trigger/image-processing
|
|
||||||
|
|
||||||
# Trigger bid monitoring
|
|
||||||
curl -X POST http://localhost:8081/api/monitor/trigger/bid-monitoring
|
|
||||||
|
|
||||||
# Trigger closing alerts
|
|
||||||
curl -X POST http://localhost:8081/api/monitor/trigger/closing-alerts
|
|
||||||
```
|
|
||||||
|
|
||||||
### Data Access
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# List auctions
|
|
||||||
curl http://localhost:8081/api/monitor/auctions
|
|
||||||
|
|
||||||
# Filter by country
|
|
||||||
curl http://localhost:8081/api/monitor/auctions?country=NL
|
|
||||||
|
|
||||||
# List active lots
|
|
||||||
curl http://localhost:8081/api/monitor/lots
|
|
||||||
|
|
||||||
# Lots closing soon
|
|
||||||
curl http://localhost:8081/api/monitor/lots/closing-soon?minutes=30
|
|
||||||
|
|
||||||
# Get lot images
|
|
||||||
curl http://localhost:8081/api/monitor/lots/12345/images
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Test notification
|
|
||||||
curl -X POST http://localhost:8081/api/monitor/test-notification \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"message":"Test","title":"Test","priority":"0"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏥 Health Checks
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Liveness (is app alive?)
|
|
||||||
curl http://localhost:8081/health/live
|
|
||||||
|
|
||||||
# Readiness (is app ready?)
|
|
||||||
curl http://localhost:8081/health/ready
|
|
||||||
|
|
||||||
# Startup (has app started?)
|
|
||||||
curl http://localhost:8081/health/started
|
|
||||||
|
|
||||||
# All health checks
|
|
||||||
curl http://localhost:8081/health
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚙️ Configuration
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Database
|
|
||||||
export AUCTION_DATABASE_PATH=/path/to/cache.db
|
|
||||||
export AUCTION_IMAGES_PATH=/path/to/images
|
|
||||||
|
|
||||||
# Notifications
|
|
||||||
export AUCTION_NOTIFICATION_CONFIG=desktop
|
|
||||||
# Or for email:
|
|
||||||
export AUCTION_NOTIFICATION_CONFIG=smtp:user@gmail.com:password:recipient@example.com
|
|
||||||
|
|
||||||
# Workflow schedules (cron)
|
|
||||||
export AUCTION_WORKFLOW_SCRAPER_IMPORT_CRON="0 */30 * * * ?"
|
|
||||||
export AUCTION_WORKFLOW_IMAGE_PROCESSING_CRON="0 0 * * * ?"
|
|
||||||
export AUCTION_WORKFLOW_BID_MONITORING_CRON="0 */15 * * * ?"
|
|
||||||
export AUCTION_WORKFLOW_CLOSING_ALERTS_CRON="0 */5 * * * ?"
|
|
||||||
|
|
||||||
# HTTP
|
|
||||||
export QUARKUS_HTTP_PORT=8081
|
|
||||||
export QUARKUS_LOG_CONSOLE_LEVEL=INFO
|
|
||||||
```
|
|
||||||
|
|
||||||
### Cron Expressions
|
|
||||||
|
|
||||||
| Expression | Meaning |
|
|
||||||
|------------|---------|
|
|
||||||
| `0 */30 * * * ?` | Every 30 minutes |
|
|
||||||
| `0 0 * * * ?` | Every hour (at minute 0) |
|
|
||||||
| `0 */15 * * * ?` | Every 15 minutes |
|
|
||||||
| `0 */5 * * * ?` | Every 5 minutes |
|
|
||||||
| `0 0 0 * * ?` | Daily at midnight |
|
|
||||||
| `0 0 */2 * * ?` | Every 2 hours |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Performance
|
|
||||||
|
|
||||||
### Startup Time
|
|
||||||
- **JVM Mode**: ~0.5 seconds
|
|
||||||
- **Native**: ~0.014 seconds (with GraalVM)
|
|
||||||
|
|
||||||
### Memory
|
|
||||||
- **JVM Mode**: ~50MB RSS
|
|
||||||
- **Native**: ~15MB RSS
|
|
||||||
|
|
||||||
### Container Size
|
|
||||||
- **Image**: ~150MB (with JRE)
|
|
||||||
- **Native**: ~50MB (native executable)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📁 Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
.
|
|
||||||
├── src/main/java/com/auction/
|
|
||||||
│ ├── QuarkusWorkflowScheduler.java # Scheduled workflows
|
|
||||||
│ ├── AuctionMonitorProducer.java # CDI producers
|
|
||||||
│ ├── AuctionMonitorResource.java # REST API
|
|
||||||
│ ├── AuctionMonitorHealthCheck.java # Health checks
|
|
||||||
│ ├── DatabaseService.java # Database operations
|
|
||||||
│ ├── NotificationService.java # Notifications
|
|
||||||
│ ├── ObjectDetectionService.java # YOLO detection
|
|
||||||
│ ├── ImageProcessingService.java # Image processing
|
|
||||||
│ ├── TroostwijkMonitor.java # Original monitor
|
|
||||||
│ └── Main.java # Entry point (legacy)
|
|
||||||
│
|
|
||||||
├── src/main/resources/
|
|
||||||
│ └── application.properties # Configuration
|
|
||||||
│
|
|
||||||
├── k8s/
|
|
||||||
│ ├── deployment.yaml # Kubernetes manifests
|
|
||||||
│ └── README.md # K8s guide
|
|
||||||
│
|
|
||||||
├── pom.xml # Maven config
|
|
||||||
├── Dockerfile # Container build
|
|
||||||
├── docker-compose.yml # Docker Compose
|
|
||||||
│
|
|
||||||
├── QUARKUS_GUIDE.md # Complete user guide
|
|
||||||
├── QUARKUS_IMPLEMENTATION.md # Implementation details
|
|
||||||
└── QUARKUS_COMPLETE.md # This file
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Documentation
|
|
||||||
|
|
||||||
### Primary Documentation
|
|
||||||
|
|
||||||
1. **QUARKUS_GUIDE.md** ⭐
|
|
||||||
- Complete usage guide
|
|
||||||
- All endpoints documented
|
|
||||||
- Configuration examples
|
|
||||||
- Deployment instructions
|
|
||||||
|
|
||||||
2. **QUARKUS_IMPLEMENTATION.md**
|
|
||||||
- Technical implementation details
|
|
||||||
- Architecture diagrams
|
|
||||||
- Code structure
|
|
||||||
- Design decisions
|
|
||||||
|
|
||||||
3. **QUARKUS_COMPLETE.md** (This file)
|
|
||||||
- Quick reference
|
|
||||||
- Summary of features
|
|
||||||
- Quick start commands
|
|
||||||
|
|
||||||
### Supporting Documentation
|
|
||||||
|
|
||||||
4. **k8s/README.md** - Kubernetes deployment
|
|
||||||
5. **docker-compose.yml** - Docker Compose reference
|
|
||||||
6. **README.md** - Main project README
|
|
||||||
7. **WORKFLOW_GUIDE.md** - Original workflow guide
|
|
||||||
8. **TEST_SUITE_SUMMARY.md** - Test documentation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✨ Key Features
|
|
||||||
|
|
||||||
### 1. **Quarkus Scheduler**
|
|
||||||
- ✅ Cron-based scheduling
|
|
||||||
- ✅ Configuration via properties
|
|
||||||
- ✅ No manual thread management
|
|
||||||
- ✅ Timezone support
|
|
||||||
|
|
||||||
### 2. **REST API**
|
|
||||||
- ✅ Status and statistics endpoints
|
|
||||||
- ✅ Manual workflow triggers
|
|
||||||
- ✅ Data access endpoints
|
|
||||||
- ✅ Test notification endpoint
|
|
||||||
|
|
||||||
### 3. **Health Checks**
|
|
||||||
- ✅ Liveness probe (Kubernetes)
|
|
||||||
- ✅ Readiness probe (Kubernetes)
|
|
||||||
- ✅ Startup probe (Kubernetes)
|
|
||||||
- ✅ Database connectivity check
|
|
||||||
|
|
||||||
### 4. **CDI/Dependency Injection**
|
|
||||||
- ✅ Type-safe injection
|
|
||||||
- ✅ Singleton services
|
|
||||||
- ✅ Configuration injection
|
|
||||||
- ✅ Centralized producers
|
|
||||||
|
|
||||||
### 5. **Docker Support**
|
|
||||||
- ✅ Optimized Dockerfile
|
|
||||||
- ✅ Fast-jar packaging
|
|
||||||
- ✅ Docker Compose config
|
|
||||||
- ✅ Health checks included
|
|
||||||
|
|
||||||
### 6. **Kubernetes Support**
|
|
||||||
- ✅ Complete manifests
|
|
||||||
- ✅ ConfigMap for configuration
|
|
||||||
- ✅ Secrets for credentials
|
|
||||||
- ✅ Persistent storage
|
|
||||||
- ✅ Auto-scaling (HPA)
|
|
||||||
- ✅ Ingress configuration
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Testing
|
|
||||||
|
|
||||||
### Quick Test
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start application
|
|
||||||
mvn quarkus:dev
|
|
||||||
|
|
||||||
# Test status endpoint
|
|
||||||
curl http://localhost:8081/api/monitor/status
|
|
||||||
|
|
||||||
# Test health check
|
|
||||||
curl http://localhost:8081/health/live
|
|
||||||
|
|
||||||
# Trigger workflow
|
|
||||||
curl -X POST http://localhost:8081/api/monitor/trigger/scraper-import
|
|
||||||
|
|
||||||
# Test notification
|
|
||||||
curl -X POST http://localhost:8081/api/monitor/test-notification \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"message":"Hello from Quarkus!"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Test
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start with Docker Compose
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Wait for startup
|
|
||||||
sleep 10
|
|
||||||
|
|
||||||
# Test health
|
|
||||||
curl http://localhost:8081/health/ready
|
|
||||||
|
|
||||||
# View logs
|
|
||||||
docker-compose logs -f
|
|
||||||
|
|
||||||
# Stop
|
|
||||||
docker-compose down
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Troubleshooting
|
|
||||||
|
|
||||||
### Issue: Port 8081 already in use
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Change port
|
|
||||||
export QUARKUS_HTTP_PORT=8082
|
|
||||||
mvn quarkus:dev
|
|
||||||
|
|
||||||
# Or in application.properties
|
|
||||||
quarkus.http.port=8082
|
|
||||||
```
|
|
||||||
|
|
||||||
### Issue: Database not found
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check path
|
|
||||||
ls -la C:/mnt/okcomputer/output/cache.db
|
|
||||||
|
|
||||||
# Create directory
|
|
||||||
mkdir -p C:/mnt/okcomputer/output
|
|
||||||
|
|
||||||
# Check permissions
|
|
||||||
chmod 755 C:/mnt/okcomputer/output
|
|
||||||
```
|
|
||||||
|
|
||||||
### Issue: Scheduler not running
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Enable debug logging
|
|
||||||
export QUARKUS_LOG_CATEGORY__IO_QUARKUS_SCHEDULER__LEVEL=DEBUG
|
|
||||||
|
|
||||||
# Check scheduler config
|
|
||||||
curl http://localhost:8081/q/dev/io.quarkus.quarkus-scheduler/scheduled-methods
|
|
||||||
```
|
|
||||||
|
|
||||||
### Issue: Health check failing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check logs
|
|
||||||
docker logs auction-monitor
|
|
||||||
|
|
||||||
# Test directly
|
|
||||||
curl -v http://localhost:8081/health/ready
|
|
||||||
|
|
||||||
# Verify database
|
|
||||||
sqlite3 C:/mnt/okcomputer/output/cache.db "SELECT COUNT(*) FROM auctions;"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Next Steps
|
|
||||||
|
|
||||||
### Immediate Actions
|
|
||||||
|
|
||||||
1. **Build and Run**
|
|
||||||
```bash
|
|
||||||
mvn clean package
|
|
||||||
java -jar target/quarkus-app/quarkus-run.jar
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Test API**
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8081/api/monitor/status
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Deploy**
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
# Or
|
|
||||||
kubectl apply -f k8s/deployment.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
### Optional Enhancements
|
|
||||||
|
|
||||||
1. **Native Image** (for ultra-fast startup)
|
|
||||||
```bash
|
|
||||||
mvn package -Pnative
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Metrics** (add Micrometer)
|
|
||||||
```xml
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.quarkus</groupId>
|
|
||||||
<artifactId>quarkus-micrometer-registry-prometheus</artifactId>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **OpenAPI** (API documentation)
|
|
||||||
```xml
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.quarkus</groupId>
|
|
||||||
<artifactId>quarkus-smallrye-openapi</artifactId>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Tracing** (distributed tracing)
|
|
||||||
```xml
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.quarkus</groupId>
|
|
||||||
<artifactId>quarkus-opentelemetry</artifactId>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📈 Comparison: Before vs After
|
|
||||||
|
|
||||||
| Aspect | Before | After (Quarkus) |
|
|
||||||
|--------|--------|-----------------|
|
|
||||||
| **Framework** | Plain Java | Quarkus |
|
|
||||||
| **Startup** | ~3-5s | ~0.5s |
|
|
||||||
| **Memory** | ~200MB | ~50MB |
|
|
||||||
| **Scheduling** | Manual ExecutorService | @Scheduled |
|
|
||||||
| **DI** | Manual | CDI @Inject |
|
|
||||||
| **REST API** | ❌ None | ✅ Full API |
|
|
||||||
| **Health** | ❌ None | ✅ Probes |
|
|
||||||
| **Config** | Hard-coded | Properties |
|
|
||||||
| **Dev Mode** | Manual restart | Live reload |
|
|
||||||
| **Docker** | Basic | Optimized |
|
|
||||||
| **K8s** | Not ready | Ready |
|
|
||||||
| **Monitoring** | Logs only | REST + Health |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 Summary
|
|
||||||
|
|
||||||
### ✅ What Works
|
|
||||||
|
|
||||||
- **Quarkus Framework** - Fully integrated and working
|
|
||||||
- **Scheduled Workflows** - Running on cron expressions
|
|
||||||
- **REST API** - All endpoints functional
|
|
||||||
- **Health Checks** - Kubernetes ready
|
|
||||||
- **Docker** - Optimized image and compose file
|
|
||||||
- **Kubernetes** - Complete deployment manifests
|
|
||||||
- **Configuration** - Externalized and flexible
|
|
||||||
- **Documentation** - Comprehensive guides
|
|
||||||
|
|
||||||
### 🚀 Ready for Production
|
|
||||||
|
|
||||||
The application is now:
|
|
||||||
- ✅ Cloud-native (Kubernetes)
|
|
||||||
- ✅ Container-ready (Docker)
|
|
||||||
- ✅ API-enabled (REST)
|
|
||||||
- ✅ Observable (Health checks)
|
|
||||||
- ✅ Configurable (Properties)
|
|
||||||
- ✅ Fast (0.5s startup)
|
|
||||||
- ✅ Efficient (50MB memory)
|
|
||||||
- ✅ Documented (3 guides)
|
|
||||||
|
|
||||||
### 🎯 Quick Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Development
|
|
||||||
mvn quarkus:dev
|
|
||||||
|
|
||||||
# Production
|
|
||||||
mvn clean package && java -jar target/quarkus-app/quarkus-run.jar
|
|
||||||
|
|
||||||
# Docker
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Kubernetes
|
|
||||||
kubectl apply -f k8s/deployment.yaml
|
|
||||||
|
|
||||||
# Test
|
|
||||||
curl http://localhost:8081/api/monitor/status
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**🎊 Quarkus integration is complete and production-ready! 🎊**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Support
|
|
||||||
|
|
||||||
For issues or questions:
|
|
||||||
1. Check **QUARKUS_GUIDE.md** for detailed usage
|
|
||||||
2. Check **QUARKUS_IMPLEMENTATION.md** for technical details
|
|
||||||
3. Check **k8s/README.md** for Kubernetes deployment
|
|
||||||
4. Review logs: `docker-compose logs -f`
|
|
||||||
5. Test health: `curl http://localhost:8081/health`
|
|
||||||
|
|
||||||
**Enjoy your Quarkus-powered Auction Monitor! 🚀**
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
# Refactoring Summary: Troostwijk Auction Monitor
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
This project has been refactored to focus on **image processing and monitoring**, removing all auction/lot scraping functionality which is now handled by the external `ARCHITECTURE-TROOSTWIJK-SCRAPER` process.
|
|
||||||
|
|
||||||
## Architecture Changes
|
|
||||||
|
|
||||||
### Removed Components
|
|
||||||
- ❌ **TroostwijkScraper.java** - Removed (replaced by TroostwijkMonitor)
|
|
||||||
- ❌ Auction discovery and scraping logic
|
|
||||||
- ❌ Lot scraping via Playwright/JSoup
|
|
||||||
- ❌ CacheDatabase (can be removed if not used elsewhere)
|
|
||||||
|
|
||||||
### New/Updated Components
|
|
||||||
|
|
||||||
#### New Classes
|
|
||||||
- ✅ **TroostwijkMonitor.java** - Monitors bids and coordinates services (no scraping)
|
|
||||||
- ✅ **ImageProcessingService.java** - Downloads images and runs object detection
|
|
||||||
- ✅ **Console.java** - Simple output utility (renamed from IO to avoid Java 25 conflict)
|
|
||||||
|
|
||||||
#### Modernized Classes
|
|
||||||
- ✅ **AuctionInfo** - Converted to immutable `record`
|
|
||||||
- ✅ **Lot** - Converted to immutable `record` with `minutesUntilClose()` method
|
|
||||||
- ✅ **DatabaseService.java** - Uses modern Java features:
|
|
||||||
- Text blocks (`"""`) for SQL
|
|
||||||
- Record accessor methods
|
|
||||||
- Added `getImagesForLot()` method
|
|
||||||
- Added `processed_at` timestamp to images table
|
|
||||||
- Nested `ImageRecord` record
|
|
||||||
|
|
||||||
#### Preserved Components
|
|
||||||
- ✅ **NotificationService.java** - Desktop/email notifications
|
|
||||||
- ✅ **ObjectDetectionService.java** - YOLO-based object detection
|
|
||||||
- ✅ **Main.java** - Updated to use new architecture
|
|
||||||
|
|
||||||
## Database Schema
|
|
||||||
|
|
||||||
### Populated by External Scraper
|
|
||||||
- `auctions` table - Auction metadata
|
|
||||||
- `lots` table - Lot details with bidding info
|
|
||||||
|
|
||||||
### Populated by This Process
|
|
||||||
- `images` table - Downloaded images with:
|
|
||||||
- `file_path` - Local storage path
|
|
||||||
- `labels` - Detected objects (comma-separated)
|
|
||||||
- `processed_at` - Processing timestamp
|
|
||||||
|
|
||||||
## Modern Java Features Used
|
|
||||||
|
|
||||||
- **Records** - Immutable data carriers (AuctionInfo, Lot, ImageRecord)
|
|
||||||
- **Text Blocks** - Multi-line SQL queries
|
|
||||||
- **var** - Type inference throughout
|
|
||||||
- **Switch expressions** - Where applicable
|
|
||||||
- **Pattern matching** - Ready for future enhancements
|
|
||||||
|
|
||||||
## Responsibilities
|
|
||||||
|
|
||||||
### This Project
|
|
||||||
1. ✅ Image downloading from URLs in database
|
|
||||||
2. ✅ Object detection using YOLO/OpenCV
|
|
||||||
3. ✅ Bid monitoring and change detection
|
|
||||||
4. ✅ Desktop and email notifications
|
|
||||||
5. ✅ Data enrichment with image analysis
|
|
||||||
|
|
||||||
### External ARCHITECTURE-TROOSTWIJK-SCRAPER
|
|
||||||
1. 🔄 Discover auctions from Troostwijk website
|
|
||||||
2. 🔄 Scrape lot details via API
|
|
||||||
3. 🔄 Populate `auctions` and `lots` tables
|
|
||||||
4. 🔄 Share database with this process
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Running the Monitor
|
|
||||||
```bash
|
|
||||||
# With environment variables
|
|
||||||
export DATABASE_FILE=troostwijk.db
|
|
||||||
export NOTIFICATION_CONFIG=desktop # or smtp:user:pass:email
|
|
||||||
|
|
||||||
java -jar troostwijk-monitor.jar
|
|
||||||
```
|
|
||||||
|
|
||||||
### Expected Output
|
|
||||||
```
|
|
||||||
=== Troostwijk Auction Monitor ===
|
|
||||||
|
|
||||||
✓ OpenCV loaded
|
|
||||||
Initializing monitor...
|
|
||||||
|
|
||||||
📊 Current Database State:
|
|
||||||
Total lots in database: 42
|
|
||||||
Total images processed: 0
|
|
||||||
|
|
||||||
[1/2] Processing images...
|
|
||||||
Processing pending images...
|
|
||||||
|
|
||||||
[2/2] Starting bid monitoring...
|
|
||||||
✓ Monitoring service started
|
|
||||||
|
|
||||||
✓ Monitor is running. Press Ctrl+C to stop.
|
|
||||||
|
|
||||||
NOTE: This process expects auction/lot data from the external scraper.
|
|
||||||
Make sure ARCHITECTURE-TROOSTWIJK-SCRAPER is running and populating the database.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Migration Notes
|
|
||||||
|
|
||||||
1. The project now compiles successfully with Java 25
|
|
||||||
2. All scraping logic removed - rely on external scraper
|
|
||||||
3. Shared database architecture for inter-process communication
|
|
||||||
4. Clean separation of concerns
|
|
||||||
5. Modern, maintainable codebase with records and text blocks
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
- Remove `CacheDatabase.java` if not needed
|
|
||||||
- Consider adding API endpoint for external scraper to trigger image processing
|
|
||||||
- Add metrics/logging framework
|
|
||||||
- Consider message queue (e.g., Redis, RabbitMQ) for better inter-process communication
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
# Troostwijk Auction Extractor - Run Instructions
|
|
||||||
|
|
||||||
## Fixed Warnings
|
|
||||||
|
|
||||||
All warnings have been resolved:
|
|
||||||
- ✅ SLF4J logging configured (slf4j-simple)
|
|
||||||
- ✅ Native access enabled for SQLite JDBC
|
|
||||||
- ✅ Logging output controlled via simplelogger.properties
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
1. **Java 21** installed
|
|
||||||
2. **Maven** installed
|
|
||||||
3. **IntelliJ IDEA** (recommended) or command line
|
|
||||||
|
|
||||||
## Setup (First Time Only)
|
|
||||||
|
|
||||||
### 1. Install Dependencies
|
|
||||||
|
|
||||||
In IntelliJ Terminal or PowerShell:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Reload Maven dependencies
|
|
||||||
mvn clean install
|
|
||||||
|
|
||||||
# Install Playwright browser binaries (first time only)
|
|
||||||
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Running the Application
|
|
||||||
|
|
||||||
### Option A: Using IntelliJ IDEA (Easiest)
|
|
||||||
|
|
||||||
1. **Add VM Options for native access:**
|
|
||||||
- Run → Edit Configurations
|
|
||||||
- Select or create configuration for `TroostwijkAuctionExtractor`
|
|
||||||
- In "VM options" field, add:
|
|
||||||
```
|
|
||||||
--enable-native-access=ALL-UNNAMED
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Add Program Arguments (optional):**
|
|
||||||
- In "Program arguments" field, add:
|
|
||||||
```
|
|
||||||
--max-visits 3
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Run the application:**
|
|
||||||
- Click the green Run button
|
|
||||||
|
|
||||||
### Option B: Using Maven (Command Line)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run with 3 page limit
|
|
||||||
mvn exec:java
|
|
||||||
|
|
||||||
# Run with custom arguments (override pom.xml defaults)
|
|
||||||
mvn exec:java -Dexec.args="--max-visits 5"
|
|
||||||
|
|
||||||
# Run without cache
|
|
||||||
mvn exec:java -Dexec.args="--no-cache --max-visits 2"
|
|
||||||
|
|
||||||
# Run with unlimited visits
|
|
||||||
mvn exec:java -Dexec.args=""
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option C: Using Java Directly
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Compile first
|
|
||||||
mvn clean compile
|
|
||||||
|
|
||||||
# Run with native access enabled
|
|
||||||
java --enable-native-access=ALL-UNNAMED \
|
|
||||||
-cp target/classes:$(mvn dependency:build-classpath -Dmdep.outputFile=/dev/stdout -q) \
|
|
||||||
com.auction.TroostwijkAuctionExtractor --max-visits 3
|
|
||||||
```
|
|
||||||
|
|
||||||
## Command Line Arguments
|
|
||||||
|
|
||||||
```
|
|
||||||
--max-visits <n> Limit actual page fetches to n (0 = unlimited, default)
|
|
||||||
--no-cache Disable page caching
|
|
||||||
--help Show help message
|
|
||||||
```
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
### Test with 3 page visits (cached pages don't count):
|
|
||||||
```bash
|
|
||||||
mvn exec:java -Dexec.args="--max-visits 3"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Fresh extraction without cache:
|
|
||||||
```bash
|
|
||||||
mvn exec:java -Dexec.args="--no-cache --max-visits 5"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Full extraction (all pages, unlimited):
|
|
||||||
```bash
|
|
||||||
mvn exec:java -Dexec.args=""
|
|
||||||
```
|
|
||||||
|
|
||||||
## Expected Output (No Warnings)
|
|
||||||
|
|
||||||
```
|
|
||||||
=== Troostwijk Auction Extractor ===
|
|
||||||
Max page visits set to: 3
|
|
||||||
|
|
||||||
Initializing Playwright browser...
|
|
||||||
✓ Browser ready
|
|
||||||
✓ Cache database initialized
|
|
||||||
|
|
||||||
Starting auction extraction from https://www.troostwijkauctions.com/auctions
|
|
||||||
|
|
||||||
[Page 1] Fetching auctions...
|
|
||||||
✓ Fetched from website (visit 1/3)
|
|
||||||
✓ Found 20 auctions
|
|
||||||
|
|
||||||
[Page 2] Fetching auctions...
|
|
||||||
✓ Loaded from cache
|
|
||||||
✓ Found 20 auctions
|
|
||||||
|
|
||||||
[Page 3] Fetching auctions...
|
|
||||||
✓ Fetched from website (visit 2/3)
|
|
||||||
✓ Found 20 auctions
|
|
||||||
|
|
||||||
✓ Total auctions extracted: 60
|
|
||||||
|
|
||||||
=== Results ===
|
|
||||||
Total auctions found: 60
|
|
||||||
Dutch auctions (NL): 45
|
|
||||||
Actual page visits: 2
|
|
||||||
|
|
||||||
✓ Browser and cache closed
|
|
||||||
```
|
|
||||||
|
|
||||||
## Cache Management
|
|
||||||
|
|
||||||
- Cache is stored in: `cache/page_cache.db`
|
|
||||||
- Cache expires after: 24 hours (configurable in code)
|
|
||||||
- To clear cache: Delete `cache/page_cache.db` file
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### If you still see warnings:
|
|
||||||
|
|
||||||
1. **Reload Maven project in IntelliJ:**
|
|
||||||
- Right-click `pom.xml` → Maven → Reload project
|
|
||||||
|
|
||||||
2. **Verify VM options:**
|
|
||||||
- Ensure `--enable-native-access=ALL-UNNAMED` is in VM options
|
|
||||||
|
|
||||||
3. **Clean and rebuild:**
|
|
||||||
```bash
|
|
||||||
mvn clean install
|
|
||||||
```
|
|
||||||
|
|
||||||
### If Playwright fails:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Reinstall browser binaries
|
|
||||||
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install chromium"
|
|
||||||
```
|
|
||||||
Reference in New Issue
Block a user