From 6ccef04914a6bc543b26b19a7c0389b227585e69 Mon Sep 17 00:00:00 2001 From: litoral05 Date: Tue, 9 Jun 2026 15:35:26 +0100 Subject: [PATCH] Multi workspace config - still not finished --- .../backend/charts/ChartWorkspace.java | 39 ++- .../charts/ChartWorkspaceController.java | 52 ++- .../charts/ChartWorkspaceRepository.java | 13 +- .../backend/charts/ChartWorkspaceService.java | 318 ++++++++++++++++-- .../charts/dto/ChartWorkspaceRequest.java | 5 +- .../charts/dto/ChartWorkspaceResponse.java | 5 +- .../backend/vnc/rfb/GreetClient.java | 60 ---- .../backend/vnc/rfb/RfbProtoDesktop.java | 2 +- src/main/resources/application.yaml | 2 +- .../V6__chart_workspace_multi_workspace.sql | 59 ++++ 10 files changed, 456 insertions(+), 99 deletions(-) delete mode 100644 src/main/java/com/litoralregas/backend/vnc/rfb/GreetClient.java create mode 100644 src/main/resources/db/migration/V6__chart_workspace_multi_workspace.sql diff --git a/src/main/java/com/litoralregas/backend/charts/ChartWorkspace.java b/src/main/java/com/litoralregas/backend/charts/ChartWorkspace.java index 1b62e80..675a2be 100644 --- a/src/main/java/com/litoralregas/backend/charts/ChartWorkspace.java +++ b/src/main/java/com/litoralregas/backend/charts/ChartWorkspace.java @@ -13,9 +13,18 @@ public class ChartWorkspace { private Integer id; @Enumerated(EnumType.STRING) - @Column(nullable = false, unique = true) + @Column(nullable = false) private ChartWorkspaceScope scope; + @Column(nullable = false, length = 120) + private String name; + + @Column(name = "sort_order", nullable = false) + private Integer sortOrder = 0; + + @Column(name = "is_default", nullable = false) + private Boolean defaultWorkspace = false; + @Column(name = "layout_mode", nullable = false) private String layoutMode; @@ -33,10 +42,12 @@ public class ChartWorkspace { public ChartWorkspace( ChartWorkspaceScope scope, + String name, String layoutMode, String chartsJson ) { this.scope = scope; + this.name = name; this.layoutMode = layoutMode; this.chartsJson = chartsJson; } @@ -71,6 +82,30 @@ public class ChartWorkspace { this.scope = scope; } + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getSortOrder() { + return sortOrder; + } + + public void setSortOrder(Integer sortOrder) { + this.sortOrder = sortOrder; + } + + public Boolean getDefaultWorkspace() { + return defaultWorkspace; + } + + public void setDefaultWorkspace(Boolean defaultWorkspace) { + this.defaultWorkspace = defaultWorkspace; + } + public String getLayoutMode() { return layoutMode; } @@ -94,4 +129,4 @@ public class ChartWorkspace { public Instant getUpdatedAt() { return updatedAt; } -} \ No newline at end of file +} diff --git a/src/main/java/com/litoralregas/backend/charts/ChartWorkspaceController.java b/src/main/java/com/litoralregas/backend/charts/ChartWorkspaceController.java index ea247eb..1c57727 100644 --- a/src/main/java/com/litoralregas/backend/charts/ChartWorkspaceController.java +++ b/src/main/java/com/litoralregas/backend/charts/ChartWorkspaceController.java @@ -4,6 +4,8 @@ import com.litoralregas.backend.charts.dto.ChartWorkspaceRequest; import com.litoralregas.backend.charts.dto.ChartWorkspaceResponse; import org.springframework.web.bind.annotation.*; +import java.util.List; + @RestController @RequestMapping("/api/chart-workspaces") public class ChartWorkspaceController { @@ -16,6 +18,54 @@ public class ChartWorkspaceController { this.service = service; } + @GetMapping + public List listWorkspaces( + @RequestParam ChartWorkspaceScope scope + ) { + + return service.listWorkspaces(scope); + } + + @PostMapping + public ChartWorkspaceResponse createWorkspace( + @RequestParam ChartWorkspaceScope scope, + @RequestBody ChartWorkspaceRequest request + ) { + + return service.createWorkspace( + scope, + request + ); + } + + @GetMapping("/id/{id}") + public ChartWorkspaceResponse getWorkspaceById( + @PathVariable Integer id + ) { + + return service.getWorkspaceById(id); + } + + @PutMapping("/id/{id}") + public ChartWorkspaceResponse updateWorkspaceById( + @PathVariable Integer id, + @RequestBody ChartWorkspaceRequest request + ) { + + return service.updateWorkspace( + id, + request + ); + } + + @DeleteMapping("/id/{id}") + public void deleteWorkspaceById( + @PathVariable Integer id + ) { + + service.deleteWorkspace(id); + } + @GetMapping("/{scope}") public ChartWorkspaceResponse getWorkspace( @PathVariable ChartWorkspaceScope scope @@ -35,4 +85,4 @@ public class ChartWorkspaceController { request ); } -} \ No newline at end of file +} diff --git a/src/main/java/com/litoralregas/backend/charts/ChartWorkspaceRepository.java b/src/main/java/com/litoralregas/backend/charts/ChartWorkspaceRepository.java index 1ea458b..51f525a 100644 --- a/src/main/java/com/litoralregas/backend/charts/ChartWorkspaceRepository.java +++ b/src/main/java/com/litoralregas/backend/charts/ChartWorkspaceRepository.java @@ -2,12 +2,21 @@ package com.litoralregas.backend.charts; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.Optional; public interface ChartWorkspaceRepository extends JpaRepository { - Optional findByScope( + List findAllByScopeOrderBySortOrderAscIdAsc( ChartWorkspaceScope scope ); -} \ No newline at end of file + + Optional findFirstByScopeAndDefaultWorkspaceTrue( + ChartWorkspaceScope scope + ); + + long countByScope( + ChartWorkspaceScope scope + ); +} diff --git a/src/main/java/com/litoralregas/backend/charts/ChartWorkspaceService.java b/src/main/java/com/litoralregas/backend/charts/ChartWorkspaceService.java index f2c2623..7c2410a 100644 --- a/src/main/java/com/litoralregas/backend/charts/ChartWorkspaceService.java +++ b/src/main/java/com/litoralregas/backend/charts/ChartWorkspaceService.java @@ -1,19 +1,31 @@ package com.litoralregas.backend.charts; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.litoralregas.backend.charts.dto.ChartWorkspaceRequest; import com.litoralregas.backend.charts.dto.ChartWorkspaceResponse; import jakarta.transaction.Transactional; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; @Service public class ChartWorkspaceService { + private static final int MAX_WORKSPACES_PER_SCOPE = 10; + private static final int MAX_CHARTS_PER_WORKSPACE = 10; + private final ChartWorkspaceRepository repository; + private final ObjectMapper objectMapper; public ChartWorkspaceService( - ChartWorkspaceRepository repository + ChartWorkspaceRepository repository, + ObjectMapper objectMapper ) { this.repository = repository; + this.objectMapper = objectMapper; } @Transactional @@ -21,24 +33,11 @@ public class ChartWorkspaceService { ChartWorkspaceScope scope, ChartWorkspaceRequest request ) { - ChartWorkspace workspace = - repository.findByScope(scope) - .orElseGet(() -> - new ChartWorkspace( - scope, - request.layoutMode(), - request.chartsJson() - ) - ); + repository.findFirstByScopeAndDefaultWorkspaceTrue(scope) + .orElseGet(() -> createDefaultWorkspace(scope)); - workspace.setLayoutMode( - request.layoutMode() - ); - - workspace.setChartsJson( - request.chartsJson() - ); + applyRequest(workspace, request, true); ChartWorkspace saved = repository.save(workspace); @@ -49,19 +48,275 @@ public class ChartWorkspaceService { public ChartWorkspaceResponse getWorkspace( ChartWorkspaceScope scope ) { - ChartWorkspace workspace = - repository.findByScope(scope) - .orElseGet(() -> - repository.save( - new ChartWorkspace( - scope, - "fourGrid", - "[]" - ) - ) - ); + return toResponse(getOrCreateDefaultWorkspace(scope)); + } - return toResponse(workspace); + public List listWorkspaces( + ChartWorkspaceScope scope + ) { + ensureDefaultWorkspace(scope); + + return repository.findAllByScopeOrderBySortOrderAscIdAsc(scope) + .stream() + .map(this::toResponse) + .toList(); + } + + public ChartWorkspaceResponse getWorkspaceById( + Integer id + ) { + return toResponse(requireWorkspace(id)); + } + + @Transactional + public ChartWorkspaceResponse createWorkspace( + ChartWorkspaceScope scope, + ChartWorkspaceRequest request + ) { + if (repository.countByScope(scope) >= MAX_WORKSPACES_PER_SCOPE) { + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "Maximum workspaces reached for this scope." + ); + } + + ChartWorkspace workspace = + new ChartWorkspace( + scope, + normalizeName(request.name(), "Novo Workspace"), + normalizeLayoutMode(request.layoutMode()), + normalizeChartsJson(request.chartsJson()) + ); + + workspace.setSortOrder( + request.sortOrder() == null + ? nextSortOrder(scope) + : request.sortOrder() + ); + + workspace.setDefaultWorkspace(false); + + if (Boolean.TRUE.equals(request.defaultWorkspace())) { + setDefaultWorkspace(workspace); + } + + ChartWorkspace saved = + repository.save(workspace); + + return toResponse(saved); + } + + @Transactional + public ChartWorkspaceResponse updateWorkspace( + Integer id, + ChartWorkspaceRequest request + ) { + ChartWorkspace workspace = + requireWorkspace(id); + + applyRequest(workspace, request, false); + + ChartWorkspace saved = + repository.save(workspace); + + return toResponse(saved); + } + + @Transactional + public void deleteWorkspace( + Integer id + ) { + ChartWorkspace workspace = + requireWorkspace(id); + + repository.delete(workspace); + + if (Boolean.TRUE.equals(workspace.getDefaultWorkspace())) { + repository.findAllByScopeOrderBySortOrderAscIdAsc(workspace.getScope()) + .stream() + .findFirst() + .ifPresent(this::setDefaultWorkspace); + } + } + + private ChartWorkspace getOrCreateDefaultWorkspace( + ChartWorkspaceScope scope + ) { + return repository.findFirstByScopeAndDefaultWorkspaceTrue(scope) + .orElseGet(() -> + repository.findAllByScopeOrderBySortOrderAscIdAsc(scope) + .stream() + .findFirst() + .map(workspace -> { + setDefaultWorkspace(workspace); + return repository.save(workspace); + }) + .orElseGet(() -> + repository.save(createDefaultWorkspace(scope)) + ) + ); + } + + private void ensureDefaultWorkspace( + ChartWorkspaceScope scope + ) { + getOrCreateDefaultWorkspace(scope); + } + + private ChartWorkspace createDefaultWorkspace( + ChartWorkspaceScope scope + ) { + ChartWorkspace workspace = + new ChartWorkspace( + scope, + "Workspace principal", + "fourGrid", + "[]" + ); + + workspace.setDefaultWorkspace(true); + workspace.setSortOrder(0); + + return workspace; + } + + private ChartWorkspace requireWorkspace( + Integer id + ) { + return repository.findById(id) + .orElseThrow(() -> + new ResponseStatusException( + HttpStatus.NOT_FOUND, + "Workspace not found." + ) + ); + } + + private void applyRequest( + ChartWorkspace workspace, + ChartWorkspaceRequest request, + boolean legacyDefaultUpdate + ) { + if (request.name() != null || legacyDefaultUpdate) { + workspace.setName( + normalizeName(request.name(), workspace.getName()) + ); + } + + if (request.sortOrder() != null) { + workspace.setSortOrder(request.sortOrder()); + } + + if (Boolean.TRUE.equals(request.defaultWorkspace())) { + setDefaultWorkspace(workspace); + } else if ( + Boolean.FALSE.equals(request.defaultWorkspace()) && + !Boolean.TRUE.equals(workspace.getDefaultWorkspace()) + ) { + workspace.setDefaultWorkspace(false); + } + + workspace.setLayoutMode( + normalizeLayoutMode(request.layoutMode()) + ); + + workspace.setChartsJson( + normalizeChartsJson(request.chartsJson()) + ); + } + + private void setDefaultWorkspace( + ChartWorkspace workspace + ) { + repository.findAllByScopeOrderBySortOrderAscIdAsc(workspace.getScope()) + .forEach(candidate -> { + if (!candidate.getId().equals(workspace.getId())) { + candidate.setDefaultWorkspace(false); + } + }); + + workspace.setDefaultWorkspace(true); + } + + private int nextSortOrder( + ChartWorkspaceScope scope + ) { + return repository.findAllByScopeOrderBySortOrderAscIdAsc(scope) + .stream() + .map(ChartWorkspace::getSortOrder) + .filter(value -> value != null) + .max(Integer::compareTo) + .orElse(-1) + 1; + } + + private String normalizeName( + String name, + String fallback + ) { + String normalized = + name == null ? "" : name.trim(); + + if (normalized.isBlank()) { + return fallback == null || fallback.isBlank() + ? "Workspace" + : fallback; + } + + if (normalized.length() > 120) { + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "Workspace name is too long." + ); + } + + return normalized; + } + + private String normalizeLayoutMode( + String layoutMode + ) { + if (layoutMode == null || layoutMode.isBlank()) { + return "fourGrid"; + } + + return layoutMode; + } + + private String normalizeChartsJson( + String chartsJson + ) { + String normalized = + chartsJson == null || chartsJson.isBlank() + ? "[]" + : chartsJson; + + try { + JsonNode root = + objectMapper.readTree(normalized); + + if (!root.isArray()) { + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "chartsJson must be an array." + ); + } + + if (root.size() > MAX_CHARTS_PER_WORKSPACE) { + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "Maximum charts reached for this workspace." + ); + } + } catch (ResponseStatusException exception) { + throw exception; + } catch (Exception exception) { + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "chartsJson is invalid." + ); + } + + return normalized; } private ChartWorkspaceResponse toResponse( @@ -71,10 +326,13 @@ public class ChartWorkspaceService { return new ChartWorkspaceResponse( workspace.getId(), workspace.getScope(), + workspace.getName(), + workspace.getSortOrder(), + workspace.getDefaultWorkspace(), workspace.getLayoutMode(), workspace.getChartsJson(), workspace.getCreatedAt(), workspace.getUpdatedAt() ); } -} \ No newline at end of file +} diff --git a/src/main/java/com/litoralregas/backend/charts/dto/ChartWorkspaceRequest.java b/src/main/java/com/litoralregas/backend/charts/dto/ChartWorkspaceRequest.java index 8ef7fb8..0862192 100644 --- a/src/main/java/com/litoralregas/backend/charts/dto/ChartWorkspaceRequest.java +++ b/src/main/java/com/litoralregas/backend/charts/dto/ChartWorkspaceRequest.java @@ -1,7 +1,10 @@ package com.litoralregas.backend.charts.dto; public record ChartWorkspaceRequest( + String name, + Integer sortOrder, + Boolean defaultWorkspace, String layoutMode, String chartsJson ) { -} \ No newline at end of file +} diff --git a/src/main/java/com/litoralregas/backend/charts/dto/ChartWorkspaceResponse.java b/src/main/java/com/litoralregas/backend/charts/dto/ChartWorkspaceResponse.java index c52d64b..6ca76bc 100644 --- a/src/main/java/com/litoralregas/backend/charts/dto/ChartWorkspaceResponse.java +++ b/src/main/java/com/litoralregas/backend/charts/dto/ChartWorkspaceResponse.java @@ -7,9 +7,12 @@ import java.time.Instant; public record ChartWorkspaceResponse( Integer id, ChartWorkspaceScope scope, + String name, + Integer sortOrder, + Boolean defaultWorkspace, String layoutMode, String chartsJson, Instant createdAt, Instant updatedAt ) { -} \ No newline at end of file +} diff --git a/src/main/java/com/litoralregas/backend/vnc/rfb/GreetClient.java b/src/main/java/com/litoralregas/backend/vnc/rfb/GreetClient.java deleted file mode 100644 index 7b53991..0000000 --- a/src/main/java/com/litoralregas/backend/vnc/rfb/GreetClient.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.litoralregas.backend.vnc.rfb; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.PrintWriter; -import java.net.Socket; -import java.net.SocketTimeoutException; - -public class GreetClient implements AutoCloseable { - private Socket clientSocket; - private PrintWriter out; - private BufferedReader in; - - public void startConnection(String ip, int port) throws IOException { - clientSocket = new Socket(ip, port); - clientSocket.setSoTimeout(10000); - - out = new PrintWriter(clientSocket.getOutputStream(), true); - in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); - } - - public String sendMessage(String msg) throws IOException { - if (out == null || in == null) { - throw new IllegalStateException("Connection not started"); - } - - out.println(msg); - String response = in.readLine(); - - if (response == null) { - throw new SocketTimeoutException("No response from server"); - } - - return response; - } - - public void stopConnection() { - try { - if (in != null) in.close(); - } catch (IOException ignored) { - } - - if (out != null) { - out.close(); - } - - try { - if (clientSocket != null && !clientSocket.isClosed()) { - clientSocket.close(); - } - } catch (IOException ignored) { - } - } - - @Override - public void close() { - stopConnection(); - } -} \ No newline at end of file diff --git a/src/main/java/com/litoralregas/backend/vnc/rfb/RfbProtoDesktop.java b/src/main/java/com/litoralregas/backend/vnc/rfb/RfbProtoDesktop.java index 27d9efd..a423ea1 100644 --- a/src/main/java/com/litoralregas/backend/vnc/rfb/RfbProtoDesktop.java +++ b/src/main/java/com/litoralregas/backend/vnc/rfb/RfbProtoDesktop.java @@ -109,7 +109,7 @@ public class RfbProtoDesktop { this.sock.setKeepAlive(true); this.sock.setReuseAddress(true); this.sock.connect(new InetSocketAddress(host, port), CONNECT_TIMEOUT_MS); - this.sock.setSoTimeout(READ_TIMEOUT_MS); + this.sock.setSoTimeout(0); // Increase receive buffer — large ZRLE/Tight frames arrive in bursts. this.sock.setReceiveBufferSize(65536); diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index ad2425e..04ffb9d 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -43,7 +43,7 @@ litoralregas: modules: climate: - enabled: true + enabled: false exterior-enabled: true enabled-sites: - 1 diff --git a/src/main/resources/db/migration/V6__chart_workspace_multi_workspace.sql b/src/main/resources/db/migration/V6__chart_workspace_multi_workspace.sql new file mode 100644 index 0000000..e74eb0e --- /dev/null +++ b/src/main/resources/db/migration/V6__chart_workspace_multi_workspace.sql @@ -0,0 +1,59 @@ +ALTER TABLE chart_workspace + ADD COLUMN name VARCHAR(120) NOT NULL DEFAULT 'Workspace principal'; + +ALTER TABLE chart_workspace + ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0; + +ALTER TABLE chart_workspace + ADD COLUMN is_default BOOLEAN NOT NULL DEFAULT TRUE; + +CREATE TABLE chart_workspace_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + + scope VARCHAR(50) NOT NULL, + name VARCHAR(120) NOT NULL, + sort_order INTEGER NOT NULL DEFAULT 0, + is_default BOOLEAN NOT NULL DEFAULT FALSE, + + layout_mode VARCHAR(50) NOT NULL, + + charts_json TEXT NOT NULL, + + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL +); + +INSERT INTO chart_workspace_new ( + id, + scope, + name, + sort_order, + is_default, + layout_mode, + charts_json, + created_at, + updated_at +) +SELECT + id, + scope, + name, + sort_order, + is_default, + layout_mode, + charts_json, + created_at, + updated_at +FROM chart_workspace; + +DROP TABLE chart_workspace; + +ALTER TABLE chart_workspace_new + RENAME TO chart_workspace; + +CREATE INDEX idx_chart_workspace_scope_sort + ON chart_workspace (scope, sort_order, id); + +CREATE UNIQUE INDEX idx_chart_workspace_scope_default + ON chart_workspace (scope) + WHERE is_default = TRUE;