v1.5.0: utf8mb4-Migration, Rollen, phpMyAdmin, DB-Bereinigung
- Datenbank auf utf8mb4_unicode_ci migriert (migrate_to_utf8mb4.sh) - beos: Spalte 'role' (kommagetrennte Rollen: guide, admin, key, deleted) - BEO-Auswahl im Formular filtert nur noch role='guide' - logbuch_objekte: ObjektName-Spalte entfernt, stattdessen JOIN auf objekte - lib/db.ts: charset utf8mb4 in Connection-Pool - Session und Auth um role-Feld erweitert - compose.yml: phpMyAdmin mit Traefik unter /myadmin - compose.yml: MySQL auf 127.0.0.1:3336 für SSH-Tunnel (lokale Entwicklung) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,7 +18,7 @@ Next.js 16 App Router application. All pages are server components; interactive
|
|||||||
|
|
||||||
**Auth flow**: Users come from the existing MySQL `beos` table (not a separate users table). Login via `app/login/actions.ts` → `lib/auth.ts` (bcryptjs). Sessions are JWT cookies via jose (`lib/session.ts`, 1-hour expiry). If `pw IS NULL`, the default password is `logbuch123` and `mustChangePassword` is forced to `true`. The middleware file exports `proxy` (not `middleware`) — Next.js 16 requirement.
|
**Auth flow**: Users come from the existing MySQL `beos` table (not a separate users table). Login via `app/login/actions.ts` → `lib/auth.ts` (bcryptjs). Sessions are JWT cookies via jose (`lib/session.ts`, 1-hour expiry). If `pw IS NULL`, the default password is `logbuch123` and `mustChangePassword` is forced to `true`. The middleware file exports `proxy` (not `middleware`) — Next.js 16 requirement.
|
||||||
|
|
||||||
**Database**: MySQL, database name `sternwarte`, via `lib/db.ts` connection pool. The pre-existing `beos` table has non-standard columns: `` `kürzel` `` (umlaut → always needs backticks), `pw`, `id` (all lowercase). The DB charset is **latin1** — avoid non-ASCII characters in SQL WHERE clauses; use `LIKE 'Ascii%'` prefix patterns instead.
|
**Database**: MySQL, database name `sternwarte`, via `lib/db.ts` connection pool. The pre-existing `beos` table has non-standard columns: `` `kürzel` `` (umlaut → always needs backticks), `pw`, `id` (all lowercase). The DB charset is **utf8mb4** (collation `utf8mb4_unicode_ci`); connection pool uses `charset: 'utf8mb4'`.
|
||||||
|
|
||||||
**SQL in JS**: MySQL backticks inside JS template literals cause parse errors. Write complex queries using string concatenation (`+`), not template literals. `LIMIT` cannot be a parameterized placeholder in complex grouped queries — embed it directly after validating: `LIST_SQL + \` LIMIT ${limit}\``.
|
**SQL in JS**: MySQL backticks inside JS template literals cause parse errors. Write complex queries using string concatenation (`+`), not template literals. `LIMIT` cannot be a parameterized placeholder in complex grouped queries — embed it directly after validating: `LIST_SQL + \` LIMIT ${limit}\``.
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export async function GET() {
|
|||||||
if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 });
|
if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 });
|
||||||
try {
|
try {
|
||||||
const rows = await query(
|
const rows = await query(
|
||||||
'SELECT id AS ID, `kürzel` AS Kuerzel, CONCAT(IFNULL(vorname, \'\'), IF(vorname IS NOT NULL, \' \', \'\'), name) AS Name FROM beos WHERE `kürzel` IS NOT NULL ORDER BY name ASC'
|
'SELECT id AS ID, `kürzel` AS Kuerzel, CONCAT(IFNULL(vorname, \'\'), IF(vorname IS NOT NULL, \' \', \'\'), name) AS Name FROM beos WHERE `kürzel` IS NOT NULL AND FIND_IN_SET(\'guide\', role) > 0 ORDER BY name ASC'
|
||||||
) as { ID: number; Kuerzel: string; Name: string }[];
|
) as { ID: number; Kuerzel: string; Name: string }[];
|
||||||
return NextResponse.json(rows);
|
return NextResponse.json(rows);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|||||||
for (const obj of (objekte as SelectedObjekt[]) || []) {
|
for (const obj of (objekte as SelectedObjekt[]) || []) {
|
||||||
let objektId = obj.ID;
|
let objektId = obj.ID;
|
||||||
if (!objektId) {
|
if (!objektId) {
|
||||||
const existing = await query('SELECT ID FROM objekte WHERE Name = ?', [obj.Name]) as { ID: number }[];
|
const existing = await query('SELECT ID, Name FROM objekte WHERE LOWER(Name) = LOWER(?)', [obj.Name]) as { ID: number; Name: string }[];
|
||||||
if (existing[0]) {
|
if (existing[0]) {
|
||||||
objektId = existing[0].ID;
|
objektId = existing[0].ID;
|
||||||
} else {
|
} else {
|
||||||
@@ -50,8 +50,8 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|||||||
}
|
}
|
||||||
await query('UPDATE objekte SET LastUsed = NOW() WHERE ID = ?', [objektId]);
|
await query('UPDATE objekte SET LastUsed = NOW() WHERE ID = ?', [objektId]);
|
||||||
await query(
|
await query(
|
||||||
'INSERT INTO logbuch_objekte (LogbuchID, ObjektID, ObjektName) VALUES (?, ?, ?)',
|
'INSERT INTO logbuch_objekte (LogbuchID, ObjektID) VALUES (?, ?)',
|
||||||
[logbuchId, objektId, obj.Name]
|
[logbuchId, objektId]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ const LIST_SQL =
|
|||||||
' l.WetterTemp, l.WetterFeuchte, l.WetterDruck,' +
|
' l.WetterTemp, l.WetterFeuchte, l.WetterDruck,' +
|
||||||
' l.created_by, l.created_at,' +
|
' l.created_by, l.created_at,' +
|
||||||
" GROUP_CONCAT(DISTINCT bk.kuerzel ORDER BY bk.kuerzel SEPARATOR ', ') AS BEOs," +
|
" GROUP_CONCAT(DISTINCT bk.kuerzel ORDER BY bk.kuerzel SEPARATOR ', ') AS BEOs," +
|
||||||
" GROUP_CONCAT(DISTINCT lo.ObjektName ORDER BY lo.ObjektName SEPARATOR ', ') AS Objekte" +
|
" GROUP_CONCAT(DISTINCT o.Name ORDER BY o.Name SEPARATOR ', ') AS Objekte" +
|
||||||
' FROM logbuch l' +
|
' FROM logbuch l' +
|
||||||
' LEFT JOIN logbuch_beos lb ON lb.LogbuchID = l.ID' +
|
' LEFT JOIN logbuch_beos lb ON lb.LogbuchID = l.ID' +
|
||||||
' LEFT JOIN (SELECT id, `kürzel` AS kuerzel FROM beos) bk ON bk.id = lb.BeoID' +
|
' LEFT JOIN (SELECT id, `kürzel` AS kuerzel FROM beos) bk ON bk.id = lb.BeoID' +
|
||||||
' LEFT JOIN logbuch_objekte lo ON lo.LogbuchID = l.ID' +
|
' LEFT JOIN logbuch_objekte lo ON lo.LogbuchID = l.ID' +
|
||||||
|
' LEFT JOIN objekte o ON o.ID = lo.ObjektID' +
|
||||||
' WHERE l.Kuppel = ?' +
|
' WHERE l.Kuppel = ?' +
|
||||||
' GROUP BY l.ID' +
|
' GROUP BY l.ID' +
|
||||||
' ORDER BY l.Beginn DESC';
|
' ORDER BY l.Beginn DESC';
|
||||||
@@ -67,7 +68,7 @@ export async function POST(request: NextRequest) {
|
|||||||
for (const obj of (objekte as SelectedObjekt[]) || []) {
|
for (const obj of (objekte as SelectedObjekt[]) || []) {
|
||||||
let objektId = obj.ID;
|
let objektId = obj.ID;
|
||||||
if (!objektId) {
|
if (!objektId) {
|
||||||
const existing = await query('SELECT ID FROM objekte WHERE Name = ?', [obj.Name]) as { ID: number }[];
|
const existing = await query('SELECT ID, Name FROM objekte WHERE LOWER(Name) = LOWER(?)', [obj.Name]) as { ID: number; Name: string }[];
|
||||||
if (existing[0]) {
|
if (existing[0]) {
|
||||||
objektId = existing[0].ID;
|
objektId = existing[0].ID;
|
||||||
} else {
|
} else {
|
||||||
@@ -77,8 +78,8 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
await query('UPDATE objekte SET LastUsed = NOW() WHERE ID = ?', [objektId]);
|
await query('UPDATE objekte SET LastUsed = NOW() WHERE ID = ?', [objektId]);
|
||||||
await query(
|
await query(
|
||||||
'INSERT INTO logbuch_objekte (LogbuchID, ObjektID, ObjektName) VALUES (?, ?, ?)',
|
'INSERT INTO logbuch_objekte (LogbuchID, ObjektID) VALUES (?, ?)',
|
||||||
[logbuchId, objektId, obj.Name]
|
[logbuchId, objektId]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export async function changePassword(
|
|||||||
beoName: session.beoName,
|
beoName: session.beoName,
|
||||||
mustChangePassword: false,
|
mustChangePassword: false,
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
|
role: session.role ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
redirect('/');
|
redirect('/');
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export async function login(
|
|||||||
beoName: getBeoDisplayName(result.beo),
|
beoName: getBeoDisplayName(result.beo),
|
||||||
mustChangePassword: mustChange,
|
mustChangePassword: mustChange,
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
|
role: result.beo.role ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (mustChange) {
|
if (mustChange) {
|
||||||
|
|||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
services:
|
||||||
|
logbuch_mysql:
|
||||||
|
image: mysql:lts
|
||||||
|
container_name: logbuch_mysql
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS}
|
||||||
|
MYSQL_DATABASE: ${DB_NAME}
|
||||||
|
MYSQL_USER: ${DB_USER}
|
||||||
|
MYSQL_PASSWORD: ${DB_PASS}
|
||||||
|
volumes:
|
||||||
|
- db_data:/var/lib/mysql
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:3336:3306"
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- mysqladmin
|
||||||
|
- ping
|
||||||
|
- -h
|
||||||
|
- localhost
|
||||||
|
- -uroot
|
||||||
|
- -p${DB_ROOT_PASS}
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
# Kein Port nach außen — nur internes Netzwerk
|
||||||
|
|
||||||
|
logbuch_phpmyadmin:
|
||||||
|
image: phpmyadmin:latest
|
||||||
|
container_name: logbuch_phpmyadmin
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
PMA_HOST: logbuch_mysql
|
||||||
|
PMA_PORT: 3306
|
||||||
|
PMA_ABSOLUTE_URI: https://logbuch.fuerst-stuttgart.de/myadmin/
|
||||||
|
depends_on:
|
||||||
|
logbuch_mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.logbuch-pma.entrypoints=http
|
||||||
|
- traefik.http.routers.logbuch-pma.rule=Host(`logbuch.fuerst-stuttgart.de`) && PathPrefix(`/myadmin`)
|
||||||
|
- traefik.http.middlewares.logbuch-pma-https-redirect.redirectscheme.scheme=https
|
||||||
|
- traefik.http.routers.logbuch-pma.middlewares=logbuch-pma-https-redirect
|
||||||
|
- traefik.http.routers.logbuch-pma-secure.entrypoints=https
|
||||||
|
- traefik.http.routers.logbuch-pma-secure.rule=Host(`logbuch.fuerst-stuttgart.de`) && PathPrefix(`/myadmin`)
|
||||||
|
- traefik.http.routers.logbuch-pma-secure.tls=true
|
||||||
|
- traefik.http.routers.logbuch-pma-secure.middlewares=logbuch-pma-slash,logbuch-pma-strip
|
||||||
|
- traefik.http.middlewares.logbuch-pma-slash.redirectregex.regex=^https://logbuch\.fuerst-stuttgart\.de/myadmin$$
|
||||||
|
- traefik.http.middlewares.logbuch-pma-slash.redirectregex.replacement=https://logbuch.fuerst-stuttgart.de/myadmin/
|
||||||
|
- traefik.http.middlewares.logbuch-pma-strip.stripprefix.prefixes=/myadmin
|
||||||
|
- traefik.http.routers.logbuch-pma-secure.service=logbuch-pma
|
||||||
|
- traefik.http.services.logbuch-pma.loadbalancer.server.port=80
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
|
||||||
|
logbuch_app:
|
||||||
|
image: docker.citysensor.de/logbuch:latest
|
||||||
|
container_name: logbuch_app
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
DB_HOST: logbuch_mysql
|
||||||
|
DB_USER: ${DB_USER}
|
||||||
|
DB_PASS: ${DB_PASS}
|
||||||
|
DB_NAME: ${DB_NAME}
|
||||||
|
DB_PORT: 3306
|
||||||
|
AUTH_SECRET: ${AUTH_SECRET}
|
||||||
|
NODE_ENV: production
|
||||||
|
ports:
|
||||||
|
- 127.0.0.1:${APP_PORT:-3000}:3000
|
||||||
|
depends_on:
|
||||||
|
logbuch_mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.logbuch.entrypoints=http
|
||||||
|
- traefik.http.routers.logbuch.rule=Host(`logbuch.fuerst-stuttgart.de`)
|
||||||
|
- traefik.http.middlewares.logbuch-https-redirect.redirectscheme.scheme=https
|
||||||
|
- traefik.http.routers.logbuch.middlewares=logbuch-https-redirect
|
||||||
|
- traefik.http.routers.logbuch-secure.entrypoints=https
|
||||||
|
- traefik.http.routers.logbuch-secure.rule=Host(`logbuch.fuerst-stuttgart.de`)
|
||||||
|
- traefik.http.routers.logbuch-secure.tls=true
|
||||||
|
- traefik.http.routers.logbuch-secure.service=logbuch
|
||||||
|
- traefik.http.services.logbuch.loadbalancer.server.port=3000
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
- gitea-internal
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
name: dockge_default
|
||||||
|
external: true
|
||||||
|
gitea-internal:
|
||||||
|
name: gitea_gitea-internal
|
||||||
|
external: true
|
||||||
|
volumes:
|
||||||
|
db_data: null
|
||||||
@@ -20,6 +20,22 @@ services:
|
|||||||
retries: 10
|
retries: 10
|
||||||
# Kein Port nach außen — nur internes Netzwerk
|
# Kein Port nach außen — nur internes Netzwerk
|
||||||
|
|
||||||
|
logbuch_phpmyadmin:
|
||||||
|
image: phpmyadmin:latest
|
||||||
|
container_name: logbuch_phpmyadmin
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
PMA_HOST: logbuch_mysql
|
||||||
|
PMA_PORT: 3306
|
||||||
|
PMA_ABSOLUTE_URI: https://logbuch.fuerst-stuttgart.de/myadmin/
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${PMA_PORT:-8080}:80"
|
||||||
|
depends_on:
|
||||||
|
logbuch_mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- logbuch_net
|
||||||
|
|
||||||
logbuch_app:
|
logbuch_app:
|
||||||
image: docker.citysensor.de/logbuch:latest
|
image: docker.citysensor.de/logbuch:latest
|
||||||
container_name: logbuch_app
|
container_name: logbuch_app
|
||||||
|
|||||||
+2
-1
@@ -8,11 +8,12 @@ export interface Beo {
|
|||||||
kürzel: string | null;
|
kürzel: string | null;
|
||||||
pw: string | null;
|
pw: string | null;
|
||||||
MustChangePassword: number;
|
MustChangePassword: number;
|
||||||
|
role: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getBeoByKuerzel(kuerzel: string): Promise<Beo | null> {
|
export async function getBeoByKuerzel(kuerzel: string): Promise<Beo | null> {
|
||||||
const rows = await query(
|
const rows = await query(
|
||||||
'SELECT id, name, vorname, `kürzel`, pw, MustChangePassword FROM beos WHERE `kürzel` = ?',
|
'SELECT id, name, vorname, `kürzel`, pw, MustChangePassword, role FROM beos WHERE `kürzel` = ?',
|
||||||
[kuerzel]
|
[kuerzel]
|
||||||
) as Beo[];
|
) as Beo[];
|
||||||
return rows[0] ?? null;
|
return rows[0] ?? null;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const dbConfig = {
|
|||||||
user: process.env.DB_USER,
|
user: process.env.DB_USER,
|
||||||
password: process.env.DB_PASS,
|
password: process.env.DB_PASS,
|
||||||
database: process.env.DB_NAME || 'logbuch',
|
database: process.env.DB_NAME || 'logbuch',
|
||||||
|
charset: 'utf8mb4',
|
||||||
waitForConnections: true,
|
waitForConnections: true,
|
||||||
connectionLimit: 10,
|
connectionLimit: 10,
|
||||||
queueLimit: 0,
|
queueLimit: 0,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface SessionData {
|
|||||||
mustChangePassword: boolean;
|
mustChangePassword: boolean;
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
expiresAt: number;
|
expiresAt: number;
|
||||||
|
role: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function encrypt(payload: SessionData): Promise<string> {
|
async function encrypt(payload: SessionData): Promise<string> {
|
||||||
|
|||||||
Executable
+147
@@ -0,0 +1,147 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Migriert die Datenbank von latin1 auf utf8mb4.
|
||||||
|
# Läuft auf dem Server im Verzeichnis der compose.yml.
|
||||||
|
# Voraussetzung: iconv installiert (apt install libc-bin)
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
CONTAINER="logbuch_mysql"
|
||||||
|
DB="sternwarte"
|
||||||
|
DUMP_LATIN1="/tmp/sternwarte_latin1.sql"
|
||||||
|
DUMP_UTF8="/tmp/sternwarte_utf8mb4.sql"
|
||||||
|
|
||||||
|
# Root-Passwort aus .env lesen
|
||||||
|
ROOT_PASS=$(grep DB_ROOT_PASS .env | cut -d= -f2)
|
||||||
|
if [ -z "$ROOT_PASS" ]; then
|
||||||
|
echo "FEHLER: DB_ROOT_PASS nicht in .env gefunden." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "══════════════════════════════════════════════════════"
|
||||||
|
echo " latin1 → utf8mb4 Migration: $DB"
|
||||||
|
echo "══════════════════════════════════════════════════════"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── Sicherheits-Backup ───────────────────────────────────────────────────────
|
||||||
|
BACKUP="/tmp/sternwarte_backup_$(date +%Y%m%d_%H%M%S).sql"
|
||||||
|
echo ">>> Erstelle Backup: $BACKUP"
|
||||||
|
docker exec "$CONTAINER" mysqldump \
|
||||||
|
-u root -p"$ROOT_PASS" \
|
||||||
|
--default-character-set=latin1 \
|
||||||
|
--single-transaction \
|
||||||
|
--no-tablespaces \
|
||||||
|
--set-gtid-purged=OFF \
|
||||||
|
"$DB" > "$BACKUP"
|
||||||
|
echo " Backup gespeichert: $BACKUP"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── Dump als latin1 exportieren ──────────────────────────────────────────────
|
||||||
|
echo ">>> Exportiere Daten mit latin1-Zeichensatz..."
|
||||||
|
docker exec "$CONTAINER" mysqldump \
|
||||||
|
-u root -p"$ROOT_PASS" \
|
||||||
|
--default-character-set=latin1 \
|
||||||
|
--single-transaction \
|
||||||
|
--no-tablespaces \
|
||||||
|
--skip-set-charset \
|
||||||
|
--set-gtid-purged=OFF \
|
||||||
|
"$DB" > "$DUMP_LATIN1"
|
||||||
|
echo " Dump: $DUMP_LATIN1"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── Bytes latin1 → utf8 konvertieren ────────────────────────────────────────
|
||||||
|
echo ">>> Konvertiere Bytes: latin1 → utf8..."
|
||||||
|
iconv -f latin1 -t utf8 "$DUMP_LATIN1" > "$DUMP_UTF8"
|
||||||
|
|
||||||
|
# Charset-Deklarationen im SQL ersetzen
|
||||||
|
sed -i \
|
||||||
|
-e 's/DEFAULT CHARSET=latin1/DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci/g' \
|
||||||
|
-e 's/COLLATE=latin1_swedish_ci//g' \
|
||||||
|
-e 's/CHARACTER SET latin1/CHARACTER SET utf8mb4/g' \
|
||||||
|
-e 's/COLLATE latin1_swedish_ci/COLLATE utf8mb4_unicode_ci/g' \
|
||||||
|
"$DUMP_UTF8"
|
||||||
|
|
||||||
|
echo " Konvertiert: $DUMP_UTF8"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── Zeilenzähler vor Migration ───────────────────────────────────────────────
|
||||||
|
echo ">>> Zeilenzähler vor Migration:"
|
||||||
|
for TABLE in beos objekte logbuch logbuch_beos logbuch_objekte; do
|
||||||
|
COUNT=$(docker exec "$CONTAINER" mysql -u root -p"$ROOT_PASS" -sN \
|
||||||
|
-e "SELECT COUNT(*) FROM $TABLE;" "$DB" 2>/dev/null || echo "n/a")
|
||||||
|
printf " %-25s %5s Zeilen\n" "$TABLE" "$COUNT"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── Zieldatenbank anlegen ────────────────────────────────────────────────────
|
||||||
|
echo ">>> Lege Zieldatenbank an (utf8mb4)..."
|
||||||
|
docker exec -i "$CONTAINER" mysql -u root -p"$ROOT_PASS" <<EOF
|
||||||
|
DROP DATABASE IF EXISTS ${DB}_new;
|
||||||
|
CREATE DATABASE ${DB}_new CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ── utf8mb4-Dump importieren ─────────────────────────────────────────────────
|
||||||
|
echo ">>> Importiere utf8mb4-Daten..."
|
||||||
|
docker exec -i "$CONTAINER" mysql \
|
||||||
|
-u root -p"$ROOT_PASS" \
|
||||||
|
--default-character-set=utf8mb4 \
|
||||||
|
"${DB}_new" < "$DUMP_UTF8"
|
||||||
|
|
||||||
|
# ── Zeilenzähler nach Migration ──────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo ">>> Zeilenzähler nach Migration:"
|
||||||
|
ALL_OK=true
|
||||||
|
for TABLE in beos objekte logbuch logbuch_beos logbuch_objekte; do
|
||||||
|
BEFORE=$(docker exec "$CONTAINER" mysql -u root -p"$ROOT_PASS" -sN \
|
||||||
|
-e "SELECT COUNT(*) FROM $TABLE;" "${DB}" 2>/dev/null || echo "?")
|
||||||
|
AFTER=$(docker exec "$CONTAINER" mysql -u root -p"$ROOT_PASS" -sN \
|
||||||
|
-e "SELECT COUNT(*) FROM $TABLE;" "${DB}_new" 2>/dev/null || echo "?")
|
||||||
|
STATUS="✓"
|
||||||
|
[ "$BEFORE" != "$AFTER" ] && STATUS="✗ ABWEICHUNG" && ALL_OK=false
|
||||||
|
printf " %-25s %5s → %5s %s\n" "$TABLE" "$BEFORE" "$AFTER" "$STATUS"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ "$ALL_OK" = false ]; then
|
||||||
|
echo "FEHLER: Zeilenzähler stimmen nicht überein!" >&2
|
||||||
|
echo "Datenbank '${DB}_new' bleibt zur manuellen Prüfung erhalten." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Swap: sternwarte → sternwarte_old, sternwarte_new → sternwarte ───────────
|
||||||
|
echo ">>> Alle Zeilenzähler stimmen — tausche Datenbanken..."
|
||||||
|
|
||||||
|
TABLES=$(docker exec "$CONTAINER" mysql -u root -p"$ROOT_PASS" -sN \
|
||||||
|
-e "SHOW TABLES;" "${DB}_new")
|
||||||
|
|
||||||
|
# Alte DB umbenennen (Tabellen nach sternwarte_old verschieben)
|
||||||
|
docker exec "$CONTAINER" mysql -u root -p"$ROOT_PASS" -e \
|
||||||
|
"CREATE DATABASE IF NOT EXISTS ${DB}_old CHARACTER SET latin1;"
|
||||||
|
for TABLE in $TABLES; do
|
||||||
|
docker exec "$CONTAINER" mysql -u root -p"$ROOT_PASS" -e \
|
||||||
|
"RENAME TABLE \`${DB}\`.\`$TABLE\` TO \`${DB}_old\`.\`$TABLE\`;"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Neue DB nach sternwarte verschieben
|
||||||
|
docker exec "$CONTAINER" mysql -u root -p"$ROOT_PASS" -e \
|
||||||
|
"ALTER DATABASE ${DB} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
||||||
|
for TABLE in $TABLES; do
|
||||||
|
docker exec "$CONTAINER" mysql -u root -p"$ROOT_PASS" -e \
|
||||||
|
"RENAME TABLE \`${DB}_new\`.\`$TABLE\` TO \`${DB}\`.\`$TABLE\`;"
|
||||||
|
done
|
||||||
|
|
||||||
|
docker exec "$CONTAINER" mysql -u root -p"$ROOT_PASS" -e \
|
||||||
|
"DROP DATABASE ${DB}_new; DROP DATABASE ${DB}_old;"
|
||||||
|
|
||||||
|
# ── Kollation bestätigen ─────────────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo ">>> Kollation der Tabellen:"
|
||||||
|
docker exec "$CONTAINER" mysql -u root -p"$ROOT_PASS" -e \
|
||||||
|
"SELECT TABLE_NAME, TABLE_COLLATION FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = '$DB' ORDER BY TABLE_NAME;" 2>/dev/null
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "══════════════════════════════════════════════════════"
|
||||||
|
echo " Migration erfolgreich abgeschlossen!"
|
||||||
|
echo " Backup: $BACKUP"
|
||||||
|
echo " App neu starten: docker compose up -d logbuch_app"
|
||||||
|
echo "══════════════════════════════════════════════════════"
|
||||||
Reference in New Issue
Block a user