Compare commits
6 Commits
00585653c7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c80470953 | |||
| 41aea49762 | |||
| 08c9f4b8b0 | |||
| 4186190182 | |||
| 67852e6bf9 | |||
| 4fc5fe5778 |
@@ -1,9 +1,16 @@
|
||||
FROM maven:3.9-eclipse-temurin-21 AS build
|
||||
WORKDIR /app
|
||||
|
||||
# Separate layer for dependencies: only re-runs when pom.xml changes.
|
||||
# The cache mount keeps ~/.m2 across builds so even pom.xml changes
|
||||
# don't require a full re-download.
|
||||
COPY pom.xml .
|
||||
RUN mvn dependency:go-offline -B
|
||||
RUN --mount=type=cache,target=/root/.m2 \
|
||||
mvn dependency:go-offline -B -q
|
||||
|
||||
COPY src ./src
|
||||
RUN mvn package -DskipTests -B
|
||||
RUN --mount=type=cache,target=/root/.m2 \
|
||||
mvn package -DskipTests -B
|
||||
|
||||
FROM eclipse-temurin:21-jre
|
||||
WORKDIR /deployments
|
||||
|
||||
@@ -40,17 +40,18 @@
|
||||
<artifactId>quarkus-hibernate-orm-panache</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jdbc-mariadb</artifactId>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
<version>3.47.1.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hibernate.orm</groupId>
|
||||
<artifactId>hibernate-community-dialects</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-flyway</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-mysql</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-arc</artifactId>
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
# Datasource
|
||||
quarkus.datasource.db-kind=mariadb
|
||||
quarkus.datasource.username=strichliste
|
||||
quarkus.datasource.password=strichliste
|
||||
quarkus.datasource.jdbc.url=jdbc:mariadb://localhost:3306/strichliste
|
||||
# Datasource – SQLite
|
||||
quarkus.datasource.db-kind=other
|
||||
quarkus.datasource.jdbc.driver=org.sqlite.JDBC
|
||||
quarkus.datasource.jdbc.url=jdbc:sqlite:${DB_PATH:/data/qaffee.db}
|
||||
|
||||
# Agroal connection pool – evict stale connections after a DB restart
|
||||
quarkus.datasource.jdbc.background-validation-interval=30S
|
||||
quarkus.datasource.jdbc.idle-removal-interval=5M
|
||||
quarkus.datasource.jdbc.max-lifetime=10M
|
||||
# Connection Pool (SQLite: single writer)
|
||||
quarkus.datasource.jdbc.min-size=1
|
||||
quarkus.datasource.jdbc.max-size=1
|
||||
|
||||
# Hibernate
|
||||
quarkus.hibernate-orm.dialect=org.hibernate.community.dialect.SQLiteDialect
|
||||
quarkus.hibernate-orm.database.generation=none
|
||||
|
||||
# Flyway
|
||||
quarkus.flyway.migrate-at-start=true
|
||||
# Baseline: treat V1-V4 as already applied when no history table exists (post-migration start)
|
||||
quarkus.flyway.baseline-on-migrate=true
|
||||
quarkus.flyway.baseline-version=4
|
||||
# Checksums der MariaDB-Dateien passen nicht mehr zu den SQLite-kompatiblen Versionen
|
||||
quarkus.flyway.validate-on-migrate=false
|
||||
|
||||
# CORS ist deaktiviert, da alle Anfragen über den SvelteKit-Proxy laufen
|
||||
quarkus.http.cors=false
|
||||
|
||||
@@ -1,49 +1,50 @@
|
||||
CREATE TABLE company (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
id INTEGER PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE employee (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
id INTEGER PRIMARY KEY,
|
||||
company_id BIGINT NOT NULL,
|
||||
first_name VARCHAR(255) NOT NULL,
|
||||
last_name VARCHAR(255) NOT NULL,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_employee_company FOREIGN KEY (company_id) REFERENCES company(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (company_id) REFERENCES company(id)
|
||||
);
|
||||
|
||||
CREATE TABLE product (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
id INTEGER PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
price_cents INT NOT NULL DEFAULT 0,
|
||||
icon_placeholder VARCHAR(50) DEFAULT 'coffee',
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
icon_placeholder VARCHAR(255) DEFAULT 'coffee',
|
||||
active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE tally_entry (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
id INTEGER PRIMARY KEY,
|
||||
employee_id BIGINT NOT NULL,
|
||||
product_id BIGINT NOT NULL,
|
||||
month_key VARCHAR(7) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_tally_employee FOREIGN KEY (employee_id) REFERENCES employee(id),
|
||||
CONSTRAINT fk_tally_product FOREIGN KEY (product_id) REFERENCES product(id),
|
||||
INDEX idx_tally_month (month_key),
|
||||
INDEX idx_tally_employee_month (employee_id, month_key)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (employee_id) REFERENCES employee(id),
|
||||
FOREIGN KEY (product_id) REFERENCES product(id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_tally_month ON tally_entry(month_key);
|
||||
CREATE INDEX idx_tally_employee_month ON tally_entry(employee_id, month_key);
|
||||
|
||||
CREATE TABLE access_link (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
id INTEGER PRIMARY KEY,
|
||||
token VARCHAR(64) NOT NULL UNIQUE,
|
||||
role VARCHAR(20) NOT NULL,
|
||||
role VARCHAR(50) NOT NULL,
|
||||
company_id BIGINT,
|
||||
description VARCHAR(255),
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_access_link_company FOREIGN KEY (company_id) REFERENCES company(id),
|
||||
CONSTRAINT chk_role CHECK (role IN ('COMPANY_ADMIN', 'PROVIDER_ADMIN'))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
description TEXT,
|
||||
active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (company_id) REFERENCES company(id),
|
||||
CHECK (role IN ('COMPANY_ADMIN', 'PROVIDER_ADMIN'))
|
||||
);
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
ALTER TABLE company ADD COLUMN logo MEDIUMBLOB;
|
||||
ALTER TABLE company ADD COLUMN logo_content_type VARCHAR(50);
|
||||
ALTER TABLE company ADD COLUMN logo_content_type VARCHAR(255);
|
||||
|
||||
6
build.env
Normal file
6
build.env
Normal file
@@ -0,0 +1,6 @@
|
||||
REGISTRY='push.registry.cloud.aquantico.de'
|
||||
IMAGE_NAME='qaffee'
|
||||
IMAGE_VERSION='1.0.8'
|
||||
SERVICES=(backend frontend)
|
||||
SNAPSHOT_TAG='SNAPSHOT'
|
||||
platform=linux/amd64,linux/arm64
|
||||
13
build.sh
13
build.sh
@@ -28,7 +28,7 @@ set -euo pipefail
|
||||
#cd to script dir
|
||||
pushd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null
|
||||
SCRIPT_DIR="$(pwd)"
|
||||
source "${SCRIPT_DIR}/.env"
|
||||
source "${SCRIPT_DIR}/build.env"
|
||||
# read from .env:
|
||||
# REGISTRY, IMAGE_NAME, IMAGE_VERSION, SNAPSHOT_TAG
|
||||
# SERVICES (subfolder name = image suffix)
|
||||
@@ -48,8 +48,8 @@ usage() {
|
||||
# --- parse arguments -------------------------------------------------------
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-s|--snapshot) VERSION="${IMAGE_TAG}-SNAPSHOT$(git rev-parse --short HEAD 2>/dev/null || echo "local")"; shift ;;
|
||||
-r|--release) VERSION="${IMAGE_TAG}"; shift ;;
|
||||
-s|--snapshot) VERSION="${IMAGE_VERSION}-SNAPSHOT$(git rev-parse --short HEAD 2>/dev/null || echo "local")"; shift ;;
|
||||
-r|--release) VERSION="${IMAGE_VERSION}"; shift ;;
|
||||
-v|--version) VERSION="$2"; shift 2 ;;
|
||||
-p|--push) PUSH=true; shift ;;
|
||||
-o|--only) ONLY+=("$2"); shift 2 ;;
|
||||
@@ -77,7 +77,12 @@ for SERVICE in "${SERVICES[@]}"; do
|
||||
IMAGE="${REGISTRY}/${IMAGE_NAME}-${SERVICE}:${VERSION}"
|
||||
echo ""
|
||||
echo ">>> Building ${SERVICE}: ${IMAGE}"
|
||||
docker build --tag "${IMAGE}" "${SCRIPT_DIR}/${SERVICE}"
|
||||
|
||||
docker buildx build \
|
||||
--platform ${platform} \
|
||||
-t "${IMAGE}" \
|
||||
$($PUSH && echo "--push" || echo "") \
|
||||
"${SCRIPT_DIR}/${SERVICE}"
|
||||
echo " ${SERVICE} built successfully."
|
||||
done
|
||||
|
||||
|
||||
BIN
data/qaffee.db
Normal file
BIN
data/qaffee.db
Normal file
Binary file not shown.
63
db/mariadb_export.sql
Normal file
63
db/mariadb_export.sql
Normal file
File diff suppressed because one or more lines are too long
104
db/migrate_to_sqlite.py
Normal file
104
db/migrate_to_sqlite.py
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migrate MariaDB export to SQLite for qaffee.
|
||||
|
||||
Usage:
|
||||
python3 db/migrate_to_sqlite.py
|
||||
|
||||
Creates data/qaffee.db ready for use with the Quarkus backend.
|
||||
Flyway will baseline at V4 on first startup (no re-migration needed).
|
||||
"""
|
||||
|
||||
import re
|
||||
import sqlite3
|
||||
import os
|
||||
import sys
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
REPO_ROOT = os.path.join(SCRIPT_DIR, '..')
|
||||
MIGRATION_DIR = os.path.join(REPO_ROOT, 'backend', 'src', 'main', 'resources', 'db', 'migration')
|
||||
EXPORT_FILE = os.path.join(SCRIPT_DIR, 'mariadb_export.sql')
|
||||
OUTPUT_FILE = os.path.join(REPO_ROOT, 'data', 'qaffee.db')
|
||||
|
||||
# V2 (seed data) is intentionally skipped – it inserts dev/test rows that
|
||||
# conflict with production IDs in the export.
|
||||
MIGRATIONS = [
|
||||
'V1__initial_schema.sql',
|
||||
'V3__company_logo.sql',
|
||||
'V4__tally_price_at_booking.sql',
|
||||
]
|
||||
|
||||
# Tables to import in FK-safe order (parents before children)
|
||||
DATA_TABLES = ['company', 'employee', 'product', 'access_link', 'tally_entry']
|
||||
|
||||
|
||||
def apply_migrations(conn: sqlite3.Connection) -> None:
|
||||
for filename in MIGRATIONS:
|
||||
path = os.path.join(MIGRATION_DIR, filename)
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
sql = f.read()
|
||||
conn.executescript(sql)
|
||||
print(f' schema {filename}')
|
||||
|
||||
|
||||
def mariadb_to_sqlite_sql(sql: str) -> str:
|
||||
# Remove MariaDB sandbox-mode comment at the top
|
||||
sql = re.sub(r'/\*M!.*?\*/', '', sql, flags=re.DOTALL)
|
||||
# Strip backtick identifier quotes
|
||||
sql = sql.replace('`', '')
|
||||
# Convert 0xABCD hex literals to SQLite X'ABCD' blob literals
|
||||
sql = re.sub(r'\b0x([0-9A-Fa-f]+)', r"X'\1'", sql)
|
||||
return sql
|
||||
|
||||
|
||||
def extract_insert(sql: str, table: str) -> str | None:
|
||||
"""Return the full INSERT INTO <table> VALUES ...; statement, or None."""
|
||||
pattern = rf'(INSERT INTO {re.escape(table)} VALUES\s*.*?;)'
|
||||
match = re.search(pattern, sql, re.DOTALL)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def main() -> None:
|
||||
os.makedirs(os.path.dirname(OUTPUT_FILE), exist_ok=True)
|
||||
|
||||
if os.path.exists(OUTPUT_FILE):
|
||||
os.remove(OUTPUT_FILE)
|
||||
print(f'Removed existing {OUTPUT_FILE}')
|
||||
|
||||
print('Reading MariaDB export ...')
|
||||
with open(EXPORT_FILE, 'r', encoding='utf-8') as f:
|
||||
raw_sql = f.read()
|
||||
|
||||
sqlite_sql = mariadb_to_sqlite_sql(raw_sql)
|
||||
|
||||
print('Creating SQLite database ...')
|
||||
conn = sqlite3.connect(OUTPUT_FILE)
|
||||
|
||||
print('Applying schema migrations:')
|
||||
apply_migrations(conn)
|
||||
|
||||
print('Importing data:')
|
||||
for table in DATA_TABLES:
|
||||
stmt = extract_insert(sqlite_sql, table)
|
||||
if stmt is None:
|
||||
print(f' skip {table} (no data in export)')
|
||||
continue
|
||||
conn.executescript(stmt)
|
||||
# Count rows for feedback
|
||||
count = conn.execute(f'SELECT COUNT(*) FROM {table}').fetchone()[0]
|
||||
print(f' import {table} ({count} rows)')
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
size_kb = os.path.getsize(OUTPUT_FILE) // 1024
|
||||
print(f'\nDone – {OUTPUT_FILE} ({size_kb} KB)')
|
||||
print()
|
||||
print('Next steps:')
|
||||
print(' 1. docker compose up --build')
|
||||
print(' Flyway will baseline at V4 and skip all migrations.')
|
||||
print(' 2. Verify at https://qaffee.cloud.aquantico.de')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,34 +1,12 @@
|
||||
version: "3"
|
||||
volumes:
|
||||
db-data:
|
||||
services:
|
||||
db:
|
||||
image: mariadb:11.4.4
|
||||
container_name: qaffee-db
|
||||
restart: always
|
||||
command: --log-warnings=3
|
||||
environment:
|
||||
- "MARIADB_ROOT_PASSWORD=rootpassword"
|
||||
- "MARIADB_DATABASE=strichliste"
|
||||
- "MARIADB_USER=strichliste"
|
||||
- "MARIADB_PASSWORD=strichliste"
|
||||
volumes:
|
||||
- db-data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- traefik-net
|
||||
backend:
|
||||
build: ./backend
|
||||
container_name: qaffee-backend
|
||||
environment:
|
||||
- "QUARKUS_DATASOURCE_JDBC_URL=jdbc:mariadb://db:3306/strichliste"
|
||||
- "QUARKUS_DATASOURCE_USERNAME=strichliste"
|
||||
- "QUARKUS_DATASOURCE_PASSWORD=strichliste"
|
||||
- "QUARKUS_HTTP_CORS_ORIGINS=https://qaffee.cloud.aquantico.de"
|
||||
volumes:
|
||||
- ./data:/data
|
||||
labels:
|
||||
traefik.enable: "true"
|
||||
traefik.docker.network: "traefik-net"
|
||||
@@ -37,9 +15,6 @@ services:
|
||||
traefik.http.routers.qaffee-backend.tls: "true"
|
||||
traefik.http.routers.qaffee-backend.tls.certresolver: "myresolver"
|
||||
traefik.http.services.qaffee-backend.loadbalancer.server.port: 8080
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- traefik-net
|
||||
frontend:
|
||||
@@ -61,22 +36,6 @@ services:
|
||||
- backend
|
||||
networks:
|
||||
- traefik-net
|
||||
backup:
|
||||
image: mariadb:11.4.4
|
||||
container_name: qaffee-backup
|
||||
environment:
|
||||
- "MARIADB_HOST=db"
|
||||
- "MARIADB_USER=strichliste"
|
||||
- "MARIADB_PASSWORD=strichliste"
|
||||
volumes:
|
||||
- ./backups:/backups
|
||||
- ./db/backup.sh:/backup.sh
|
||||
entrypoint: ["/bin/bash", "/backup.sh"]
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- traefik-net
|
||||
networks:
|
||||
traefik-net:
|
||||
external: false
|
||||
@@ -5,10 +5,17 @@
|
||||
|
||||
let companies: Company[] = [];
|
||||
let loading = true;
|
||||
let error = false;
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
companies = await api.getCompanies();
|
||||
} catch {
|
||||
error = true;
|
||||
setTimeout(() => location.reload(), 5000);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -22,6 +29,10 @@
|
||||
<div style="text-align: center; padding: 48px;">
|
||||
<p>Laden...</p>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div style="text-align: center; padding: 48px;">
|
||||
<p style="color: var(--color-warning);">Verbindungsfehler – Seite wird in 5 Sekunden neu geladen…</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="card-grid" style="padding: 24px;">
|
||||
{#each companies as company}
|
||||
|
||||
Reference in New Issue
Block a user