env(admin): per-environment color field + V2 migration

- V2__add_environment_color.sql adds a CHECK-constrained VARCHAR color column (default 'slate'); existing rows backfill to slate.
- Environment record + EnvironmentColor constants (8 preset values) flow through repository, service, and admin API.
- UpdateEnvironmentRequest.color nullable: null preserves existing; unknown values → 400.
- ITs cover valid / invalid / null-preserves behaviour; existing Environment constructor call-sites updated with the new color arg.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-22 19:24:30 +02:00
parent 88b003d4f0
commit c2eab71a31
15 changed files with 147 additions and 18 deletions

View File

@@ -12,5 +12,6 @@ public record Environment(
boolean enabled,
Map<String, Object> defaultContainerConfig,
Integer jarRetentionCount,
String color,
Instant createdAt
) {}

View File

@@ -0,0 +1,28 @@
package com.cameleer.server.core.runtime;
import java.util.Set;
/**
* Preset palette for the per-environment UI color indicator. Stored as a plain
* lowercase string on {@link Environment#color()}. The eight values are
* CHECK-constrained in PostgreSQL (V2 migration) and validated again here on
* the write path so the controller can return a 400 with a readable message.
*
* <p>Unknown values are silently tolerated on read (the UI falls back to
* {@link #DEFAULT}), so a manual DB tweak won't break rendering — but the API
* refuses to persist anything outside this set.
*/
public final class EnvironmentColor {
public static final String DEFAULT = "slate";
public static final Set<String> VALUES = Set.of(
"slate", "red", "amber", "green", "teal", "blue", "purple", "pink"
);
private EnvironmentColor() {}
public static boolean isValid(String color) {
return color != null && VALUES.contains(color);
}
}

View File

@@ -10,7 +10,7 @@ public interface EnvironmentRepository {
Optional<Environment> findById(UUID id);
Optional<Environment> findBySlug(String slug);
UUID create(String slug, String displayName, boolean production);
void update(UUID id, String displayName, boolean production, boolean enabled);
void update(UUID id, String displayName, boolean production, boolean enabled, String color);
void updateDefaultContainerConfig(UUID id, Map<String, Object> defaultContainerConfig);
void updateJarRetentionCount(UUID id, Integer jarRetentionCount);
void delete(UUID id);

View File

@@ -43,9 +43,17 @@ public class EnvironmentService {
return repo.create(slug, displayName, production);
}
public void update(UUID id, String displayName, boolean production, boolean enabled) {
/**
* Update mutable environment fields. Color is validated against
* {@link EnvironmentColor#VALUES}. Unknown colors raise
* {@link IllegalArgumentException}; the controller maps that to 400.
*/
public void update(UUID id, String displayName, boolean production, boolean enabled, String color) {
getById(id); // verify exists
repo.update(id, displayName, production, enabled);
if (!EnvironmentColor.isValid(color)) {
throw new IllegalArgumentException("unknown environment color: " + color);
}
repo.update(id, displayName, production, enabled, color);
}
public void updateDefaultContainerConfig(UUID id, Map<String, Object> defaultContainerConfig) {