Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,36 @@ jobs:

- name: Run tests
run: mvn -B -Dspring-boot.version=${{ matrix.spring-boot }} clean test --file pom.xml

tests-boot4:
name: Run tests with Spring Boot 4
runs-on: ubuntu-latest

steps:
- name: Checkout repo
uses: actions/checkout@v3

- name: Setup Node
uses: actions/setup-node@v3

- name: Cache Node.js modules
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-

- name: Install Node.js dependencies
run: |
cd db-scheduler-ui-frontend
npm install

- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'

- name: Run tests
run: mvn -B -pl db-scheduler-ui-spring-boot-4-starter,example-app-boot4 -am clean test --file pom.xml
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,6 @@ replay_pid*
.springBeans
.sts4-cache
.dbeaver

# Claude
.claude
70 changes: 70 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

A UI dashboard extension for [db-scheduler](https://github.com/kagkarlsson/db-scheduler) — a Java library for persistent task scheduling. The UI provides monitoring and administration (view, run, delete, reschedule tasks) via a Spring Boot auto-configured web interface.

## Build & Development Commands

```bash
# Full build (compiles frontend + backend, runs tests, checks license headers)
./mvnw clean install -q

# Run tests only
./mvnw test -q

# Run a single test class
./mvnw test -pl example-app -am -q -DskipTests && ./mvnw test -pl example-app -q -Dtest="SmokeTest"

# Format Java code (Google Java Format via Spotless — required before PR)
./mvnw spotless:apply

# Sort pom.xml files
./mvnw sortpom:sort

# Check license headers
./mvnw license:check

# Add missing license headers
./mvnw license:format

# Frontend dev server (proxies API to localhost:8081)
cd db-scheduler-ui-frontend && npm run dev

# Frontend lint
cd db-scheduler-ui-frontend && npm run lint
```

## Module Architecture

Multi-module Maven project (`pom.xml` at root):

- **db-scheduler-ui** — Core library. REST controllers (`/db-scheduler-api/**`), service logic (`TaskLogic`, `LogLogic`), query/filter/sort utilities, and task-to-model mapping. Contains the bundled frontend in `src/main/resources/static/`. No Spring Boot auto-configuration here — just plain Spring `@RestController` beans.
- `TaskController` (GET endpoints: `/all`, `/details`, `/poll`)
- `TaskAdminController` (POST endpoints: `/rerun`, `/rerunGroup`, `/delete`) — disabled when `read-only=true`
- `LogController` — task execution history (requires `history=true`)
- `ConfigController` — exposes UI config (history enabled, read-only mode)
- `Caching` — in-memory cache of scheduler executions for polling/status changes

- **db-scheduler-ui-starter** — Spring Boot 3 auto-configuration (`UiApiAutoConfiguration`). Wires up all beans from `db-scheduler-ui`, handles context-path/servlet-path resolution, SPA fallback routing, and index.html rewriting. Properties class: `DbSchedulerUiProperties`.

- **db-scheduler-ui-spring-boot-4-starter** — Spring Boot 4 variant of the starter. Near-identical to the Boot 3 starter (only `WebMvcRegistrations` import differs due to Boot 4 package relocation).

- **db-scheduler-ui-frontend** — React/TypeScript SPA (Vite + Chakra UI + React Query). Built output is copied into `db-scheduler-ui/src/main/resources/static/db-scheduler/` during `mvn install`. Dev server runs on port 51373 and proxies `/db-scheduler-api` to `localhost:8081`.

- **example-app** — Spring Boot app with H2, sample tasks, and integration tests (smoke tests, read-only, Spring Security, context-path). Main class: `ExampleApp`. Runs on port 8081.

- **example-app-boot4** — Spring Boot 4 variant of the example app (uses Jackson 3/`tools.jackson` packages, `@AutoConfigureTestRestTemplate`, Boot 4 starter renames).

- **example-app-webflux** — WebFlux variant of the example app.

## Key Patterns

- The frontend build is triggered during Maven's `generate-resources` phase via `exec-maven-plugin` (runs `npm install` and `npm run build` in `db-scheduler-ui-frontend/`), then output is copied to backend resources via `maven-resources-plugin`.
- Controllers are not annotated with `@Component` — they're instantiated as `@Bean` in `UiApiAutoConfiguration` so auto-configuration controls their lifecycle.
- Java code uses Lombok (`@Data`, `@Builder`, etc.) and Google Java Format style.
- All `.java`, `.ts`, `.tsx` source files (except tests) require Apache 2.0 license headers (enforced by `license-maven-plugin`).
- CI tests against Spring Boot 3.3, 3.4, 3.5, and 4.0 for compatibility.
- Frontend uses path alias `src` → `/src` (configured in vite.config.ts).
64 changes: 64 additions & 0 deletions db-scheduler-ui-spring-boot-4-starter/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>no.bekk.db-scheduler-ui</groupId>
<artifactId>db-scheduler-ui-parent</artifactId>
<version>main-SNAPSHOT</version>
</parent>

<artifactId>db-scheduler-ui-spring-boot-4-starter</artifactId>
<name>db-scheduler-ui-spring-boot-4-starter</name>
<description>Spring Boot 4 starter for db-scheduler-ui</description>
<url>https://github.com/bekk/db-scheduler-ui</url>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot-4.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.github.kagkarlsson</groupId>
<artifactId>db-scheduler</artifactId>
<version>${db-scheduler-boot4.version}</version>
</dependency>
<dependency>
<groupId>com.github.kagkarlsson</groupId>
<artifactId>db-scheduler-spring-boot-4-starter</artifactId>
<version>${db-scheduler-boot4.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.github.kagkarlsson</groupId>
<artifactId>db-scheduler-spring-boot-4-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>no.bekk.db-scheduler-ui</groupId>
<artifactId>db-scheduler-ui</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/*
* Copyright (C) Bekk
*
* <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License at
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package no.bekk.dbscheduler.uistarter.autoconfigure;

import static no.bekk.dbscheduler.uistarter.config.DbSchedulerUiUtil.normalizePath;
import static no.bekk.dbscheduler.uistarter.config.DbSchedulerUiUtil.normalizePaths;

import com.github.kagkarlsson.scheduler.Scheduler;
import com.github.kagkarlsson.scheduler.boot.config.DbSchedulerCustomizer;
import com.github.kagkarlsson.scheduler.serializer.Serializer;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import javax.sql.DataSource;
import no.bekk.dbscheduler.ui.controller.ConfigController;
import no.bekk.dbscheduler.ui.controller.IndexHtmlController;
import no.bekk.dbscheduler.ui.controller.LogController;
import no.bekk.dbscheduler.ui.controller.SpaFallbackMvc;
import no.bekk.dbscheduler.ui.controller.TaskAdminController;
import no.bekk.dbscheduler.ui.controller.TaskController;
import no.bekk.dbscheduler.ui.service.LogLogic;
import no.bekk.dbscheduler.ui.service.TaskLogic;
import no.bekk.dbscheduler.ui.util.Caching;
import no.bekk.dbscheduler.uistarter.config.DbSchedulerUiProperties;
import no.bekk.dbscheduler.uistarter.config.DbSchedulerUiWebConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;

@AutoConfiguration
@ConditionalOnProperty(value = "db-scheduler-ui.enabled", matchIfMissing = true)
@EnableConfigurationProperties(DbSchedulerUiProperties.class)
public class UiApiAutoConfiguration {

private static final Logger logger = LoggerFactory.getLogger(UiApiAutoConfiguration.class);
private final String servletContextPath;
private final String mvcServletPath;
private final String dbSchedulerContextPath;

UiApiAutoConfiguration(
@Value("${server.servlet.context-path:}") String servletContextPath,
@Value("${spring.mvc.servlet.path:}") String mvcServletPath,
@Value("${db-scheduler-ui.context-path:}") String dbSchedulerContextPath) {
logger.info("UiApiAutoConfiguration created");
this.servletContextPath = normalizePaths(servletContextPath);
this.dbSchedulerContextPath = normalizePaths(dbSchedulerContextPath);
this.mvcServletPath = normalizePaths(mvcServletPath);
}

@Bean
@ConditionalOnMissingBean
Caching caching() {
return new Caching();
}

@Bean
@ConditionalOnMissingBean
TaskLogic taskLogic(Scheduler scheduler, Caching caching, DbSchedulerUiProperties properties) {
return new TaskLogic(scheduler, caching, properties.isTaskData());
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(
prefix = "db-scheduler-ui",
name = "history",
havingValue = "true",
matchIfMissing = false)
LogLogic logLogic(
DataSource dataSource,
Caching caching,
DbSchedulerCustomizer customizer,
DbSchedulerUiProperties properties,
@Value("${db-scheduler-log.table-name:scheduled_execution_logs}") String logTableName,
@Value("${db-scheduler-ui.log-limit:0}") int logLimit) {
return new LogLogic(
customizer.dataSource().orElse(dataSource),
customizer.serializer().orElse(Serializer.DEFAULT_JAVA_SERIALIZER),
caching,
properties.isTaskData(),
logTableName,
logLimit);
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(
prefix = "db-scheduler-ui",
name = "read-only",
havingValue = "false",
matchIfMissing = true)
TaskAdminController taskAdminController(TaskLogic taskLogic) {
return new TaskAdminController(taskLogic);
}

@Bean
@ConditionalOnMissingBean
TaskController taskController(TaskLogic taskLogic) {
return new TaskController(taskLogic);
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(
prefix = "db-scheduler-ui",
name = "history",
havingValue = "true",
matchIfMissing = false)
LogController logController(LogLogic logLogic) {
return new LogController(logLogic);
}

@Bean
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnMissingBean
SpaFallbackMvc spaFallbackMvc(
@Value("${db-scheduler-ui.context-path:}") String contextPath,
@Qualifier("indexHtml") String indexHtml) {
return new SpaFallbackMvc(normalizePath(contextPath), indexHtml);
}

@Bean
@ConditionalOnWebApplication(type = Type.REACTIVE)
@ConditionalOnMissingBean
public RouterFunction<ServerResponse> dbSchedulerRouter(
@Qualifier("indexHtml") String indexHtml) {
return RouterFunctions.route(
RequestPredicates.GET("/db-scheduler/**").and(request -> !request.path().contains(".")),
request -> ServerResponse.ok().contentType(MediaType.TEXT_HTML).bodyValue(indexHtml));
}

@Bean
@ConditionalOnMissingBean
ConfigController configController(DbSchedulerUiProperties properties) {
return new ConfigController(properties.isHistory(), properties::isReadOnly);
}

@Bean
@ConditionalOnMissingBean
IndexHtmlController indexHtmlController(
@Qualifier("indexHtml") String indexHtml, @Qualifier("contextPath") String contextPath) {
return new IndexHtmlController(indexHtml, contextPath);
}

@Bean
@ConditionalOnProperty(prefix = "db-scheduler-ui", name = "context-path")
DbSchedulerUiWebConfiguration dbSchedulerUiWebConfiguration(
@Value("${db-scheduler-ui.context-path:}") String contextPath) {
return new DbSchedulerUiWebConfiguration(normalizePath(contextPath));
}

@Bean(name = "contextPath")
public String contextPath() {
return normalizePaths(servletContextPath, mvcServletPath, dbSchedulerContextPath);
}

@Bean(name = "indexHtml")
public String indexHtml(@Qualifier("contextPath") String contextPath) throws IOException {
String indexHtml =
new ClassPathResource(SpaFallbackMvc.DEFAULT_STARTING_PAGE)
.getContentAsString(StandardCharsets.UTF_8);

String contextPathScript = contextPath + "/db-scheduler/js/context-path.js";

return indexHtml
.replaceAll("/db-scheduler", contextPath + "/db-scheduler")
.replaceAll(
"<head>",
"""
<head>
<script src='%s'></script>"""
.formatted(contextPathScript));
}
}
Loading