diff --git a/pom.xml b/pom.xml
index b2c9c1a..e89cb17 100644
--- a/pom.xml
+++ b/pom.xml
@@ -103,6 +103,28 @@
openpdf
1.3.30
+
+
+
+ com.itextpdf
+ itext7-core
+ 7.2.5
+ pom
+
+
+
+
+ com.itextpdf
+ kernel
+ 7.2.5
+
+
+
+
+ com.itextpdf
+ layout
+ 7.2.5
+
org.springframework.boot
diff --git a/src/main/java/de/assecutor/votianlt/mqtt/MqttV5ClientManager.java b/src/main/java/de/assecutor/votianlt/mqtt/MqttV5ClientManager.java
index a08cb2e..103815e 100644
--- a/src/main/java/de/assecutor/votianlt/mqtt/MqttV5ClientManager.java
+++ b/src/main/java/de/assecutor/votianlt/mqtt/MqttV5ClientManager.java
@@ -8,6 +8,8 @@ import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient;
import com.hivemq.client.mqtt.mqtt5.Mqtt5Client;
import de.assecutor.votianlt.config.MqttProperties;
import de.assecutor.votianlt.controller.MessageController;
+import de.assecutor.votianlt.model.PendingMqttMessage;
+import de.assecutor.votianlt.repository.PendingMqttMessageRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.SmartLifecycle;
import org.springframework.stereotype.Service;
@@ -16,6 +18,7 @@ import org.springframework.context.annotation.Lazy;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
+import java.util.List;
import java.util.Map;
import java.util.UUID;
@@ -29,14 +32,16 @@ public class MqttV5ClientManager implements SmartLifecycle {
private final MqttProperties props;
private final MessageController messageController;
+ private final PendingMqttMessageRepository pendingMessageRepository;
private final ObjectMapper objectMapper = new ObjectMapper();
private volatile boolean running = false;
private Mqtt5AsyncClient client;
- public MqttV5ClientManager(MqttProperties props, @Lazy MessageController messageController) {
+ public MqttV5ClientManager(MqttProperties props, @Lazy MessageController messageController, PendingMqttMessageRepository pendingMessageRepository) {
this.props = props;
this.messageController = messageController;
+ this.pendingMessageRepository = pendingMessageRepository;
}
@Override
@@ -89,6 +94,9 @@ public class MqttV5ClientManager implements SmartLifecycle {
}
running = true;
log.info("[MQTT] Subscribed to {} topics (QoS={}), awaiting messages ...", topics.length, qos);
+
+ // Process pending messages after successful connection
+ processPendingMessages();
} catch (Exception e) {
log.error("Failed to start HiveMQ MQTT client: {}", e.getMessage(), e);
}
@@ -192,18 +200,75 @@ public class MqttV5ClientManager implements SmartLifecycle {
public void publish(String topic, byte[] payload, int qos, boolean retained) {
try {
- if (client == null) {
- log.warn("[MQTT] Not connected, dropping publish topic={}", topic);
+ if (client == null || !running) {
+ log.warn("[MQTT] Not connected, saving message for later: topic={}", topic);
+ savePendingMessage(topic, payload, qos, retained);
return;
}
client.publishWith().topic(topic).payload(payload).qos(mapQos(qos)).retain(retained).send()
.whenComplete((ack, ex) -> {
if (ex != null) {
- log.error("Failed to publish to {}: {}", topic, ex.getMessage(), ex);
+ log.error("Failed to publish to {}: {}, saving for retry", topic, ex.getMessage(), ex);
+ savePendingMessage(topic, payload, qos, retained);
+ } else {
+ log.debug("Successfully published to topic: {}", topic);
}
});
} catch (Exception e) {
- log.error("Failed to publish to {}: {}", topic, e.getMessage(), e);
+ log.error("Failed to publish to {}: {}, saving for retry", topic, e.getMessage(), e);
+ savePendingMessage(topic, payload, qos, retained);
+ }
+ }
+
+ private void savePendingMessage(String topic, byte[] payload, int qos, boolean retained) {
+ try {
+ PendingMqttMessage pendingMessage = new PendingMqttMessage(topic, payload, qos, retained);
+ pendingMessageRepository.save(pendingMessage);
+ log.info("[MQTT] Saved pending message for topic: {}", topic);
+ } catch (Exception e) {
+ log.error("Failed to save pending MQTT message: {}", e.getMessage(), e);
+ }
+ }
+
+ private void processPendingMessages() {
+ try {
+ List pendingMessages = pendingMessageRepository.findAllByOrderByCreatedAtAsc();
+ if (pendingMessages.isEmpty()) {
+ log.debug("[MQTT] No pending messages to process");
+ return;
+ }
+
+ log.info("[MQTT] Processing {} pending messages", pendingMessages.size());
+ for (PendingMqttMessage pendingMessage : pendingMessages) {
+ try {
+ // Attempt to send the pending message
+ client.publishWith()
+ .topic(pendingMessage.getTopic())
+ .payload(pendingMessage.getPayload())
+ .qos(mapQos(pendingMessage.getQos()))
+ .retain(pendingMessage.isRetained())
+ .send()
+ .whenComplete((ack, ex) -> {
+ if (ex != null) {
+ log.warn("Failed to resend pending message to {}: {}", pendingMessage.getTopic(), ex.getMessage());
+ // Update retry count
+ pendingMessage.incrementRetryCount();
+ pendingMessageRepository.save(pendingMessage);
+ } else {
+ // Successfully sent, remove from pending
+ log.info("Successfully resent pending message to topic: {}", pendingMessage.getTopic());
+ pendingMessageRepository.delete(pendingMessage);
+ }
+ }).join(); // Wait for completion to process sequentially
+
+ } catch (Exception e) {
+ log.warn("Error processing pending message for {}: {}", pendingMessage.getTopic(), e.getMessage());
+ pendingMessage.incrementRetryCount();
+ pendingMessageRepository.save(pendingMessage);
+ }
+ }
+ } catch (Exception e) {
+ log.error("Error processing pending MQTT messages: {}", e.getMessage(), e);
}
}
}
diff --git a/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java b/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java
index 341705d..ea2be4e 100644
--- a/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java
+++ b/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java
@@ -27,7 +27,7 @@ import static com.vaadin.flow.theme.lumo.LumoUtility.*;
@AnonymousAllowed
-@Layout
+@Layout("main")
public final class MainLayout extends AppLayout {
private final SecurityService securityService;
diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java b/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java
index 0b226ce..7e3d523 100644
--- a/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java
+++ b/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java
@@ -12,16 +12,24 @@ import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.security.SecurityService;
import de.assecutor.votianlt.pages.base.ui.view.MainLayout;
import jakarta.annotation.security.RolesAllowed;
+import com.vaadin.flow.component.UI;
@Route(value = "dashboard", layout = MainLayout.class)
@PageTitle("VotianLT - Dashboard")
-@RolesAllowed("USER")
+@RolesAllowed({"USER"})
public class AuthenticatedStartView extends VerticalLayout {
private final SecurityService securityService;
public AuthenticatedStartView(SecurityService securityService) {
this.securityService = securityService;
+
+ // Redirect admin users to admin dashboard
+ if (securityService.hasRole("ADMIN")) {
+ getUI().ifPresent(ui -> ui.navigate("admin-dashboard"));
+ return;
+ }
+
setSizeFull();
setPadding(false);
setSpacing(false);
diff --git a/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java b/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java
index 0295ed7..780ccd2 100644
--- a/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java
+++ b/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java
@@ -115,7 +115,13 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
wrappedSession.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
SecurityContextHolder.getContext());
}
- UI.getCurrent().getPage().setLocation("/dashboard");
+ // Check if user is admin and redirect accordingly
+ if (auth.getAuthorities().stream()
+ .anyMatch(authority -> authority.getAuthority().equals("ROLE_ADMIN"))) {
+ UI.getCurrent().getPage().setLocation("/admin-dashboard");
+ } else {
+ UI.getCurrent().getPage().setLocation("/dashboard");
+ }
}
} catch (Exception ex) {
loginForm.setError(true);
@@ -152,7 +158,13 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
}
this.pendingAuth = null;
// Full reload, damit der neue SecurityContext im UI sicher greift
- UI.getCurrent().getPage().setLocation("/dashboard");
+ // Check if user is admin and redirect accordingly
+ if (SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream()
+ .anyMatch(auth -> auth.getAuthority().equals("ROLE_ADMIN"))) {
+ UI.getCurrent().getPage().setLocation("/admin-dashboard");
+ } else {
+ UI.getCurrent().getPage().setLocation("/dashboard");
+ }
}
@Override
diff --git a/src/main/java/de/assecutor/votianlt/repository/TaskRepository.java b/src/main/java/de/assecutor/votianlt/repository/TaskRepository.java
index c6a8b09..3587e12 100644
--- a/src/main/java/de/assecutor/votianlt/repository/TaskRepository.java
+++ b/src/main/java/de/assecutor/votianlt/repository/TaskRepository.java
@@ -9,6 +9,11 @@ import java.util.List;
public interface TaskRepository extends MongoRepository {
List findByJobIdOrderByTaskOrderAsc(ObjectId jobId);
+ /**
+ * Count tasks by completion status
+ */
+ long countByCompleted(boolean completed);
+
// Deprecated - use findByJobIdOrderByTaskOrderAsc instead
@Deprecated
default List findByJobId(ObjectId jobId) {