This commit is contained in:
2026-03-22 00:19:26 +00:00
parent 81375a1acb
commit f75548d08e
10 changed files with 431 additions and 121 deletions

View File

@@ -1,18 +1,44 @@
package org.zaine.app.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import org.springframework.context.annotation.Configuration;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.responses.ApiResponse;
@Configuration
@OpenAPIDefinition(info = @Info(title = "org-backend API", version = "1.0.0"))
@SecurityScheme(
name = "bearerAuth", // referenced by name in controllers
name = "bearerAuth",
type = SecuritySchemeType.HTTP,
scheme = "bearer",
bearerFormat = "JWT"
)
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
ApiResponse notFoundResponse = new ApiResponse()
.description("Resource not found")
.content(new Content().addMediaType("application/json",
new MediaType().example("""
{
"message": "Could not find resource with URL: /api/example/123"
}
""")
));
return new OpenAPI()
.components(new Components()
.addResponses("NotFound", notFoundResponse)
);
}
}

View File

@@ -1,5 +1,7 @@
package org.zaine.app.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -7,24 +9,25 @@ import org.springframework.web.bind.annotation.RestController;
import org.zaine.app.dto.CalibreBookDTO;
import org.zaine.app.service.CalibreService;
import java.util.List;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
/**
* Exposes read-only Calibre library data to the frontend.
* Only used to populate the book picker in the Mutāla'ah modal.
*/
@RestController
@RequestMapping("/api/calibre")
@Tag(name = "Calibre", description = "Calibre API")
public class CalibreController {
@Autowired
private CalibreService calibreService;
/**
* GET /api/calibre/books
* Returns all books (id, title, authors) sorted alphabetically.
* Used to populate the Mutāla'ah session book picker.
*/
@Operation(summary = "Get all books", description = "Fetches all books from the Calibre database")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Books retrieved successfully"),
@ApiResponse(responseCode = "404", description = "No books found"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
@GetMapping("/books")
public List<CalibreBookDTO> getBooks() {
return calibreService.getAllBooks();

View File

@@ -14,31 +14,73 @@ import org.zaine.app.dto.CreateCommentDTO;
import org.zaine.app.model.Comments;
import org.zaine.app.service.CommentsService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
@RestController
@RequestMapping("/api/comments")
@Tag(name = "Comments", description = "Endpoints for managing comments")
public class CommentsController {
private static final Logger logger = System.getLogger(CommentsController.class.getName());
@Autowired
@Autowired
private CommentsService commentsService;
@Operation(
summary = "Get comments by page slug",
description = "Fetches all comments associated with a given page slug"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Comments retrieved successfully"),
@ApiResponse(responseCode = "404", description = "No comments found for this slug"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
@GetMapping("/{page_slug}")
public List<Comments> getAllCommentsBySlug(@PathVariable String page_slug) {
List<Comments> allComments = commentsService.getAllCommentsBySlug(page_slug);
return allComments;
List<Comments> allComments = commentsService.getAllCommentsBySlug(page_slug);
return allComments;
}
@Operation(
summary = "Get a comment by id",
description = "Fetches a comment by its id"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Comment retrieved successfully"),
@ApiResponse(responseCode = "404", description = "No comment found for this id"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
@GetMapping("/item/{id}")
public Comments getCommentById(@PathVariable Integer id) {
return commentsService.getCommentById(id);
return commentsService.getCommentById(id);
}
@Operation(
summary = "Get all comments",
description = "Fetches all comments from the database"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Comments retrieved successfully"),
@ApiResponse(responseCode = "404", description = "No comments found"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
@GetMapping("")
public List<Comments> getAllComments() {
return commentsService.getAllComments();
return commentsService.getAllComments();
}
@Operation(
summary = "Add a comment",
description = "Adds a comment to the database"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Comment added successfully"),
@ApiResponse(responseCode = "404", description = "No comments found"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
@PostMapping(
value = "",
consumes = "application/json"
@@ -58,12 +100,20 @@ public class CommentsController {
newComment.setParentId(dto.getParentId());
newComment.setCreatedAt(java.time.Instant.now());
logger.log(System.Logger.Level.INFO,
"Adding comment to pageSlug: " + dto.getPageSlug());
logger.log(System.Logger.Level.INFO, () -> "Adding comment to pageSlug: " + dto.getPageSlug());
commentsService.addComment(newComment);
}
@Operation(
summary = "Get a comment thread",
description = "Fetches a comment thread by its id"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Comment thread retrieved successfully"),
@ApiResponse(responseCode = "404", description = "No comment thread found for this id"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
@GetMapping("/thread/{comment_id}")
public List<Comments> getCommentThread(@PathVariable Integer comment_id) {
return commentsService.getCommentThread(comment_id);

View File

@@ -1,25 +1,40 @@
package org.zaine.app.controller;
import java.util.List;
import org.zaine.app.dto.CompetenciesDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.zaine.app.dto.CompetenciesDTO;
import org.zaine.app.model.Competencies;
import org.zaine.app.service.CompetenciesService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
@RestController
@RequestMapping("/api/competencies")
@Tag(name = "Competencies", description = "Operations about competencies")
public class CompetenciesController {
//private static final Logger logger = System.getLogger(CompetenciesController.class.getName());
@Autowired
private CompetenciesService competenciesService;
@Operation(
summary = "Get all competencies",
description = "Returns all competencies"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Competencies retrieved successfully"),
@ApiResponse(responseCode = "404", description = "No competencies found"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
@GetMapping("/items")
public List<Competencies> getAllCompetencies(
@RequestParam(value = "group", required = false) String group) {
@@ -29,11 +44,29 @@ public class CompetenciesController {
return competenciesService.getAllCompetencies();
}
@Operation(
summary = "Get a competency by id",
description = "Fetches a competency by its id"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Competency retrieved successfully"),
@ApiResponse(responseCode = "404", description = "No competency found for this id"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
@GetMapping("/item/{id}")
public Competencies getCompetencyById(@PathVariable Integer id) {
return competenciesService.getCompetencyById(id);
}
@Operation(
summary = "Update a competency state",
description = "Updates the state of a competency"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Competency state updated successfully"),
@ApiResponse(responseCode = "404", description = "No competency found for this id"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
@PostMapping(
value = "/items/{id}/state",
consumes = "application/json"

View File

@@ -1,75 +0,0 @@
package org.zaine.app.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.zaine.app.dto.MotalahSessionDTO;
import org.zaine.app.model.MotalahSession;
import org.zaine.app.service.MotalahService;
import java.lang.System.Logger;
import java.time.LocalDate;
import java.util.List;
@RestController
@RequestMapping("/api/wird/motalah")
public class MotalahController {
private static final Logger logger = System.getLogger(MotalahController.class.getName());
@Autowired
private MotalahService motalahService;
/**
* GET /api/wird/motalah
* All study sessions, newest first.
*/
@GetMapping
public List<MotalahSession> getAll() {
logger.log(Logger.Level.INFO, "Fetching all motalah sessions");
return motalahService.getAll();
}
/**
* GET /api/wird/motalah/today
* Convenience endpoint for today's sessions only.
*/
@GetMapping("/today")
public List<MotalahSession> getToday() {
return motalahService.getToday();
}
/**
* GET /api/wird/motalah/range?from=2025-01-01&to=2025-01-31
* Used by the trend chart and month-total stat.
*/
@GetMapping("/range")
public List<MotalahSession> getInRange(
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) {
return motalahService.getInRange(from, to);
}
/**
* POST /api/wird/motalah
* Body: { date, durationMinutes, bookIds, notes }
*/
@PostMapping
public ResponseEntity<MotalahSession> create(@RequestBody MotalahSessionDTO dto) {
logger.log(Logger.Level.INFO, "Creating motalah session: {0} min on {1}",
dto.getDurationMinutes(), dto.getDate());
MotalahSession saved = motalahService.create(dto);
return ResponseEntity.ok(saved);
}
/**
* DELETE /api/wird/motalah/{id}
*/
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
logger.log(Logger.Level.INFO, "Deleting motalah session id: {0}", id);
motalahService.delete(id);
return ResponseEntity.noContent().build();
}
}

View File

@@ -1,7 +1,8 @@
package org.zaine.app.controller;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -10,20 +11,41 @@ import org.springframework.web.bind.annotation.RestController;
import org.zaine.app.dto.CreateNoteDTO;
import org.zaine.app.model.Notes;
import org.zaine.app.service.NotesService;
import java.util.List;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
@RestController
@RequestMapping("/api")
@Tag(name = "Notes", description = "Operations related to notes")
public class NotesController {
@Autowired
private NotesService notesService;
@Operation(
summary = "Get all notes",
description = "Returns a list of all notes"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Notes retrieved successfully"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
@GetMapping("/notes")
public List<Notes> getAllNotes() {
return notesService.getAllNotes();
}
@Operation(
summary = "Create a note",
description = "Creates a new note"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Note created successfully"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
@PostMapping(
value = "/notes",
consumes = "application/json"
@@ -36,9 +58,5 @@ public class NotesController {
return notesService.createNote(note);
}
}

View File

@@ -7,19 +7,35 @@ import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.zaine.app.dto.MotalahSessionDTO;
import org.zaine.app.dto.WirdEntryDTO;
import org.zaine.app.model.MotalahSession;
import org.zaine.app.model.WirdEntry;
import org.zaine.app.service.MotalahService;
import org.zaine.app.service.WirdService;
import io.swagger.v3.oas.annotations.tags.Tag;
@RestController
@RequestMapping("/api/wird")
@Tag(name = "Wird", description = "Wird API")
public class WirdController {
private static final Logger logger = System.getLogger(WirdController.class.getName());
@Autowired
private WirdService wirdService;
@Autowired
private MotalahService motalahService;
/**
* GET /api/wird/entries
@@ -103,5 +119,57 @@ public class WirdController {
public List<WirdEntry> getKhatmEntries() {
return wirdService.getKhatmEntries();
}
/**
* GET /api/wird/motalah
* All study sessions, newest first.
*/
@GetMapping("/motalah")
public List<MotalahSession> getAll() {
logger.log(Logger.Level.INFO, "Fetching all motalah sessions");
return motalahService.getAll();
}
/**
* GET /api/wird/motalah/today
* Convenience endpoint for today's sessions only.
*/
@GetMapping("/motalah/today")
public List<MotalahSession> getToday() {
return motalahService.getToday();
}
/**
* GET /api/wird/motalah/range?from=2025-01-01&to=2025-01-31
* Used by the trend chart and month-total stat.
*/
@GetMapping("/motalah/range")
public List<MotalahSession> getInRange(
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) {
return motalahService.getInRange(from, to);
}
/**
* POST /api/wird/motalah
* Body: { date, durationMinutes, bookIds, notes }
*/
@PostMapping("/motalah")
public ResponseEntity<MotalahSession> create(@RequestBody MotalahSessionDTO dto) {
logger.log(Logger.Level.INFO, "Creating motalah session: {0} min on {1}",
dto.getDurationMinutes(), dto.getDate());
MotalahSession saved = motalahService.create(dto);
return ResponseEntity.ok(saved);
}
/**
* DELETE /api/wird/motalah/{id}
*/
@DeleteMapping("/motalah/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
logger.log(Logger.Level.INFO, "Deleting motalah session id: {0}", id);
motalahService.delete(id);
return ResponseEntity.noContent().build();
}
}

View File

@@ -29,21 +29,22 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
@RestController
@RequestMapping("/api")
@Tag(name = "Build", description = "Build management API")
public class BuildController {
private static final Logger log = LoggerFactory.getLogger(BuildController.class);
private static final Instant SERVER_START = Instant.now();
// ── ANSI escape sequence pattern ─────────────────────────────────────────
// Matches CSI sequences: ESC [ <params> <final-byte>
// and simple ESC sequences: ESC <single-char>
private static final Pattern ANSI_ESCAPE = Pattern.compile(
"\u001B(?:\\[[0-9;]*[A-Za-z]|[^\\[])");
// Map of SGR codes to CSS classes (added to <span class="...">)
// Covers the codes actually emitted by build-site.el
private static final Map<String, String> SGR_CLASS = Map.ofEntries(
Map.entry("0", "ansi-reset"),
Map.entry("1", "ansi-bold"),
@@ -96,11 +97,27 @@ public class BuildController {
HEALTH + UPTIME
========================================================= */
@Operation(
summary = "Get health",
description = "Returns a simple health check"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Healthy"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
@GetMapping("/health")
public ResponseEntity<String> health() {
return ResponseEntity.ok("ok");
}
@Operation(
summary = "Get uptime",
description = "Returns the uptime of the server"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Uptime retrieved successfully"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
@GetMapping("/uptime")
public ResponseEntity<Map<String, Object>> getUptime() {
long uptimeMs = Instant.now().toEpochMilli() - SERVER_START.toEpochMilli();
@@ -116,21 +133,53 @@ public class BuildController {
WEB BUILD
========================================================= */
@Operation(
summary = "Trigger web build",
description = "Triggers a build of the web application"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Build triggered successfully"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
@PostMapping("/build-web")
public ResponseEntity<String> triggerWebBuild() {
return startBuild(webBuildDirectory, webBuildLogFile, webBuildRunning, webProcess, true);
}
@Operation(
summary = "Cancel web build",
description = "Cancels a running web build"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Build cancelled successfully"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
@DeleteMapping("/build-web")
public ResponseEntity<String> cancelWebBuild() {
return killProcess(webProcess, webBuildRunning, "web build");
}
@Operation(
summary = "Get web build status",
description = "Returns the status of a running web build"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Build status retrieved successfully"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
@GetMapping("/build-web/status")
public ResponseEntity<BuildStatus> getWebBuildStatus() {
return ResponseEntity.ok(new BuildStatus(webBuildRunning.get(), webLastRun, webLastExitCode));
}
@Operation(
summary = "Stream web build logs",
description = "Streams the logs of a running web build"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Logs streamed successfully"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
@GetMapping("/build-web/logs")
public SseEmitter streamWebLogs() {
return streamLogs(webBuildLogFile);
@@ -140,21 +189,53 @@ public class BuildController {
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<String> 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<String> 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<BuildStatus> 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);
@@ -164,22 +245,54 @@ public class BuildController {
EMACS RERUN
========================================================= */
@Operation(
summary = "Trigger Emacs rerun",
description = "Triggers a rerun of Emacs"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Emacs rerun triggered successfully"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
@PostMapping("/rerun-emacs")
public ResponseEntity<String> triggerEmacs() {
log.info("Triggering Emacs command");
return startCommand(emacsRunDirectory, emacsRunLogFile, emacsRunning, emacsProcess);
}
@Operation(
summary = "Cancel Emacs rerun",
description = "Cancels a running Emacs rerun"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Emacs rerun cancelled successfully"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
@DeleteMapping("/rerun-emacs")
public ResponseEntity<String> cancelEmacs() {
return killProcess(emacsProcess, emacsRunning, "emacs");
}
@Operation(
summary = "Get Emacs rerun status",
description = "Returns the status of a running Emacs rerun"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Emacs rerun status retrieved successfully"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
@GetMapping("/rerun-emacs/status")
public ResponseEntity<BuildStatus> getEmacsStatus() {
return ResponseEntity.ok(new BuildStatus(emacsRunning.get(), emacsLastRun, emacsLastExitCode));
}
@Operation(
summary = "Stream Emacs rerun logs",
description = "Streams the logs of a running Emacs rerun"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Logs streamed successfully"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
@GetMapping("/rerun-emacs/logs")
public SseEmitter streamEmacsLogs() {
return streamLogs(emacsRunLogFile);
@@ -189,6 +302,14 @@ public class BuildController {
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<String> triggerCombined() {
if (!combinedRunning.compareAndSet(false, true)) {
@@ -259,16 +380,40 @@ public class BuildController {
}
}
@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<String> 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<BuildStatus> 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);
@@ -278,6 +423,14 @@ public class BuildController {
LAST RUN TIMESTAMPS
========================================================= */
@Operation(
summary = "Get last run timestamps",
description = "Returns the last run timestamps of all builds"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Last run timestamps retrieved successfully"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
@GetMapping("/last-runs")
public ResponseEntity<Map<String, Object>> getLastRuns() {
return ResponseEntity.ok(Map.of(

View File

@@ -0,0 +1,36 @@
package org.zaine.app.exception;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
// 🔹 Handles "no route found" (true 404 URLs)
@ExceptionHandler(org.springframework.web.servlet.NoHandlerFoundException.class)
public ResponseEntity<Map<String, String>> handleNotFound(
HttpServletRequest request
) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(
Map.of(
"message", "Could not find resource with URL: " + request.getRequestURI()
)
);
}
// 🔹 Handles your own "not found in DB"
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<Map<String, String>> handleRuntime(
RuntimeException ex,
HttpServletRequest request
) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(
Map.of(
"message", "Could not find resource with URL: " + request.getRequestURI()
)
);
}
}

View File

@@ -1,29 +1,27 @@
package org.zaine.app.service;
import java.util.List;
import java.util.logging.Logger;
import java.util.Optional;
import org.springframework.stereotype.Service;
import org.zaine.app.model.Notes;
import org.zaine.app.repositories.NotesRepository;
@Service
public class NotesService {
private static final Logger logger = Logger.getLogger(NotesService.class.getName());
private final NotesRepository notesRepository;
public NotesService(NotesRepository notesRepository) {
this.notesRepository = notesRepository;
this.notesRepository = notesRepository;
}
public List<Notes> getAllNotes() {
return notesRepository.findAll();
return Optional.ofNullable(notesRepository.findAll())
.orElseThrow(() -> new RuntimeException("Failed to retrieve notes"));
}
public Notes createNote(Notes note) {
logger.info("Creating note with content: " + note.getContent());
return notesRepository.save(note);
return Optional.ofNullable(notesRepository.save(note)).orElseThrow(() -> new RuntimeException("Failed to create note"));
}
}