diff --git a/data/auction.db.sqlite b/data/auction.db.sqlite index d81fc69..08c5267 100644 Binary files a/data/auction.db.sqlite and b/data/auction.db.sqlite differ diff --git a/resources/images/c0a465b1-fc59-4765-b63f-b5d7dcd961c2_1.jpg b/resources/images/c0a465b1-fc59-4765-b63f-b5d7dcd961c2_1.jpg new file mode 100644 index 0000000..43d1832 Binary files /dev/null and b/resources/images/c0a465b1-fc59-4765-b63f-b5d7dcd961c2_1.jpg differ diff --git a/resources/thumbs/c0a465b1-fc59-4765-b63f-b5d7dcd961c2_1_thumb.jpg b/resources/thumbs/c0a465b1-fc59-4765-b63f-b5d7dcd961c2_1_thumb.jpg new file mode 100644 index 0000000..99f0bb5 Binary files /dev/null and b/resources/thumbs/c0a465b1-fc59-4765-b63f-b5d7dcd961c2_1_thumb.jpg differ diff --git a/src/main/java/com/auction/client/controllers/AuctionDetailController.java b/src/main/java/com/auction/client/controllers/AuctionDetailController.java index d107776..91ddb8c 100644 --- a/src/main/java/com/auction/client/controllers/AuctionDetailController.java +++ b/src/main/java/com/auction/client/controllers/AuctionDetailController.java @@ -50,7 +50,7 @@ public void loadAuction(int auctionId) { this.currentAuctionId = auctionId; try { var service = ClientContext.getInstance().getRmiProvider().getService(); - this.pollingService = new PollingService(service); + this.pollingService = new PollingService(); // load initial item state AuctionItem initial = service.getAuctionById(auctionId); this.currentItem = initial; @@ -60,10 +60,21 @@ public void loadAuction(int auctionId) { loadDetailThumbnail(auctionId, 1, thumb1View); loadDetailThumbnail(auctionId, 2, thumb2View); loadDetailThumbnail(auctionId, 3, thumb3View); - pollingService.startPolling(auctionId, item -> { - this.currentItem = item; - Platform.runLater(() -> updateUi(item)); - }); + pollingService.startPolling(() -> { + try { + AuctionItem item = service.getAuctionById(auctionId); + if (item != null && currentItem != null) { + String oldEnd = currentItem.getEndTime(); + if (oldEnd != null && !oldEnd.equals(item.getEndTime())) { + Platform.runLater(() -> showToast("Timer Extended!")); + } + } + this.currentItem = item; + Platform.runLater(() -> updateUi(item)); + } catch (Exception e) { + throw new RuntimeException(e); + } + }, 2); } catch (Exception e) { e.printStackTrace(); } @@ -180,6 +191,7 @@ private void handlePlaceBid() { Platform.runLater(() -> { updateUi(currentItem); bidStatusLabel.setText("Failed: " + finalMsg); + animateShakeError(); }); } finally { Platform.runLater(() -> { @@ -191,6 +203,17 @@ private void handlePlaceBid() { }, executor); } + private void animateShakeError() { + if (bidAmountField == null) return; + bidAmountField.setStyle("-fx-border-color: #f85149; -fx-border-width: 2px;"); + javafx.animation.TranslateTransition tt = new javafx.animation.TranslateTransition(Duration.millis(50), bidAmountField); + tt.setByX(10f); + tt.setCycleCount(6); + tt.setAutoReverse(true); + tt.setOnFinished(e -> bidAmountField.setStyle("")); + tt.play(); + } + private void loadDetailThumbnail(int auctionId, int index, javafx.scene.image.ImageView target) { java.util.concurrent.CompletableFuture.supplyAsync(() -> { try { diff --git a/src/main/java/com/auction/client/controllers/ConnectController.java b/src/main/java/com/auction/client/controllers/ConnectController.java index 0b10852..b062fe7 100644 --- a/src/main/java/com/auction/client/controllers/ConnectController.java +++ b/src/main/java/com/auction/client/controllers/ConnectController.java @@ -55,32 +55,63 @@ public void initialize() { @FXML private void handleConnect() { - String host = ipField.getText(); - if (host == null || host.trim().isEmpty()) { - host = "localhost"; - } + String hostInput = ipField.getText(); + String portInput = portField.getText(); + + String host = (hostInput == null || hostInput.trim().isEmpty()) ? "localhost" : hostInput.trim(); int port = 1099; - try { - String portStr = portField.getText(); - if (portStr != null && !portStr.trim().isEmpty()) { - port = Integer.parseInt(portStr); + if (portInput != null && !portInput.trim().isEmpty()) { + try { + port = Integer.parseInt(portInput.trim()); + } catch (NumberFormatException e) { + statusLabel.setText("Invalid port number."); + return; } - } catch (NumberFormatException e) { - statusLabel.setText("Invalid port number."); - return; } - try { - com.auction.client.core.ClientContext context = com.auction.client.core.ClientContext.getInstance(); - context.getRmiProvider().connect(host, port); - statusLabel.setText("Connected successfully!"); - context.getUdpClient().stopListening(); - - // Navigate to login - context.getViewLoader().loadView("login.fxml"); - } catch (Exception e) { - statusLabel.setText("Connection failed: " + e.getMessage()); - e.printStackTrace(); - } + statusLabel.setText("Connecting to " + host + ":" + port + "..."); + + final String finalHost = host; + final int finalPort = port; + + // Set short timeout for RMI connection attempts + System.setProperty("sun.rmi.transport.tcp.connectTimeout", "3000"); + + // Use a standard Thread to avoid any issues with common pool size or configuration + new Thread(() -> { + try { + com.auction.client.core.ClientContext context = com.auction.client.core.ClientContext.getInstance(); + // Perform the RMI connection and health check + context.getRmiProvider().connect(finalHost, finalPort); + + javafx.application.Platform.runLater(() -> { + try { + statusLabel.setText("Connected successfully!"); + context.getUdpClient().stopListening(); + // Navigate to login + context.getViewLoader().loadView("login.fxml"); + } catch (Exception e) { + statusLabel.setText("Navigation failed: " + e.getMessage()); + } + }); + } catch (Exception e) { + javafx.application.Platform.runLater(() -> { + Throwable root = e; + while (root.getCause() != null && root.getCause() != root) { + root = root.getCause(); + } + + String errorMsg; + if (root instanceof java.net.ConnectException || root instanceof java.rmi.ConnectException) { + errorMsg = "Server unreachable at " + finalHost + ":" + finalPort; + } else if (e instanceof java.rmi.NotBoundException) { + errorMsg = "RTDAS service not found on this server."; + } else { + errorMsg = (root.getMessage() != null) ? root.getMessage() : root.toString(); + } + statusLabel.setText("Connection failed: " + errorMsg); + }); + } + }).start(); } } diff --git a/src/main/java/com/auction/client/controllers/GalleryController.java b/src/main/java/com/auction/client/controllers/GalleryController.java index b2442a7..4c70ce7 100644 --- a/src/main/java/com/auction/client/controllers/GalleryController.java +++ b/src/main/java/com/auction/client/controllers/GalleryController.java @@ -30,11 +30,26 @@ public class GalleryController { private static final Image PLACEHOLDER_IMAGE = loadPlaceholderImage(); private java.util.List allAuctions = java.util.List.of(); + private com.auction.client.service.PollingService pollingService; + @FXML public void initialize() { try { var context = ClientContext.getInstance(); var service = context.getRmiProvider().getService(); + + pollingService = new com.auction.client.service.PollingService(); + pollingService.startPolling(() -> { + try { + List items = service.getActiveAuctions(); + allAuctions = (items == null) ? java.util.List.of() : items; + Platform.runLater(() -> handleSearch()); // handleSearch applies current filter/sort + } catch (Exception e) { + throw new RuntimeException(e); + } + }, 2); + + // initial load List items = service.getActiveAuctions(); allAuctions = (items == null) ? java.util.List.of() : items; Platform.runLater(() -> renderAuctions(allAuctions)); @@ -216,6 +231,7 @@ private static Image loadPlaceholderImage() { @FXML private void handleBackToDashboard() { try { + if (pollingService != null) pollingService.shutdown(); ClientContext context = ClientContext.getInstance(); String targetView = context.getPreviousViewName(); if (targetView == null || targetView.isBlank()) { @@ -226,4 +242,22 @@ private void handleBackToDashboard() { e.printStackTrace(); } } + + // Also shutdown before going to detail view + private void loadDetailView(AuctionItem item, int index) { + try { + if (pollingService != null) pollingService.shutdown(); + ClientContext context = ClientContext.getInstance(); + context.setPreviousViewName("gallery.fxml"); + Object ctrl = context.getViewLoader().loadView("auction_detail.fxml"); + if (ctrl instanceof AuctionDetailController) { + ((AuctionDetailController) ctrl).loadAuction(item.getId()); + if (index >= 0) { + ((AuctionDetailController) ctrl).showHeroImageIndex(index); + } + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } } diff --git a/src/main/java/com/auction/client/controllers/UserDashboardController.java b/src/main/java/com/auction/client/controllers/UserDashboardController.java index b7ba506..246ba02 100644 --- a/src/main/java/com/auction/client/controllers/UserDashboardController.java +++ b/src/main/java/com/auction/client/controllers/UserDashboardController.java @@ -60,8 +60,14 @@ public class UserDashboardController { private byte[] img1Bytes, img2Bytes, img3Bytes; + private com.auction.client.service.PollingService pollingService; + @FXML public void initialize() { + pollingService = new com.auction.client.service.PollingService(); + pollingService.startPolling(() -> { + javafx.application.Platform.runLater(() -> refreshDashboard()); + }, 2); refreshDashboard(); } @@ -116,6 +122,7 @@ private void handleRefreshDashboard() { @FXML private void handleOpenGallery() { try { + if (pollingService != null) pollingService.shutdown(); com.auction.client.core.ClientContext context = com.auction.client.core.ClientContext.getInstance(); context.setPreviousViewName("user_dashboard.fxml"); @@ -139,6 +146,7 @@ private void handleOpenAuctionDetail() { if (selected != null) { try { + if (pollingService != null) pollingService.shutdown(); com.auction.client.core.ClientContext context = com.auction.client.core.ClientContext.getInstance(); context.setCurrentAuctionId(selected.getId()); context.setPreviousViewName("user_dashboard.fxml"); diff --git a/src/main/java/com/auction/client/network/RmiClientProvider.java b/src/main/java/com/auction/client/network/RmiClientProvider.java index 16d83f6..7f8c8ea 100644 --- a/src/main/java/com/auction/client/network/RmiClientProvider.java +++ b/src/main/java/com/auction/client/network/RmiClientProvider.java @@ -25,6 +25,21 @@ public class RmiClientProvider { public IAuctionService connect(String host, int port) throws RemoteException, NotBoundException { Registry registry = LocateRegistry.getRegistry(host, port); service = (IAuctionService) registry.lookup(Constants.RMI_SERVICE_NAME); + + // Health check + service.serverTime(); + + // Persist last server + try { + java.nio.file.Path dir = java.nio.file.Paths.get(System.getProperty("user.home"), ".rtdas"); + if (!java.nio.file.Files.exists(dir)) { + java.nio.file.Files.createDirectories(dir); + } + java.nio.file.Files.writeString(dir.resolve("last_server"), host + ":" + port); + } catch (java.io.IOException e) { + System.err.println("Failed to persist last_server: " + e.getMessage()); + } + return service; } diff --git a/src/main/java/com/auction/client/network/UdpDiscoveryClient.java b/src/main/java/com/auction/client/network/UdpDiscoveryClient.java index 4a86484..dc97a9c 100644 --- a/src/main/java/com/auction/client/network/UdpDiscoveryClient.java +++ b/src/main/java/com/auction/client/network/UdpDiscoveryClient.java @@ -31,29 +31,36 @@ public void startListening() { if (running) return; running = true; listenerThread = new Thread(() -> { - try (DatagramSocket socket = new DatagramSocket(Constants.UDP_BROADCAST_PORT)) { - socket.setSoTimeout(1000); // 1 second timeout to allow interrupt checking - byte[] buffer = new byte[1024]; - while (running) { - try { - DatagramPacket packet = new DatagramPacket(buffer, buffer.length); - socket.receive(packet); - String data = new String(packet.getData(), 0, packet.getLength()).trim(); - // Format: RTDAS|| - if (data.startsWith(Constants.UDP_PREFIX + "|")) { - String[] parts = data.split("\\|"); - if (parts.length == 3) { - String serverName = parts[1]; - int rmiPort = Integer.parseInt(parts[2]); - String host = packet.getAddress().getHostAddress(); - ServerInfo info = new ServerInfo(serverName, host, rmiPort); - if (!discoveredServers.contains(info)) { - discoveredServers.add(info); + try { + java.net.DatagramSocket socket = new java.net.DatagramSocket(null); + socket.setReuseAddress(true); + socket.bind(new java.net.InetSocketAddress(com.auction.shared.Constants.UDP_BROADCAST_PORT)); + + try (socket) { + socket.setSoTimeout(1000); // 1 second timeout to allow interrupt checking + byte[] buffer = new byte[1024]; + while (running) { + try { + java.net.DatagramPacket packet = new java.net.DatagramPacket(buffer, buffer.length); + socket.receive(packet); + String data = new String(packet.getData(), 0, packet.getLength()).trim(); + // Format: RTDAS|v1|||| + if (data.startsWith(com.auction.shared.Constants.UDP_PREFIX + "|v1|")) { + String[] parts = data.split("\\|"); + if (parts.length >= 6) { + int rmiPort = Integer.parseInt(parts[2]); + String serverName = parts[3]; + String rmiHost = parts[5]; + String host = (rmiHost != null && !rmiHost.trim().isEmpty()) ? rmiHost : packet.getAddress().getHostAddress(); + ServerInfo info = new ServerInfo(serverName, host, rmiPort); + if (!discoveredServers.contains(info)) { + discoveredServers.add(info); + } } } + } catch (java.net.SocketTimeoutException e) { + // Expected timeout, loop continues and checks running flag } - } catch (java.net.SocketTimeoutException e) { - // Expected timeout, loop continues and checks running flag } } } catch (Exception e) { diff --git a/src/main/java/com/auction/client/service/PollingService.java b/src/main/java/com/auction/client/service/PollingService.java index fa6f647..fe7cf23 100644 --- a/src/main/java/com/auction/client/service/PollingService.java +++ b/src/main/java/com/auction/client/service/PollingService.java @@ -15,33 +15,38 @@ */ public class PollingService { - private final IAuctionService service; private ScheduledExecutorService scheduler; + private int failureCount = 0; - public PollingService(IAuctionService service) { - this.service = service; + public PollingService() { + // generic service } /** - * Start polling a specific auction for updates. - * @param auctionId auction to watch - * @param onUpdate callback with updated AuctionItem (called on background thread) + * Start polling a generic task. + * @param task the RMI call to execute + * @param intervalSeconds polling interval */ - public void startPolling(int auctionId, Consumer onUpdate) { - // TODO: schedule getAuctionById every 2s, call onUpdate with result + public void startPolling(Runnable task, int intervalSeconds) { scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(() -> { - try{ - AuctionItem item = service.getAuctionById(auctionId); - onUpdate.accept(item); - } catch(Exception e){ - System.err.println(e.getMessage()); - e.printStackTrace(); + try { + task.run(); + failureCount = 0; // reset on success + } catch (Exception e) { + failureCount++; + System.err.println("Polling failed (" + failureCount + "): " + e.getMessage()); + if (failureCount >= 3) { + shutdown(); + javafx.application.Platform.runLater(() -> { + com.auction.client.core.ClientContext.getInstance().handleConnectionLost(); + }); + } } - }, 0, 2, TimeUnit.SECONDS); + }, 0, intervalSeconds, TimeUnit.SECONDS); } - /** Stop all polling. Call when leaving the detail view. */ + /** Stop all polling. Call when leaving the view. */ public void shutdown() { if (scheduler != null && !scheduler.isShutdown()) { scheduler.shutdownNow(); diff --git a/src/main/java/com/auction/server/core/AdminManager.java b/src/main/java/com/auction/server/core/AdminManager.java index 2aa374f..9633b8f 100644 --- a/src/main/java/com/auction/server/core/AdminManager.java +++ b/src/main/java/com/auction/server/core/AdminManager.java @@ -47,11 +47,15 @@ public List getAuditLogs(int lastNLines, SessionContext context) throws } public List getAllUsers(SessionContext context) { - return userRepo.findAllUsers(); + return userRepo.findAllUsers().stream() + .map(u -> new User(u.getUsername(), null, u.getRoleType(), u.getCreatedAt())) + .toList(); } public List searchUsers(String query, SessionContext context) { - return userRepo.searchUsers(query); + return userRepo.searchUsers(query).stream() + .map(u -> new User(u.getUsername(), null, u.getRoleType(), u.getCreatedAt())) + .toList(); } public void promoteUserToAdmin(String username, SessionContext context) throws AuctionException { diff --git a/src/main/java/com/auction/server/core/UdpBroadcaster.java b/src/main/java/com/auction/server/core/UdpBroadcaster.java index 4062e6b..2bbe9e0 100644 --- a/src/main/java/com/auction/server/core/UdpBroadcaster.java +++ b/src/main/java/com/auction/server/core/UdpBroadcaster.java @@ -31,7 +31,12 @@ public void start() { if (scheduler != null) return; scheduler = Executors.newSingleThreadScheduledExecutor(); - String message = Constants.UDP_PREFIX + "|" + serverName + "|" + rmiPort; + String serverId = java.util.UUID.randomUUID().toString(); + // Fallback to localhost if we cannot determine address, but client prefers payload host + String rmiHost = "127.0.0.1"; + try { rmiHost = java.net.InetAddress.getLocalHost().getHostAddress(); } catch (Exception ignored) {} + + String message = Constants.UDP_PREFIX + "|v1|" + rmiPort + "|" + serverName + "|" + serverId + "|" + rmiHost; byte[] data = message.getBytes(); scheduler.scheduleAtFixedRate(() -> {