diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml
index ab5c177..68bdf98 100755
--- a/.gitea/workflows/build.yml
+++ b/.gitea/workflows/build.yml
@@ -14,7 +14,7 @@ jobs:
build:
runs-on: site-build
env:
- APP_HOME: /home/zaine/master-folder/projects/java_projects/org_backend
+ APP_HOME: /home/zaine/master-folder/org-platform/org_backend
SERVICE_NAME: org-backend.service
SUDO_PASSWORD: ${{ secrets.SUDO_PASSWORD }}
diff --git a/README.md b/README.md
index cf998de..11cd9ca 100755
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ service, and restarts it.
The service runs the stable build artifact at:
```text
-/home/zaine/master-folder/projects/java_projects/org_backend/target/org-backend.jar
+/home/zaine/master-folder/org-platform/org_backend/target/org-backend.jar
```
Runtime configuration is loaded by systemd from:
diff --git a/backups/systemd-20260604-121946/org-backend.service.permanent-copy b/backups/systemd-20260604-121946/org-backend.service.permanent-copy
new file mode 100755
index 0000000..9d6c380
--- /dev/null
+++ b/backups/systemd-20260604-121946/org-backend.service.permanent-copy
@@ -0,0 +1,23 @@
+[Unit]
+Description=Org Backend Spring Boot App
+After=network.target
+
+[Service]
+User=zaine
+Group=zaine
+Environment=SPRING_PROFILES_ACTIVE=prod
+EnvironmentFile=/etc/org-backend/org-backend.env
+
+WorkingDirectory=/home/zaine/master-folder/projects/java_projects/org_backend
+
+ExecStart=/usr/bin/java -jar /home/zaine/master-folder/projects/java_projects/org_backend/target/org-backend.jar
+
+SuccessExitStatus=143
+Restart=always
+RestartSec=5
+
+# Optional: limit memory
+# Environment="JAVA_OPTS=-Xms256m -Xmx1g"
+
+[Install]
+WantedBy=multi-user.target
diff --git a/backups/systemd-20260604-121946/org-backend.service.symlink-target b/backups/systemd-20260604-121946/org-backend.service.symlink-target
new file mode 100755
index 0000000..116d0c8
--- /dev/null
+++ b/backups/systemd-20260604-121946/org-backend.service.symlink-target
@@ -0,0 +1 @@
+/home/zaine/.cache/act/2b629be6fbcb71eb/hostexecutor/misc/org-backend.service
diff --git a/backups/systemd-20260604-121946/org-backend.service.wants-symlink-target b/backups/systemd-20260604-121946/org-backend.service.wants-symlink-target
new file mode 100755
index 0000000..116d0c8
--- /dev/null
+++ b/backups/systemd-20260604-121946/org-backend.service.wants-symlink-target
@@ -0,0 +1 @@
+/home/zaine/.cache/act/2b629be6fbcb71eb/hostexecutor/misc/org-backend.service
diff --git a/backups/systemd-20260604-121946/summary.txt b/backups/systemd-20260604-121946/summary.txt
new file mode 100755
index 0000000..37c0b94
--- /dev/null
+++ b/backups/systemd-20260604-121946/summary.txt
@@ -0,0 +1,6 @@
+timestamp=20260604-121946
+unit=/etc/systemd/system/org-backend.service
+unit_target=/home/zaine/.cache/act/2b629be6fbcb71eb/hostexecutor/misc/org-backend.service
+wants=/etc/systemd/system/multi-user.target.wants/org-backend.service
+wants_target=/home/zaine/.cache/act/2b629be6fbcb71eb/hostexecutor/misc/org-backend.service
+permanent=/home/zaine/master-folder/projects/java_projects/org_backend/misc/org-backend.service
diff --git a/misc/org-backend.env.example b/misc/org-backend.env.example
new file mode 100755
index 0000000..e11a249
--- /dev/null
+++ b/misc/org-backend.env.example
@@ -0,0 +1,31 @@
+# Copy to /etc/org-backend/org-backend.env (systemd EnvironmentFile)
+# Paths must use org-platform/ after Phase 1 reorg — not org_files/
+
+SERVER_PORT=9010
+SPRING_PROFILES_ACTIVE=prod
+SPRING_DATASOURCE_URL=jdbc:postgresql://127.0.0.1:5432/org_web
+SPRING_DATASOURCE_USERNAME=zaine
+SPRING_DATASOURCE_PASSWORD=change-me
+SPRING_DATASOURCE_DRIVER_CLASS_NAME=org.postgresql.Driver
+SPRING_JPA_SHOW_SQL=false
+
+CALIBRE_DB_PATH=/home/zaine/master-folder/projects/calibre/library/metadata.db
+JWT_SECRET=change-me-to-a-long-random-secret-at-least-32-characters
+JWT_EXPIRATION_MS=86400000
+
+ZONE_BUILD_DIR=/home/zaine/master-folder/org-platform/org_web
+ZONE_BUILD_LOG=/home/zaine/master-folder/org-platform/org_web/org-web-build.log
+ZONE_MANIFEST_PATH=/home/zaine/master-folder/org-platform/zone/data/manifest.json
+ZONE_AUTHORING_URL=http://127.0.0.1:8765
+ZONE_WATCHER_UNIT=watcher.service
+ZONE_SCRIPTS_LOG_DIR=/home/zaine/logs
+
+EMACS_RUN_DIR=/home/zaine
+EMACS_RUN_LOG=/home/zaine/logs/emacs.log
+COMBINED_RUN_LOG=/home/zaine/logs/combined.log
+
+PLAY_RPG_SAVE_DIR=/home/zaine/master-folder/org-platform/org_backend/data/rpg-saves
+
+# Leave unset on private zone — timesheet API read/write without credentials
+# ORG_BACKEND_API_KEY=
+TIMESHEET_AUTH_REQUIRED=false
diff --git a/misc/org-backend.service b/misc/org-backend.service
index 9d6c380..c76d5a0 100755
--- a/misc/org-backend.service
+++ b/misc/org-backend.service
@@ -8,9 +8,9 @@ Group=zaine
Environment=SPRING_PROFILES_ACTIVE=prod
EnvironmentFile=/etc/org-backend/org-backend.env
-WorkingDirectory=/home/zaine/master-folder/projects/java_projects/org_backend
+WorkingDirectory=/home/zaine/master-folder/org-platform/org_backend
-ExecStart=/usr/bin/java -jar /home/zaine/master-folder/projects/java_projects/org_backend/target/org-backend.jar
+ExecStart=/usr/bin/java -jar /home/zaine/master-folder/org-platform/org_backend/target/org-backend.jar
SuccessExitStatus=143
Restart=always
diff --git a/pom.xml b/pom.xml
index 3a2f795..a4487b3 100755
--- a/pom.xml
+++ b/pom.xml
@@ -44,6 +44,12 @@
postgresql
+
+
+ org.flywaydb
+ flyway-core
+
+
org.xerial
sqlite-jdbc
diff --git a/scripts/deploy-systemd.sh b/scripts/deploy-systemd.sh
index 374a9a8..e48c926 100755
--- a/scripts/deploy-systemd.sh
+++ b/scripts/deploy-systemd.sh
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
-APP_HOME="${APP_HOME:-/home/zaine/master-folder/projects/java_projects/org_backend}"
+APP_HOME="${APP_HOME:-/home/zaine/master-folder/org-platform/org_backend}"
SERVICE_NAME="${SERVICE_NAME:-org-backend.service}"
SYSTEMCTL_TIMEOUT="${SYSTEMCTL_TIMEOUT:-120}"
ENV_FILE="${ENV_FILE:-/etc/org-backend/org-backend.env}"
diff --git a/src/main/java/org/zaine/app/config/RestClientConfig.java b/src/main/java/org/zaine/app/config/RestClientConfig.java
new file mode 100755
index 0000000..9199e84
--- /dev/null
+++ b/src/main/java/org/zaine/app/config/RestClientConfig.java
@@ -0,0 +1,14 @@
+package org.zaine.app.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+public class RestClientConfig {
+
+ @Bean
+ public RestTemplate restTemplate() {
+ return new RestTemplate();
+ }
+}
diff --git a/src/main/java/org/zaine/app/controller/zone/BuildController.java b/src/main/java/org/zaine/app/controller/zone/BuildController.java
index 20ead76..2ca4e01 100755
--- a/src/main/java/org/zaine/app/controller/zone/BuildController.java
+++ b/src/main/java/org/zaine/app/controller/zone/BuildController.java
@@ -12,14 +12,15 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.Map;
-import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
+import org.zaine.app.service.BuildRunStateService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
@@ -58,28 +59,15 @@ public class BuildController {
Map.entry("37", "ansi-white")
);
- /* =========================================================
- BUILD STATE
- ========================================================= */
+ private final BuildRunStateService buildRunState;
- private final AtomicBoolean webBuildRunning = new AtomicBoolean(false);
- private final AtomicBoolean roamBuildRunning = new AtomicBoolean(false);
- private final AtomicBoolean emacsRunning = new AtomicBoolean(false);
- private final AtomicBoolean combinedRunning = new AtomicBoolean(false);
+ private final AtomicReference webProcess = new AtomicReference<>();
+ private final AtomicReference emacsProcess = new AtomicReference<>();
- private final AtomicReference webProcess = new AtomicReference<>();
- private final AtomicReference roamProcess = new AtomicReference<>();
- private final AtomicReference emacsProcess = new AtomicReference<>();
- private final AtomicReference combinedProcess = new AtomicReference<>();
-
- private volatile Instant emacsLastRun;
- private volatile Integer emacsLastExitCode;
- private volatile Instant webLastRun;
- private volatile Integer webLastExitCode;
- private volatile Instant roamLastRun;
- private volatile Integer roamLastExitCode;
- private volatile Instant combinedLastRun;
- private volatile Integer combinedLastExitCode;
+ @Autowired
+ public BuildController(BuildRunStateService buildRunState) {
+ this.buildRunState = buildRunState;
+ }
/* =========================================================
CONFIG
@@ -87,8 +75,6 @@ public class BuildController {
@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;
@Value("${emacs.run.dir}") private String emacsRunDirectory;
@Value("${emacs.run.log}") private String emacsRunLogFile;
@Value("${combined.run.log}") private String combinedRunLogFile;
@@ -143,7 +129,7 @@ public class BuildController {
})
@PostMapping("/build-web")
public ResponseEntity triggerWebBuild() {
- return startBuild(webBuildDirectory, webBuildLogFile, webBuildRunning, webProcess, true);
+ return startBuild(webBuildDirectory, webBuildLogFile, true, webProcess);
}
@Operation(
@@ -156,7 +142,7 @@ public class BuildController {
})
@DeleteMapping("/build-web")
public ResponseEntity cancelWebBuild() {
- return killProcess(webProcess, webBuildRunning, "web build");
+ return killProcess(webProcess, true, "web build");
}
@Operation(
@@ -169,7 +155,8 @@ public class BuildController {
})
@GetMapping("/build-web/status")
public ResponseEntity getWebBuildStatus() {
- return ResponseEntity.ok(new BuildStatus(webBuildRunning.get(), webLastRun, webLastExitCode));
+ var s = buildRunState.webStatus();
+ return ResponseEntity.ok(new BuildStatus(s.running(), s.lastRun(), s.lastExitCode()));
}
@Operation(
@@ -185,62 +172,6 @@ public class BuildController {
return streamLogs(webBuildLogFile);
}
- /* =========================================================
- ORG ROAM BUILD
- ========================================================= */
-
- @Operation(
- summary = "Trigger org-roam build",
- description = "Triggers a build of the org-roam application"
- )
- @ApiResponses(value = {
- @ApiResponse(responseCode = "200", description = "Build triggered successfully"),
- @ApiResponse(responseCode = "500", description = "Internal server error")
- })
- @PostMapping("/build-roam")
- public ResponseEntity triggerRoamBuild() {
- return startBuild(roamBuildDirectory, roamBuildLogFile, roamBuildRunning, roamProcess, false);
- }
-
- @Operation(
- summary = "Cancel org-roam build",
- description = "Cancels a running org-roam build"
- )
- @ApiResponses(value = {
- @ApiResponse(responseCode = "200", description = "Build cancelled successfully"),
- @ApiResponse(responseCode = "500", description = "Internal server error")
- })
- @DeleteMapping("/build-roam")
- public ResponseEntity cancelRoamBuild() {
- return killProcess(roamProcess, roamBuildRunning, "roam build");
- }
-
- @Operation(
- summary = "Get org-roam build status",
- description = "Returns the status of a running org-roam build"
- )
- @ApiResponses(value = {
- @ApiResponse(responseCode = "200", description = "Build status retrieved successfully"),
- @ApiResponse(responseCode = "500", description = "Internal server error")
- })
- @GetMapping("/build-roam/status")
- public ResponseEntity getRoamBuildStatus() {
- return ResponseEntity.ok(new BuildStatus(roamBuildRunning.get(), roamLastRun, roamLastExitCode));
- }
-
- @Operation(
- summary = "Stream org-roam build logs",
- description = "Streams the logs of a running org-roam build"
- )
- @ApiResponses(value = {
- @ApiResponse(responseCode = "200", description = "Logs streamed successfully"),
- @ApiResponse(responseCode = "500", description = "Internal server error")
- })
- @GetMapping("/build-roam/logs")
- public SseEmitter streamRoamLogs() {
- return streamLogs(roamBuildLogFile);
- }
-
/* =========================================================
EMACS RERUN
========================================================= */
@@ -256,7 +187,7 @@ public class BuildController {
@PostMapping("/rerun-emacs")
public ResponseEntity triggerEmacs() {
log.info("Triggering Emacs command");
- return startCommand(emacsRunDirectory, emacsRunLogFile, emacsRunning, emacsProcess);
+ return startCommand(emacsRunDirectory, emacsRunLogFile, emacsProcess);
}
@Operation(
@@ -269,7 +200,7 @@ public class BuildController {
})
@DeleteMapping("/rerun-emacs")
public ResponseEntity cancelEmacs() {
- return killProcess(emacsProcess, emacsRunning, "emacs");
+ return killProcess(emacsProcess, false, "emacs");
}
@Operation(
@@ -282,7 +213,8 @@ public class BuildController {
})
@GetMapping("/rerun-emacs/status")
public ResponseEntity getEmacsStatus() {
- return ResponseEntity.ok(new BuildStatus(emacsRunning.get(), emacsLastRun, emacsLastExitCode));
+ var s = buildRunState.emacsStatus();
+ return ResponseEntity.ok(new BuildStatus(s.running(), s.lastRun(), s.lastExitCode()));
}
@Operation(
@@ -298,127 +230,6 @@ public class BuildController {
return streamLogs(emacsRunLogFile);
}
- /* =========================================================
- COMBINED: EMACS + ROAM (sequential)
- ========================================================= */
-
- @Operation(
- summary = "Trigger combined run",
- description = "Triggers a combined run of Emacs and org-roam"
- )
- @ApiResponses(value = {
- @ApiResponse(responseCode = "200", description = "Combined run triggered successfully"),
- @ApiResponse(responseCode = "500", description = "Internal server error")
- })
- @PostMapping("/run-combined")
- public ResponseEntity triggerCombined() {
- if (!combinedRunning.compareAndSet(false, true)) {
- return ResponseEntity.status(HttpStatus.CONFLICT).body("Combined run already in progress");
- }
- if (emacsRunning.get() || roamBuildRunning.get()) {
- combinedRunning.set(false);
- return ResponseEntity.status(HttpStatus.CONFLICT)
- .body("Emacs or Roam already running independently");
- }
-
- try {
- File logFile = prepareLogFile(combinedRunLogFile);
- combinedLastRun = Instant.now();
-
- new Thread(() -> {
- try {
- appendToLog(logFile, "=== [1/2] Starting Emacs ===\n");
-
- ProcessBuilder emacsPb = new ProcessBuilder(
- "bash", "-c",
- "TERM=vt100 /usr/bin/timeout 10 /usr/bin/script -q -c \"emacs -nw\" /dev/null"
- );
- emacsPb.directory(Path.of(emacsRunDirectory).toFile());
- emacsPb.redirectErrorStream(true);
- emacsPb.redirectOutput(ProcessBuilder.Redirect.appendTo(logFile));
-
- Process ep = emacsPb.start();
- combinedProcess.set(ep);
- int emacsExit = ep.waitFor();
- emacsLastExitCode = emacsExit;
-
- appendToLog(logFile, "\n=== Emacs exited (code " + emacsExit + ") ===\n");
- appendToLog(logFile, "=== [2/2] Starting Roam build ===\n");
-
- ProcessBuilder roamPb = new ProcessBuilder("make");
- roamPb.directory(Path.of(roamBuildDirectory).toFile());
- roamPb.redirectErrorStream(true);
- roamPb.redirectOutput(ProcessBuilder.Redirect.appendTo(logFile));
-
- Process rp = roamPb.start();
- combinedProcess.set(rp);
- int roamExit = rp.waitFor();
- roamLastExitCode = roamExit;
- roamLastRun = Instant.now();
-
- int finalCode = (emacsExit == 0 || emacsExit == 124) && roamExit == 0 ? 0 : 1;
- combinedLastExitCode = finalCode;
-
- appendToLog(logFile, "\n=== Roam build exited (code " + roamExit + ") ===\n");
- appendToLog(logFile, "=== Combined run complete (exit " + finalCode + ") ===\n");
-
- } catch (Exception e) {
- combinedLastExitCode = 1;
- log.error("Combined run failed", e);
- } finally {
- combinedRunning.set(false);
- combinedProcess.set(null);
- }
- }).start();
-
- return ResponseEntity.ok("Combined run started");
-
- } catch (IOException e) {
- combinedRunning.set(false);
- return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
- .body("Failed to start: " + e.getMessage());
- }
- }
-
- @Operation(
- summary = "Cancel combined run",
- description = "Cancels a combined run of Emacs and org-roam"
- )
- @ApiResponses(value = {
- @ApiResponse(responseCode = "200", description = "Combined run cancelled successfully"),
- @ApiResponse(responseCode = "500", description = "Internal server error")
- })
- @DeleteMapping("/run-combined")
- public ResponseEntity cancelCombined() {
- return killProcess(combinedProcess, combinedRunning, "combined run");
- }
-
- @Operation(
- summary = "Get combined run status",
- description = "Returns the status of a running combined run"
- )
- @ApiResponses(value = {
- @ApiResponse(responseCode = "200", description = "Combined run status retrieved successfully"),
- @ApiResponse(responseCode = "500", description = "Internal server error")
- })
- @GetMapping("/run-combined/status")
- public ResponseEntity getCombinedStatus() {
- return ResponseEntity.ok(new BuildStatus(combinedRunning.get(), combinedLastRun, combinedLastExitCode));
- }
-
- @Operation(
- summary = "Get combined run logs",
- description = "Returns the logs of a running combined run"
- )
- @ApiResponses(value = {
- @ApiResponse(responseCode = "200", description = "Combined run logs retrieved successfully"),
- @ApiResponse(responseCode = "500", description = "Internal server error")
- })
- @GetMapping("/run-combined/logs")
- public SseEmitter streamCombinedLogs() {
- return streamLogs(combinedRunLogFile);
- }
-
/* =========================================================
LAST RUN TIMESTAMPS
========================================================= */
@@ -433,12 +244,7 @@ public class BuildController {
})
@GetMapping("/last-runs")
public ResponseEntity