diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index bc9679f..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(tree:*)", - "Bash(xargs:*)", - "Bash(mvn compile:*)", - "Bash(mvn test-compile:*)", - "Bash(find:*)", - "Bash(mvn test:*)", - "Bash(tee:*)", - "Bash(export TESTCONTAINERS_RYUK_DISABLED=true)", - "Bash(echo:*)", - "Bash(pgrep:*)", - "Bash(pkill:*)", - "Bash(ls:*)", - "Bash(sleep 120 echo \"=== Screenshots generated so far ===\" ls -la target/screenshots/case_*.png)", - "Bash(wc:*)", - "Bash(export DOCKER_HOST=unix:///run/user/1000/podman/podman.sock)" - ] - } -} diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml deleted file mode 100644 index 6d07884..0000000 --- a/.gitea/workflows/test.yml +++ /dev/null @@ -1,118 +0,0 @@ -name: Tests - -on: - push: - branches: [main, dev] - pull_request: - branches: [main] - -env: - ALLURE_SERVER: "http://10.80.0.6:5050" - ALLURE_PROJECT: "lcc-${{ gitea.ref_name }}" - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Java 23 - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '23' - cache: 'maven' - - - name: Install Maven - run: | - apt-get update && apt-get install -y maven - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Build Frontend - run: cd src/frontend && npm ci && BUILD_FOR_SPRING=true npm run build - - - name: Install Playwright Browsers - run: | - mvn test-compile -B --no-transfer-progress - mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.classpathScope=test -D exec.args="install --with-deps chromium" - env: - PLAYWRIGHT_DOWNLOAD_CONNECTION_TIMEOUT: "300000" - - - name: Run Tests - run: mvn verify -B --no-transfer-progress - env: - TESTCONTAINERS_RYUK_DISABLED: "true" - - - name: Prepare Allure Results - if: always() - run: | - mkdir -p target/allure-results - cat > target/allure-results/executor.json << EOF - { - "name": "Gitea Actions", - "type": "gitea", - "buildName": "#${{ gitea.run_number }}", - "buildOrder": ${{ gitea.run_number }}, - "buildUrl": "${{ gitea.server_url }}/${{ gitea.repository }}/actions/runs/${{ gitea.run_id }}" - } - EOF - - - name: Upload to Allure - if: always() - run: | - # Login - curl -s -c cookies.txt \ - -X POST "${ALLURE_SERVER}/allure-docker-service/login" \ - -H 'Content-Type: application/json' \ - -d '{"username":"admin","password":"${{ secrets.ALLURE_PASSWORD }}"}' - - CSRF_TOKEN=$(grep csrf_access_token cookies.txt | awk '{print $7}') - - # Create project - curl -s -o /dev/null -b cookies.txt \ - -H "X-CSRF-TOKEN: ${CSRF_TOKEN}" \ - -X POST "${ALLURE_SERVER}/allure-docker-service/projects" \ - -H "Content-Type: application/json" \ - -d '{"id":"'${ALLURE_PROJECT}'"}' || true - - # Clean old results - curl -s -o /dev/null -b cookies.txt \ - -H "X-CSRF-TOKEN: ${CSRF_TOKEN}" \ - "${ALLURE_SERVER}/allure-docker-service/clean-results?project_id=${ALLURE_PROJECT}" - - # Build JSON payload with base64 - echo '{"results":[' > payload.json - FIRST=true - for f in target/allure-results/*; do - if [ -f "$f" ]; then - FILENAME=$(basename "$f") - CONTENT=$(base64 -w 0 "$f") - if [ "$FIRST" = true ]; then - FIRST=false - else - echo ',' >> payload.json - fi - echo '{"file_name":"'"$FILENAME"'","content_base64":"'"$CONTENT"'"}' >> payload.json - fi - done - echo ']}' >> payload.json - - # Upload via JSON - curl -s -o /dev/null -b cookies.txt \ - -H "X-CSRF-TOKEN: ${CSRF_TOKEN}" \ - -H "Content-Type: application/json" \ - -X POST "${ALLURE_SERVER}/allure-docker-service/send-results?project_id=${ALLURE_PROJECT}" \ - -d @payload.json - - # Generate report - curl -s -b cookies.txt \ - -H "X-CSRF-TOKEN: ${CSRF_TOKEN}" \ - "${ALLURE_SERVER}/allure-docker-service/generate-report?project_id=${ALLURE_PROJECT}" - - echo "✅ Allure upload complete" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9a34d5b..34ea8ae 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,6 @@ target/ .sts4-cache .env.example /.env -/.env.* ### IntelliJ IDEA ### .idea diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 5505623..0ea42ff 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -16,4 +16,4 @@ # under the License. wrapperVersion=3.3.4 distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 85b4b5f..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,636 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -LCC (Logistic Cost Calculator) is a Spring Boot 3.5.9 backend API for calculating complex logistics costs across supply chain networks. It handles materials, packaging, transportation rates, route planning, and multi-component cost calculations including customs duties, handling, inventory, and risk assessment. - -**Database Support:** The application supports both **MySQL 8.0** and **MSSQL Server 2022** through a database abstraction layer (`SqlDialectProvider`), allowing deployment flexibility across different database platforms. - -## Build & Run Commands - -```bash -# Build the project -mvn clean install - -# Run the application (default: MySQL) -mvn spring-boot:run - -# Run with MSSQL -mvn spring-boot:run -Dspring.profiles.active=mssql - -# Run all tests on MySQL -mvn test -Dspring.profiles.active=test,mysql - -# Run all tests on MSSQL -mvn test -Dspring.profiles.active=test,mssql - -# Run repository integration tests on both databases -mvn test -Dtest="*RepositoryIntegrationTest" -Dspring.profiles.active=test,mysql -mvn test -Dtest="*RepositoryIntegrationTest" -Dspring.profiles.active=test,mssql - -# Run a specific test class -mvn test -Dtest=NodeControllerIntegrationTest - -# Run a specific test method -mvn test -Dtest=NodeControllerIntegrationTest#shouldReturnListOfNodesWithDefaultPagination - -# Skip tests during build -mvn clean install -DskipTests - -# Generate JAXB classes from WSDL (EU taxation service) -mvn jaxb:generate - -# Generate Allure test report (requires allure-commandline) -mvn clean test -allure serve target/allure-results -``` - -## Development Environment (Distrobox) - -**IMPORTANT:** This project runs inside a **Distrobox** container. This affects how TestContainers and Podman work. - -### TestContainers with Distrobox + Podman - -TestContainers needs access to the **host's Podman socket**, not the one inside the Distrobox. The configuration is handled via `~/.testcontainers.properties`: - -```properties -docker.host=unix:///run/host/run/user/1000/podman/podman.sock -ryuk.disabled=true -``` - -### Troubleshooting TestContainers / Podman Issues - -If tests fail with "Could not find a valid Docker environment": - -1. **Check if Podman works on the host:** - ```bash - distrobox-host-exec podman info - ``` - -2. **If you see cgroup or UID/GID errors, run migration on the host:** - ```bash - distrobox-host-exec podman system migrate - ``` - -3. **Restart podman socket on host if needed:** - ```bash - distrobox-host-exec systemctl --user restart podman.socket - ``` - -4. **Verify the host socket is accessible from Distrobox:** - ```bash - ls -la /run/host/run/user/1000/podman/podman.sock - ``` - -5. **Test container execution via host:** - ```bash - distrobox-host-exec podman run --rm hello-world - ``` - -### Key Paths - -| Path | Description | -|------|-------------| -| `/run/host/run/user/1000/podman/podman.sock` | Host's Podman socket (accessible from Distrobox) | -| `~/.testcontainers.properties` | TestContainers configuration file | - -## Architecture - -### Layered Architecture -``` -Controllers → DTOs → Services → Transformers → Repositories → SqlDialectProvider → Database (MySQL/MSSQL) -``` - -### Package Structure (`de.avatic.lcc`) -- **controller/** - REST endpoints organized by domain (calculation, configuration, bulk, users, report) -- **service/access/** - Business logic for domain entities (PremisesService, MaterialService, NodeService, etc.) -- **service/calculation/** - Logistics cost calculation orchestration and step services -- **service/calculation/execution/steps/** - Individual calculation components (airfreight, handling, inventory, customs, etc.) -- **service/bulk/** - Excel-based bulk import/export operations -- **service/api/** - External API integrations (Azure Maps geocoding, EU taxation) -- **service/transformer/** - Entity-to-DTO mapping -- **repositories/** - JDBC-based data access (not JPA) with custom RowMappers -- **database/dialect/** - Database abstraction layer (SqlDialectProvider, MySQLDialectProvider, MSSQLDialectProvider) -- **model/db/** - Database entity classes -- **dto/** - Data transfer objects for API contracts - -### Key Design Decisions -- **JDBC over JPA**: Uses `JdbcTemplate` and `NamedParameterJdbcTemplate` for complex queries -- **SqlDialectProvider abstraction**: Database-agnostic SQL through dialect-specific implementations (MySQL/MSSQL) -- **Transformer layer**: Explicit DTO mapping keeps entities separate from API contracts -- **Calculation chain**: Cost calculations broken into fine-grained services in `execution/steps/` -- **Profile-based configuration**: Spring profiles for environment-specific database selection - -### Core Calculation Flow -``` -CalculationExecutionService.launchJobCalculation() - → ContainerCalculationService (container type selection: FEU/TEU/HC/TRUCK) - → RouteSectionCostCalculationService (per-section costs) - → AirfreightCalculationService - → HandlingCostCalculationService - → InventoryCostCalculationService - → CustomCostCalculationService (tariff/duties) -``` - -### Authorization Model -Role-based access control via `@PreAuthorize` annotations: -- SUPER, CALCULATION, MATERIAL, FREIGHT, PACKAGING, BASIC - -## Testing - -### Test Architecture - -**Integration Test Base Class:** -All repository integration tests extend `AbstractRepositoryIntegrationTest`, which provides: -- `JdbcTemplate` for test data setup -- `SqlDialectProvider` for database-agnostic SQL -- Helper methods: `isMysql()`, `isMssql()`, `executeRawSql()` -- Automatic TestContainers setup via `@Testcontainers` -- Transaction isolation via `@Transactional` - -**TestContainers Setup:** -```java -@SpringBootTest(classes = {RepositoryTestConfig.class}) -@Testcontainers -@Import(DatabaseTestConfiguration.class) -@Transactional -public abstract class AbstractRepositoryIntegrationTest { - @Autowired - protected JdbcTemplate jdbcTemplate; - - @Autowired - protected SqlDialectProvider dialectProvider; - - protected boolean isMysql() { - return getDatabaseProfile().contains("mysql"); - } - - protected void executeRawSql(String sql, Object... params) { - jdbcTemplate.update(sql, params); - } -} -``` - -**DatabaseTestConfiguration:** -- MySQL: `MySQLContainer` with `mysql:8.0` image -- MSSQL: `MSSQLServerContainer` with `mcr.microsoft.com/mssql/server:2022-latest` image -- Profile-based activation via `@Profile("mysql")` and `@Profile("mssql")` - -### Database-Agnostic Test Patterns - -**Pattern 1: Boolean literals in test data** -```java -String sql = String.format( - "INSERT INTO node (name, is_active) VALUES (?, %s)", - dialectProvider.getBooleanTrue()); -``` - -**Pattern 2: Auto-increment ID retrieval** -```java -executeRawSql("INSERT INTO table (name) VALUES (?)", name); -String selectSql = isMysql() ? "SELECT LAST_INSERT_ID()" : "SELECT CAST(@@IDENTITY AS INT)"; -return jdbcTemplate.queryForObject(selectSql, Integer.class); -``` - -**Pattern 3: Date functions** -```java -String dateFunc = isMysql() ? "NOW()" : "GETDATE()"; -String sql = String.format("INSERT INTO table (created_at) VALUES (%s)", dateFunc); -``` - -### Running Tests - -**Run all tests on MySQL:** -```bash -mvn test -Dspring.profiles.active=test,mysql -``` - -**Run all tests on MSSQL:** -```bash -mvn test -Dspring.profiles.active=test,mssql -``` - -**Run specific repository tests:** -```bash -mvn test -Dtest=CalculationJobRepositoryIntegrationTest -Dspring.profiles.active=test,mysql -mvn test -Dtest=CalculationJobRepositoryIntegrationTest -Dspring.profiles.active=test,mssql -``` - -**Run all repository integration tests on both databases:** -```bash -mvn test -Dtest="*RepositoryIntegrationTest" -Dspring.profiles.active=test,mysql -mvn test -Dtest="*RepositoryIntegrationTest" -Dspring.profiles.active=test,mssql -``` - -### Test Coverage - -**Current Status (as of Phase 6 completion):** -- **365 tests** passing on both MySQL and MSSQL (100% success rate) -- **28 repository integration test classes** covering: - - Calculation repositories (CalculationJobRepository, CalculationJobDestinationRepository, CalculationJobRouteSectionRepository) - - Configuration repositories (NodeRepository, MaterialRepository, PackagingRepository, CountryRepository) - - Rate repositories (ContainerRateRepository, MatrixRateRepository) - - Property repositories (PropertyRepository, CountryPropertyRepository, PackagingPropertiesRepository) - - User repositories (UserRepository, GroupRepository) - - Bulk operation repositories (BulkOperationRepository) - - And 14 additional repositories - -**Test Data:** -- `@Sql` annotations for controller integration tests from `src/test/resources/master_data/` -- Repository tests use inline SQL with `executeRawSql()` for database-agnostic test data setup -- Test data cleanup in `@BeforeEach` respects foreign key constraints - -### Allure Test Reporting - -**Overview:** -All tests (46 test classes, ~624 test methods) are annotated with Allure reporting framework annotations for comprehensive test documentation and reporting. - -**Annotation Hierarchy:** -```java -@Epic("Controller") // Test layer: Controller, Repository, Database Layer, End-to-End -@Feature("Calculation") // Domain/subpackage: Calculation, Configuration, Master Data, etc. -@DisplayName("Test Suite Name") // Human-readable test suite name -class ExampleTest { - - @Test - @Story("Create new calculation") // Test scenario description - @DisplayName("Should create calculation with valid data") - void testCreateCalculation() { ... } -} -``` - -**Annotation Coverage by Layer:** - -| Layer | Epic | Features | Test Classes | Test Methods | -|-------|------|----------|--------------|--------------| -| **Controller** | `@Epic("Controller")` | Configuration, Calculation, Report | 11 | ~100 | -| **Repository** | `@Epic("Repository")` | Calculation, Master Data, Premise, Rates, Properties, Country, Packaging, Users, Bulk, Error, Infrastructure | 28 | ~400 | -| **Database Layer** | `@Epic("Database Layer")` | MySQL Dialect, MSSQL Dialect | 2 | ~42 | -| **End-to-End** | `@Epic("End-to-End")` | Smoke Tests, Calculation Workflow, Deviation Analysis | 3 | ~7 | - -**Local Report Generation:** -```bash -# Run tests and generate Allure results -mvn clean test -Dspring.profiles.active=test,mysql - -# Generate and view Allure report (requires allure-commandline) -allure serve target/allure-results -``` - -**CI/CD Integration:** -- Gitea Actions workflow (`.gitea/workflows/test.yml`) automatically uploads Allure results to Allure server -- Reports available at: `http://10.80.0.6:5050` (project: `lcc-{branch}`) -- Each CI run generates a new report with execution metadata - -**Allure Configuration:** -- Dependency: `io.qameta.allure:allure-junit5` (version 2.29.0) -- Results directory: `target/allure-results` -- Report includes: test duration, stack traces, categorization by Epic/Feature/Story - -## Database - -### Multi-Database Support - -The application supports both **MySQL 8.0** and **MSSQL Server 2022** through the `SqlDialectProvider` abstraction layer. - -**Database selection via Spring profiles:** -- `mysql` - MySQL 8.0 (default) -- `mssql` - Microsoft SQL Server 2022 - -**Environment variables:** -```bash -export SPRING_PROFILES_ACTIVE=mysql # or mssql -export DB_HOST=localhost -export DB_DATABASE=lcc -export DB_USER=your_user -export DB_PASSWORD=your_password -``` - -### SqlDialectProvider Pattern - -Database-specific SQL syntax is abstracted through `de.avatic.lcc.database.dialect.SqlDialectProvider`: - -- **MySQLDialectProvider** - MySQL-specific SQL (LIMIT/OFFSET, NOW(), ON DUPLICATE KEY UPDATE, FOR UPDATE SKIP LOCKED) -- **MSSQLDialectProvider** - MSSQL-specific SQL (OFFSET/FETCH, GETDATE(), MERGE, WITH (UPDLOCK, READPAST)) - -**Key dialect differences:** -| Feature | MySQL | MSSQL | -|---------|-------|-------| -| Pagination | `LIMIT ? OFFSET ?` | `OFFSET ? ROWS FETCH NEXT ? ROWS ONLY` | -| Current timestamp | `NOW()` | `GETDATE()` | -| Date subtraction | `DATE_SUB(NOW(), INTERVAL 3 DAY)` | `DATEADD(DAY, -3, GETDATE())` | -| Boolean literals | `TRUE`, `FALSE` | `1`, `0` | -| Auto-increment | `AUTO_INCREMENT` | `IDENTITY(1,1)` | -| Upsert | `ON DUPLICATE KEY UPDATE` | `MERGE` statement | -| Insert ignore | `INSERT IGNORE` | `IF NOT EXISTS ... INSERT` | -| Skip locked rows | `FOR UPDATE SKIP LOCKED` | `WITH (UPDLOCK, READPAST)` | -| Last insert ID | `LAST_INSERT_ID()` | `CAST(@@IDENTITY AS INT)` | - -**Repository usage example:** -```java -@Repository -public class ExampleRepository { - private final JdbcTemplate jdbcTemplate; - private final SqlDialectProvider dialectProvider; - - public ExampleRepository(JdbcTemplate jdbcTemplate, SqlDialectProvider dialectProvider) { - this.jdbcTemplate = jdbcTemplate; - this.dialectProvider = dialectProvider; - } - - public List list(int limit, int offset) { - String sql = "SELECT * FROM table ORDER BY id " + - dialectProvider.buildPaginationClause(limit, offset); - Object[] params = dialectProvider.getPaginationParameters(limit, offset); - return jdbcTemplate.query(sql, params, rowMapper); - } -} -``` - -### Flyway Migrations - -Database-specific migrations are organized by database type: - -``` -src/main/resources/db/migration/ -├── mysql/ -│ ├── V1__Create_schema.sql -│ ├── V2__Property_Set_Period.sql -│ └── V3-V12 (additional migrations) -└── mssql/ - ├── V1__Create_schema.sql - ├── V2__Property_Set_Period.sql - └── V3-V12 (MSSQL-specific conversions) -``` - -**Migration naming:** `V{N}__{Description}.sql` - -**Key schema differences:** -- MySQL uses `AUTO_INCREMENT`, MSSQL uses `IDENTITY(1,1)` -- MySQL supports `TIMESTAMP ... ON UPDATE CURRENT_TIMESTAMP`, MSSQL requires triggers -- MySQL `BOOLEAN` maps to MSSQL `BIT` -- Check constraints syntax differs (BETWEEN vs >= AND <=) - -### Key Tables - -Core entities: -- **premiss**, **premiss_sink**, **premiss_route** - Supply chain scenarios and routing -- **calculation_job**, **calculation_job_destination**, **calculation_job_route_section** - Calculation workflow -- **node** - Suppliers, destinations, intermediate locations -- **material**, **packaging** - Product and packaging master data -- **container_rate**, **country_matrix_rate** - Transportation rates -- **property_set**, **property** - Versioned configuration properties - -## Important Database Considerations - -### Concurrency Control - -**Calculation Job Locking:** -The `CalculationJobRepository.fetchAndLockNextJob()` method uses database-specific row-level locking to prevent concurrent job processing: - -- **MySQL**: `FOR UPDATE SKIP LOCKED` - Skips locked rows and returns next available job -- **MSSQL**: `WITH (UPDLOCK, READPAST)` - Similar semantics but different syntax - -Both implementations ensure that multiple job processors can run concurrently without conflicts. - -### Transaction Isolation - -- Default isolation level: READ_COMMITTED -- Repository tests use `@Transactional` for automatic rollback -- Critical operations (job locking, rate updates) use pessimistic locking - -### Schema Conversion Gotchas - -When adding new Flyway migrations, be aware of these differences: - -**Auto-increment columns:** -```sql --- MySQL -id INT AUTO_INCREMENT PRIMARY KEY - --- MSSQL -id INT IDENTITY(1,1) PRIMARY KEY -``` - -**Timestamp with auto-update:** -```sql --- MySQL -updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP - --- MSSQL (requires trigger) -updated_at DATETIME2 DEFAULT GETDATE() --- Plus CREATE TRIGGER for ON UPDATE behavior -``` - -**Boolean values:** -```sql --- MySQL -is_active BOOLEAN DEFAULT TRUE - --- MSSQL -is_active BIT DEFAULT 1 -``` - -**Check constraints:** -```sql --- MySQL -CHECK (latitude BETWEEN -90 AND 90) - --- MSSQL -CHECK (latitude >= -90 AND latitude <= 90) -``` - -### Performance Considerations - -- Both databases use similar execution plans for most queries -- Indexes are defined identically in both migration sets -- MSSQL may benefit from additional statistics maintenance for complex joins -- Performance regression < 5% observed in comparative testing - -## External Integrations - -- **Azure AD**: OAuth2/OIDC authentication -- **Azure Maps**: Geocoding and route distance calculations (GeoApiService, DistanceApiService) -- **EU Taxation API**: TARIC nomenclature lookup for customs duties (EUTaxationApiService) - -## Configuration - -### Profile-Based Database Configuration - -The application uses Spring profiles for database selection: - -**application-mysql.properties:** -```properties -spring.profiles.active=mysql -spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver -spring.datasource.url=jdbc:mysql://${DB_HOST:localhost}:3306/${DB_DATABASE} -spring.datasource.username=${DB_USER} -spring.datasource.password=${DB_PASSWORD} - -spring.flyway.enabled=true -spring.flyway.locations=classpath:db/migration/mysql -spring.flyway.baseline-on-migrate=true -``` - -**application-mssql.properties:** -```properties -spring.profiles.active=mssql -spring.datasource.driver-class-name=com.microsoft.sqlserver.jdbc.SQLServerDriver -spring.datasource.url=jdbc:sqlserver://${DB_HOST:localhost}:1433;databaseName=${DB_DATABASE};encrypt=true;trustServerCertificate=true -spring.datasource.username=${DB_USER} -spring.datasource.password=${DB_PASSWORD} - -spring.flyway.enabled=true -spring.flyway.locations=classpath:db/migration/mssql -spring.flyway.baseline-on-migrate=true -``` - -**Environment Variables:** -```bash -# MySQL setup -export SPRING_PROFILES_ACTIVE=mysql -export DB_HOST=localhost -export DB_DATABASE=lcc -export DB_USER=root -export DB_PASSWORD=your_password - -# MSSQL setup -export SPRING_PROFILES_ACTIVE=mssql -export DB_HOST=localhost -export DB_DATABASE=lcc -export DB_USER=sa -export DB_PASSWORD=YourStrong!Passw0rd -``` - -### Application Properties - -Key properties in `application.properties`: -- `lcc.auth.identify.by` - User identification method (workday) -- `calculation.job.processor.*` - Async calculation job settings -- Flyway enabled by default; migrations run on startup - -**Database-specific bean activation:** -- `@Profile("mysql")` - Activates MySQLDialectProvider -- `@Profile("mssql")` - Activates MSSQLDialectProvider - -## Quick Reference - -### Switching Databases - -**Switch from MySQL to MSSQL:** -```bash -# Update environment -export SPRING_PROFILES_ACTIVE=mssql -export DB_HOST=localhost -export DB_DATABASE=lcc -export DB_USER=sa -export DB_PASSWORD=YourStrong!Passw0rd - -# Run application -mvn spring-boot:run -``` - -**Switch back to MySQL:** -```bash -export SPRING_PROFILES_ACTIVE=mysql -export DB_HOST=localhost -export DB_DATABASE=lcc -export DB_USER=root -export DB_PASSWORD=your_password - -mvn spring-boot:run -``` - -### Running Migrations - -Migrations run automatically on application startup when Flyway is enabled. - -**Manual migration with Flyway CLI:** -```bash -# MySQL -flyway -url=jdbc:mysql://localhost:3306/lcc -user=root -password=pass -locations=filesystem:src/main/resources/db/migration/mysql migrate - -# MSSQL -flyway -url=jdbc:sqlserver://localhost:1433;databaseName=lcc -user=sa -password=pass -locations=filesystem:src/main/resources/db/migration/mssql migrate -``` - -### Testing Checklist - -When modifying repositories or adding new database-dependent code: - -1. **Run unit tests** (if applicable) - ```bash - mvn test -Dtest=MySQLDialectProviderTest - mvn test -Dtest=MSSQLDialectProviderTest - ``` - -2. **Run repository integration tests on MySQL** - ```bash - mvn test -Dtest="*RepositoryIntegrationTest" -Dspring.profiles.active=test,mysql - ``` - -3. **Run repository integration tests on MSSQL** - ```bash - mvn test -Dtest="*RepositoryIntegrationTest" -Dspring.profiles.active=test,mssql - ``` - -4. **Run full test suite on both databases** - ```bash - mvn test -Dspring.profiles.active=test,mysql - mvn test -Dspring.profiles.active=test,mssql - ``` - -### Common Repository Patterns - -**Pattern 1: Constructor injection with SqlDialectProvider** -```java -@Repository -public class ExampleRepository { - private final JdbcTemplate jdbcTemplate; - private final SqlDialectProvider dialectProvider; - - public ExampleRepository(JdbcTemplate jdbcTemplate, SqlDialectProvider dialectProvider) { - this.jdbcTemplate = jdbcTemplate; - this.dialectProvider = dialectProvider; - } -} -``` - -**Pattern 2: Pagination queries** -```java -public List list(int limit, int offset) { - String sql = "SELECT * FROM table WHERE condition ORDER BY id " + - dialectProvider.buildPaginationClause(limit, offset); - Object[] params = ArrayUtils.addAll( - new Object[]{conditionValue}, - dialectProvider.getPaginationParameters(limit, offset) - ); - return jdbcTemplate.query(sql, params, rowMapper); -} -``` - -**Pattern 3: Insert with ID retrieval** -```java -public Integer create(Entity entity) { - String sql = "INSERT INTO table (name, is_active) VALUES (?, ?)"; - jdbcTemplate.update(sql, entity.getName(), entity.isActive()); - - String idSql = dialectProvider.getLastInsertIdQuery(); - return jdbcTemplate.queryForObject(idSql, Integer.class); -} -``` - -**Pattern 4: Upsert operations** -```java -public void upsert(Entity entity) { - String sql = dialectProvider.buildUpsertStatement( - "table_name", - List.of("unique_col1", "unique_col2"), // unique columns - List.of("unique_col1", "unique_col2", "value"), // insert columns - List.of("value") // update columns - ); - jdbcTemplate.update(sql, entity.getCol1(), entity.getCol2(), entity.getValue()); -} -``` \ No newline at end of file diff --git a/db.sh b/db.sh deleted file mode 100755 index a736b65..0000000 --- a/db.sh +++ /dev/null @@ -1,131 +0,0 @@ -#!/bin/bash -# db.sh - Manage database containers - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" - -usage() { - echo "Usage: $0 [--clean] [--users] [--down]" - echo "" - echo "Options:" - echo " mysql|mssql Which database to start" - echo " --clean Delete volumes and start fresh" - echo " --users Only import test users (database must be running)" - echo " --down Stop the database container" - exit 1 -} - -# Parse parameters -DB="" -CLEAN=false -USERS_ONLY=false -DOWN_ONLY=false - -for arg in "$@"; do - case $arg in - mysql|mssql) - DB=$arg - ;; - --clean) - CLEAN=true - ;; - --users) - USERS_ONLY=true - ;; - --down) - DOWN_ONLY=true - ;; - *) - usage - ;; - esac -done - -[ -z "$DB" ] && usage - -# Stop container only -if [ "$DOWN_ONLY" = true ]; then - if [ "$DB" = "mysql" ]; then - echo "==> Stopping MySQL..." - podman-compose down 2>/dev/null || true - elif [ "$DB" = "mssql" ]; then - echo "==> Stopping MSSQL..." - podman-compose --profile mssql down 2>/dev/null || true - fi - echo "==> Done!" - exit 0 -fi - -# Import users only -if [ "$USERS_ONLY" = true ]; then - if [ "$DB" = "mysql" ]; then - echo "==> Importing users into MySQL..." - DB_USER=$(grep SPRING_DATASOURCE_USERNAME .env | cut -d= -f2) - DB_PASS=$(grep SPRING_DATASOURCE_PASSWORD .env | cut -d= -f2) - podman exec -i lcc-mysql-local mysql -u"${DB_USER}" -p"${DB_PASS}" lcc \ - < src/test/resources/master_data/users.sql - echo "==> Users imported!" - elif [ "$DB" = "mssql" ]; then - echo "==> Importing users into MSSQL..." - DB_PASS=$(grep DB_ROOT_PASSWORD .env.mssql | cut -d= -f2) - podman exec -e "SQLCMDPASSWORD=${DB_PASS}" lcc-mssql-local /opt/mssql-tools18/bin/sqlcmd \ - -S localhost -U sa -d lcc -C \ - -i /dev/stdin < src/test/resources/master_data/users_mssql.sql - echo "==> Users imported!" - fi - exit 0 -fi - -echo "==> Stopping all DB containers..." -podman-compose --profile mssql down 2>/dev/null || true - -if [ "$CLEAN" = true ]; then - echo "==> Deleting volumes..." - podman volume rm lcc_tool_mysql-data-local 2>/dev/null || true - podman volume rm lcc_tool_mssql-data-local 2>/dev/null || true -fi - -echo "==> Linking .env -> .env.$DB" -rm -f .env -ln -s .env.$DB .env - -# Check if volume exists (for init decision) -VOLUME_EXISTS=false -if [ "$DB" = "mysql" ]; then - podman volume exists lcc_tool_mysql-data-local 2>/dev/null && VOLUME_EXISTS=true -elif [ "$DB" = "mssql" ]; then - podman volume exists lcc_tool_mssql-data-local 2>/dev/null && VOLUME_EXISTS=true -fi - -echo "==> Starting $DB..." -if [ "$DB" = "mysql" ]; then - podman-compose up -d mysql - - echo "==> Waiting for MySQL..." - until podman exec lcc-mysql-local mysqladmin ping -h localhost --silent 2>/dev/null; do - sleep 2 - done - echo "==> MySQL is ready!" - -elif [ "$DB" = "mssql" ]; then - podman-compose --profile mssql up -d mssql - - echo "==> Waiting for MSSQL..." - until [ "$(podman inspect -f '{{.State.Health.Status}}' lcc-mssql-local 2>/dev/null)" = "healthy" ]; do - sleep 2 - done - echo "==> MSSQL is ready!" - - if [ "$VOLUME_EXISTS" = false ]; then - echo "==> New volume detected, creating database..." - DB_PASS=$(grep DB_ROOT_PASSWORD .env | cut -d= -f2) - podman exec lcc-mssql-local /opt/mssql-tools18/bin/sqlcmd \ - -S localhost -U sa -P "${DB_PASS}" -C \ - -Q "IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'lcc') CREATE DATABASE lcc" - echo "==> Database 'lcc' created!" - fi -fi - -echo "==> Done! .env points to .env.$DB" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 22f72bf..3d27214 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,8 +2,6 @@ services: mysql: image: mysql:8.4 container_name: lcc-mysql-local - env_file: - - .env.mysql environment: MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} MYSQL_DATABASE: lcc @@ -22,30 +20,6 @@ services: retries: 5 restart: unless-stopped - # MSSQL Database (optional - nur für MSSQL-Tests) - mssql: - image: mcr.microsoft.com/mssql/server:2022-latest - container_name: lcc-mssql-local - environment: - ACCEPT_EULA: "Y" - MSSQL_SA_PASSWORD: ${DB_ROOT_PASSWORD} - MSSQL_PID: "Developer" - volumes: - - mssql-data-local:/var/opt/mssql - ports: - - "1433:1433" - networks: - - lcc-network-local - healthcheck: - test: /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "$${MSSQL_SA_PASSWORD}" -Q "SELECT 1" -C || exit 1 - interval: 10s - timeout: 5s - retries: 10 - start_period: 30s - restart: unless-stopped - profiles: - - mssql # Startet nur mit: docker-compose --profile mssql up - lcc-app: #image: git.avatic.de/avatic/lcc:latest # Oder für lokales Bauen: @@ -55,7 +29,7 @@ services: mysql: condition: service_healthy env_file: - - .env.mysql + - .env environment: # Überschreibe die Datasource URL für Docker-Netzwerk SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/lcc @@ -70,7 +44,6 @@ services: volumes: mysql-data-local: - mssql-data-local: networks: lcc-network-local: diff --git a/mvnw b/mvnw old mode 100755 new mode 100644 diff --git a/pom.xml b/pom.xml index c3892d6..1de9138 100644 --- a/pom.xml +++ b/pom.xml @@ -31,17 +31,8 @@ 5.24.1 5.20.0 11.18.0 - analysis - 1.9.21 - - - io.qameta.allure - allure-junit5 - 2.29.0 - test - org.springframework.boot spring-boot-starter-jdbc @@ -99,12 +90,6 @@ mysql-connector-j runtime - - com.microsoft.sqlserver - mssql-jdbc - 12.6.1.jre11 - runtime - org.springframework.boot spring-boot-starter-test @@ -145,11 +130,6 @@ fastexcel 0.19.0 - - org.commonmark - commonmark - 0.22.0 - org.springframework.boot spring-boot-devtools @@ -193,10 +173,6 @@ org.flywaydb flyway-mysql - - org.flywaydb - flyway-sqlserver - org.glassfish.jaxb @@ -214,52 +190,6 @@ 3.2.3 - - - org.springframework.boot - spring-boot-testcontainers - test - - - org.testcontainers - testcontainers - 1.19.7 - test - - - org.testcontainers - mysql - 1.19.7 - test - - - org.testcontainers - mssqlserver - 1.19.7 - test - - - org.testcontainers - junit-jupiter - 1.19.7 - test - - - - - com.microsoft.playwright - playwright - 1.48.0 - test - - - - org.aspectj - aspectjweaver - 1.9.21 - test - - @@ -275,7 +205,6 @@ - org.codehaus.mojo versions-maven-plugin @@ -301,30 +230,15 @@ + org.apache.maven.plugins maven-surefire-plugin 3.5.4 -javaagent:${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar - -javaagent:${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar - - ${project.build.directory}/allure-results - - - ${surefire.excludedGroups} - - **/controller/**/*Test.java - - - - org.aspectj - aspectjweaver - 1.9.21 - - org.springframework.boot diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index 218eac1..e7552ea 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@phosphor-icons/vue": "^2.2.1", "@vueuse/core": "^13.6.0", + "azure-maps-control": "^3.6.1", "chart.js": "^4.5.0", "leaflet": "^1.9.4", "loglevel": "^1.9.2", @@ -42,6 +43,27 @@ "node": ">=6.0.0" } }, + "node_modules/@azure/msal-browser": { + "version": "2.39.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-2.39.0.tgz", + "integrity": "sha512-kks/n2AJzKUk+DBqZhiD+7zeQGBl+WpSOQYzWy6hff3bU0ZrYFqr4keFLlzB5VKuKZog0X59/FGHb1RPBDZLVg==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "13.3.3" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "13.3.3", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-13.3.3.tgz", + "integrity": "sha512-n278DdCXKeiWhLwhEL7/u9HRMyzhUXLefeajiknf6AmEedoiOiv2r5aRJ7LXdT3NGPyubkdIbthaJlVtmuEqvA==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -73,6 +95,7 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -957,6 +980,46 @@ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", "license": "MIT" }, + "node_modules/@mapbox/jsonlint-lines-primitives": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", + "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@mapbox/mapbox-gl-supported": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-2.0.1.tgz", + "integrity": "sha512-HP6XvfNIzfoMVfyGjBckjiAOQK9WfX0ywdLubuPMPv+Vqf5fj0uCbgBQYpiqcWZT6cbyyRnTSXDheT1ugvF6UQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@mapbox/unitbezier": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", + "license": "BSD-2-Clause" + }, + "node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "20.4.0", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.4.0.tgz", + "integrity": "sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==", + "license": "ISC", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/unitbezier": "^0.0.1", + "json-stringify-pretty-compact": "^4.0.0", + "minimist": "^1.2.8", + "quickselect": "^2.0.0", + "rw": "^1.3.3", + "tinyqueue": "^3.0.0" + }, + "bin": { + "gl-style-format": "dist/gl-style-format.mjs", + "gl-style-migrate": "dist/gl-style-migrate.mjs", + "gl-style-validate": "dist/gl-style-validate.mjs" + } + }, "node_modules/@phosphor-icons/vue": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@phosphor-icons/vue/-/vue-2.2.1.tgz", @@ -1282,6 +1345,12 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/web-bluetooth": { "version": "0.0.21", "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", @@ -1627,6 +1696,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/azure-maps-control": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/azure-maps-control/-/azure-maps-control-3.6.1.tgz", + "integrity": "sha512-EqJ96GOjUcCG9XizUbyqDu92x3KKT9C9AwRL3hmPicQjn00ql7em6RbBqJYO4nvIoH53DG6MOITj9t/zv1mQYg==", + "license": "SEE LICENSE.TXT", + "dependencies": { + "@azure/msal-browser": "^2.32.1", + "@mapbox/mapbox-gl-supported": "^2.0.1", + "@maplibre/maplibre-gl-style-spec": "^20.0.0", + "@types/geojson": "^7946.0.14" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1680,6 +1761,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001737", "electron-to-chromium": "^1.5.211", @@ -1735,6 +1817,7 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", "license": "MIT", + "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -2288,6 +2371,12 @@ "node": ">=6" } }, + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -2358,6 +2447,15 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", @@ -2602,6 +2700,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", + "license": "ISC" + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -2685,6 +2789,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -2805,6 +2915,12 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2902,6 +3018,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.4.tgz", "integrity": "sha512-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -3133,6 +3250,7 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.21.tgz", "integrity": "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.21", "@vue/compiler-sfc": "3.5.21", diff --git a/src/frontend/src/App.vue b/src/frontend/src/App.vue index 9facbb2..0c997b1 100644 --- a/src/frontend/src/App.vue +++ b/src/frontend/src/App.vue @@ -1,6 +1,5 @@