diff --git a/src/main/java/org/zaine/app/config/OpenApiConfig.java b/src/main/java/org/zaine/app/config/OpenApiConfig.java index 9d1105b..d561e14 100644 --- a/src/main/java/org/zaine/app/config/OpenApiConfig.java +++ b/src/main/java/org/zaine/app/config/OpenApiConfig.java @@ -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) + ); +} } \ No newline at end of file diff --git a/src/main/java/org/zaine/app/controller/CalibreController.java b/src/main/java/org/zaine/app/controller/CalibreController.java index 32571bf..724a894 100644 --- a/src/main/java/org/zaine/app/controller/CalibreController.java +++ b/src/main/java/org/zaine/app/controller/CalibreController.java @@ -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 getBooks() { return calibreService.getAllBooks(); diff --git a/src/main/java/org/zaine/app/controller/CommentsController.java b/src/main/java/org/zaine/app/controller/CommentsController.java index 83e6b7f..22e3cc4 100755 --- a/src/main/java/org/zaine/app/controller/CommentsController.java +++ b/src/main/java/org/zaine/app/controller/CommentsController.java @@ -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 getAllCommentsBySlug(@PathVariable String page_slug) { - List allComments = commentsService.getAllCommentsBySlug(page_slug); - return allComments; - + List 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 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 getCommentThread(@PathVariable Integer comment_id) { return commentsService.getCommentThread(comment_id); diff --git a/src/main/java/org/zaine/app/controller/CompetenciesController.java b/src/main/java/org/zaine/app/controller/CompetenciesController.java index 96cf346..37bbb4b 100755 --- a/src/main/java/org/zaine/app/controller/CompetenciesController.java +++ b/src/main/java/org/zaine/app/controller/CompetenciesController.java @@ -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 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" diff --git a/src/main/java/org/zaine/app/controller/MotalahController.java b/src/main/java/org/zaine/app/controller/MotalahController.java deleted file mode 100644 index 0192eb8..0000000 --- a/src/main/java/org/zaine/app/controller/MotalahController.java +++ /dev/null @@ -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 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 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 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 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 delete(@PathVariable Long id) { - logger.log(Logger.Level.INFO, "Deleting motalah session id: {0}", id); - motalahService.delete(id); - return ResponseEntity.noContent().build(); - } -} \ No newline at end of file diff --git a/src/main/java/org/zaine/app/controller/NotesController.java b/src/main/java/org/zaine/app/controller/NotesController.java index d75e2e8..fd5ebdc 100755 --- a/src/main/java/org/zaine/app/controller/NotesController.java +++ b/src/main/java/org/zaine/app/controller/NotesController.java @@ -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 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); } - - - - } diff --git a/src/main/java/org/zaine/app/controller/WirdController.java b/src/main/java/org/zaine/app/controller/WirdController.java index 0dc3cdb..9d90e31 100644 --- a/src/main/java/org/zaine/app/controller/WirdController.java +++ b/src/main/java/org/zaine/app/controller/WirdController.java @@ -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 getKhatmEntries() { return wirdService.getKhatmEntries(); } + + /** + * GET /api/wird/motalah + * All study sessions, newest first. + */ + @GetMapping("/motalah") + public List 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 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 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 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 delete(@PathVariable Long id) { + logger.log(Logger.Level.INFO, "Deleting motalah session id: {0}", id); + motalahService.delete(id); + return ResponseEntity.noContent().build(); + } } 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 b8d7c90..20ead76 100644 --- a/src/main/java/org/zaine/app/controller/zone/BuildController.java +++ b/src/main/java/org/zaine/app/controller/zone/BuildController.java @@ -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 [ - // and simple ESC sequences: ESC private static final Pattern ANSI_ESCAPE = Pattern.compile( "\u001B(?:\\[[0-9;]*[A-Za-z]|[^\\[])"); - // Map of SGR codes to CSS classes (added to ) - // Covers the codes actually emitted by build-site.el private static final Map 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 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> 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 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 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 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 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); @@ -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 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 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 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 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 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); @@ -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> getLastRuns() { return ResponseEntity.ok(Map.of( diff --git a/src/main/java/org/zaine/app/exception/GlobalExceptionHandler.java b/src/main/java/org/zaine/app/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..41df243 --- /dev/null +++ b/src/main/java/org/zaine/app/exception/GlobalExceptionHandler.java @@ -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> 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> handleRuntime( + RuntimeException ex, + HttpServletRequest request + ) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body( + Map.of( + "message", "Could not find resource with URL: " + request.getRequestURI() + ) + ); + } +} \ No newline at end of file diff --git a/src/main/java/org/zaine/app/service/NotesService.java b/src/main/java/org/zaine/app/service/NotesService.java index dee885a..40bd70e 100755 --- a/src/main/java/org/zaine/app/service/NotesService.java +++ b/src/main/java/org/zaine/app/service/NotesService.java @@ -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 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")); } }