Multi workspace config - still not finished

This commit is contained in:
litoral05
2026-06-09 15:35:26 +01:00
parent 6ef1e83e63
commit 6ccef04914
10 changed files with 456 additions and 99 deletions
@@ -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;
}
@@ -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<ChartWorkspaceResponse> 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
@@ -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<ChartWorkspace, Integer> {
Optional<ChartWorkspace> findByScope(
List<ChartWorkspace> findAllByScopeOrderBySortOrderAscIdAsc(
ChartWorkspaceScope scope
);
Optional<ChartWorkspace> findFirstByScopeAndDefaultWorkspaceTrue(
ChartWorkspaceScope scope
);
long countByScope(
ChartWorkspaceScope scope
);
}
@@ -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
) {
return toResponse(getOrCreateDefaultWorkspace(scope));
}
public List<ChartWorkspaceResponse> 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 =
repository.findByScope(scope)
.orElseGet(() ->
repository.save(
new ChartWorkspace(
scope,
"fourGrid",
"[]"
)
)
normalizeName(request.name(), "Novo Workspace"),
normalizeLayoutMode(request.layoutMode()),
normalizeChartsJson(request.chartsJson())
);
return toResponse(workspace);
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,6 +326,9 @@ public class ChartWorkspaceService {
return new ChartWorkspaceResponse(
workspace.getId(),
workspace.getScope(),
workspace.getName(),
workspace.getSortOrder(),
workspace.getDefaultWorkspace(),
workspace.getLayoutMode(),
workspace.getChartsJson(),
workspace.getCreatedAt(),
@@ -1,6 +1,9 @@
package com.litoralregas.backend.charts.dto;
public record ChartWorkspaceRequest(
String name,
Integer sortOrder,
Boolean defaultWorkspace,
String layoutMode,
String chartsJson
) {
@@ -7,6 +7,9 @@ import java.time.Instant;
public record ChartWorkspaceResponse(
Integer id,
ChartWorkspaceScope scope,
String name,
Integer sortOrder,
Boolean defaultWorkspace,
String layoutMode,
String chartsJson,
Instant createdAt,
@@ -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();
}
}
@@ -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);
+1 -1
View File
@@ -43,7 +43,7 @@ litoralregas:
modules:
climate:
enabled: true
enabled: false
exterior-enabled: true
enabled-sites:
- 1
@@ -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;