diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/.mvn/jvm.config b/.mvn/jvm.config old mode 100644 new mode 100755 diff --git a/.mvn/maven.config b/.mvn/maven.config old mode 100644 new mode 100755 diff --git a/Dockerfile b/Dockerfile old mode 100644 new mode 100755 index 1efbdd7..a10c287 --- a/Dockerfile +++ b/Dockerfile @@ -9,13 +9,24 @@ RUN mvn -B dependency:go-offline COPY src ./src RUN mvn -B clean package -DskipTests + # -------- Runtime stage -------- FROM eclipse-temurin:21-jre +# Install build dependencies +RUN apt-get update && apt-get install -y \ + make \ + emacs-nox \ + python3 \ + python3-venv \ + python3-pip \ + git \ + && rm -rf /var/lib/apt/lists/* + WORKDIR /app COPY --from=build /app/target/*.jar app.jar EXPOSE 9010 -ENTRYPOINT ["java", "-jar", "app.jar"] +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/Makefile b/Makefile old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/pom.xml b/pom.xml old mode 100644 new mode 100755 diff --git a/src/main/java/org/zaine/app/Application.java b/src/main/java/org/zaine/app/Application.java old mode 100644 new mode 100755 diff --git a/src/main/java/org/zaine/app/controller/CommentsController.java b/src/main/java/org/zaine/app/controller/CommentsController.java old mode 100644 new mode 100755 diff --git a/src/main/java/org/zaine/app/controller/CompetenciesController.java b/src/main/java/org/zaine/app/controller/CompetenciesController.java old mode 100644 new mode 100755 diff --git a/src/main/java/org/zaine/app/controller/HealthController.java b/src/main/java/org/zaine/app/controller/HealthController.java old mode 100644 new mode 100755 index cc34ba8..98dc4e2 --- a/src/main/java/org/zaine/app/controller/HealthController.java +++ b/src/main/java/org/zaine/app/controller/HealthController.java @@ -6,7 +6,7 @@ import org.springframework.web.bind.annotation.RestController; @RestController public class HealthController { - @GetMapping("/health") + @GetMapping("/api/health") public String health() { return "OK"; } diff --git a/src/main/java/org/zaine/app/controller/NotesController.java b/src/main/java/org/zaine/app/controller/NotesController.java old mode 100644 new mode 100755 diff --git a/src/main/java/org/zaine/app/controller/zone/BuildController.java b/src/main/java/org/zaine/app/controller/zone/BuildController.java new file mode 100644 index 0000000..b266556 --- /dev/null +++ b/src/main/java/org/zaine/app/controller/zone/BuildController.java @@ -0,0 +1,237 @@ +package org.zaine.app.controller.zone; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.file.Path; +import java.time.Instant; +import java.util.concurrent.atomic.AtomicBoolean; + +@RestController +@RequestMapping("/api") + +public class BuildController { + + // Separate state for each build + private final AtomicBoolean webBuildRunning = new AtomicBoolean(false); + private final AtomicBoolean roamBuildRunning = new AtomicBoolean(false); + + private volatile Instant webLastRun; + private volatile Integer webLastExitCode; + + private volatile Instant roamLastRun; + private volatile Integer roamLastExitCode; + + private static final Logger log = + LoggerFactory.getLogger(BuildController.class); + + @Value("${zone.build.dir}") + private String webBuildDirectory; + + @Value("${zone.build.log}") + private String webBuildLogFile; + + @Value("${orgroam.build.dir}") + private String roamBuildDirectory; + + @Value("${orgroam.build.log}") + private String roamBuildLogFile; + + /* ========================================================= + WEB BUILD + ========================================================= */ + + @PostMapping("/build-web") + public ResponseEntity triggerWebBuild() { + return startBuild( + webBuildDirectory, + webBuildLogFile, + webBuildRunning, + true + ); + } + + @GetMapping("/build-web/status") + public ResponseEntity getWebBuildStatus() { + return ResponseEntity.ok( + new BuildStatus( + webBuildRunning.get(), + webLastRun, + webLastExitCode + ) + ); + } + + @GetMapping("/build-web/logs") + public SseEmitter streamWebLogs() { + return streamLogs(webBuildLogFile); + } + + /* ========================================================= + ORG ROAM BUILD + ========================================================= */ + + @PostMapping("/build-roam") + public ResponseEntity triggerRoamBuild() { + return startBuild( + roamBuildDirectory, + roamBuildLogFile, + roamBuildRunning, + false + ); + } + + @GetMapping("/build-roam/status") + public ResponseEntity getRoamBuildStatus() { + return ResponseEntity.ok( + new BuildStatus( + roamBuildRunning.get(), + roamLastRun, + roamLastExitCode + ) + ); + } + + @GetMapping("/build-roam/logs") + public SseEmitter streamRoamLogs() { + return streamLogs(roamBuildLogFile); + } + + /* ========================================================= + CORE BUILD LOGIC + ========================================================= */ + + private ResponseEntity startBuild( + String buildDir, + String logPath, + AtomicBoolean runningFlag, + boolean isWeb + ) { + + if (!runningFlag.compareAndSet(false, true)) { + return ResponseEntity.status(HttpStatus.CONFLICT) + .body("Build already running"); + } + + try { + File workingDir = Path.of(buildDir).toFile(); + File logFile = new File(logPath); + + if (logFile.getParentFile() != null) { + logFile.getParentFile().mkdirs(); + } + + if (logFile.exists()) { + logFile.delete(); + } + + logFile.createNewFile(); + + ProcessBuilder pb = new ProcessBuilder("make"); + pb.directory(workingDir); + pb.redirectErrorStream(true); + pb.redirectOutput(ProcessBuilder.Redirect.appendTo(logFile)); + + log.info("Starting build in {}", workingDir.getAbsolutePath()); + + Process process = pb.start(); + + if (isWeb) { + webLastRun = Instant.now(); + } else { + roamLastRun = Instant.now(); + } + + new Thread(() -> { + try { + int exit = process.waitFor(); + + if (isWeb) { + webLastExitCode = exit; + } else { + roamLastExitCode = exit; + } + + log.info("Build finished with exit code {}", exit); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("Build thread interrupted", e); + } finally { + runningFlag.set(false); + } + }).start(); + + return ResponseEntity.ok("Build started"); + + } catch (IOException e) { + runningFlag.set(false); + log.error("Build failed to start", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to start build: " + e.getMessage()); + } + } + + /* ========================================================= + LOG STREAMING (SSE) + ========================================================= */ + + private SseEmitter streamLogs(String logFilePath) { + + SseEmitter emitter = new SseEmitter(0L); + + new Thread(() -> { + try (RandomAccessFile file = new RandomAccessFile(logFilePath, "r")) { + + long pointer = 0; + + while (!Thread.currentThread().isInterrupted()) { + + long length = file.length(); + + if (length > pointer) { + file.seek(pointer); + + String line; + while ((line = file.readLine()) != null) { + emitter.send(SseEmitter.event().data(line)); + } + + pointer = file.getFilePointer(); + } + + Thread.sleep(1000); + } + + } catch (Exception e) { + log.info("Log stream closed"); + emitter.complete(); + } + }).start(); + + return emitter; + } + + /* ========================================================= + DTO + ========================================================= */ + + static class BuildStatus { + public boolean running; + public Instant lastRun; + public Integer lastExitCode; + + public BuildStatus(boolean running, Instant lastRun, Integer lastExitCode) { + this.running = running; + this.lastRun = lastRun; + this.lastExitCode = lastExitCode; + } + } +} diff --git a/src/main/java/org/zaine/app/dto/CompetenciesDTO.java b/src/main/java/org/zaine/app/dto/CompetenciesDTO.java old mode 100644 new mode 100755 diff --git a/src/main/java/org/zaine/app/dto/CreateCommentDTO.java b/src/main/java/org/zaine/app/dto/CreateCommentDTO.java old mode 100644 new mode 100755 diff --git a/src/main/java/org/zaine/app/dto/CreateNoteDTO.java b/src/main/java/org/zaine/app/dto/CreateNoteDTO.java old mode 100644 new mode 100755 diff --git a/src/main/java/org/zaine/app/model/Comments.java b/src/main/java/org/zaine/app/model/Comments.java old mode 100644 new mode 100755 diff --git a/src/main/java/org/zaine/app/model/Competencies.java b/src/main/java/org/zaine/app/model/Competencies.java old mode 100644 new mode 100755 diff --git a/src/main/java/org/zaine/app/model/Notes.java b/src/main/java/org/zaine/app/model/Notes.java old mode 100644 new mode 100755 index c72e277..480c261 --- a/src/main/java/org/zaine/app/model/Notes.java +++ b/src/main/java/org/zaine/app/model/Notes.java @@ -10,6 +10,7 @@ import java.time.Instant; import java.util.UUID; import com.fasterxml.jackson.annotation.JsonProperty; +import org.hibernate.annotations.CreationTimestamp; @Entity @Table(name = "public_notes") @@ -28,8 +29,10 @@ public class Notes { private String authorName; @JsonProperty("created_at") - @Column(name="created_at") - private Instant createdAt; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private Instant createdAt; public UUID getId() { diff --git a/src/main/java/org/zaine/app/repositories/CommentsRepository.java b/src/main/java/org/zaine/app/repositories/CommentsRepository.java old mode 100644 new mode 100755 diff --git a/src/main/java/org/zaine/app/repositories/CompetenciesRepository.java b/src/main/java/org/zaine/app/repositories/CompetenciesRepository.java old mode 100644 new mode 100755 diff --git a/src/main/java/org/zaine/app/repositories/NotesRepository.java b/src/main/java/org/zaine/app/repositories/NotesRepository.java old mode 100644 new mode 100755 diff --git a/src/main/java/org/zaine/app/service/CommentsService.java b/src/main/java/org/zaine/app/service/CommentsService.java old mode 100644 new mode 100755 diff --git a/src/main/java/org/zaine/app/service/CompetenciesService.java b/src/main/java/org/zaine/app/service/CompetenciesService.java old mode 100644 new mode 100755 diff --git a/src/main/java/org/zaine/app/service/NotesService.java b/src/main/java/org/zaine/app/service/NotesService.java old mode 100644 new mode 100755 diff --git a/src/test/java/org/zaine/app/ApplicationTest.java b/src/test/java/org/zaine/app/ApplicationTest.java old mode 100644 new mode 100755 diff --git a/src/test/java/org/zaine/app/service/CommentsServiceTest.java b/src/test/java/org/zaine/app/service/CommentsServiceTest.java old mode 100644 new mode 100755 diff --git a/src/test/java/org/zaine/app/service/CompetenciesServiceTest.java b/src/test/java/org/zaine/app/service/CompetenciesServiceTest.java old mode 100644 new mode 100755 diff --git a/src/test/java/org/zaine/app/service/NotesServiceTest.java b/src/test/java/org/zaine/app/service/NotesServiceTest.java old mode 100644 new mode 100755