diff --git a/ANLEITUNG.md b/ANLEITUNG.md
index 11f2925..d021e46 100644
--- a/ANLEITUNG.md
+++ b/ANLEITUNG.md
@@ -19,7 +19,7 @@ Die App ist passwortgeschützt. Beim ersten Aufruf erscheint die Anmeldeseite.
- **Kürzel**: das persönliche BEO-Kürzel (z. B. `RXF`)
- **Passwort**: individuell gesetztes Passwort
-Wurde das Passwort noch nicht geändert (Anzeige „Standard"), muss nach dem ersten Login sofort ein neues Passwort vergeben werden. Das Standard-Passwort lautet `welzheim`.
+Wurde das Passwort noch nicht geändert, muss nach dem ersten Login sofort ein neues Passwort vergeben werden. Das Standard-Passwort lautet `welzheim`.
---
@@ -141,6 +141,8 @@ Zeigt eine Monatstabelle mit Anzahl der Führungen und Besuchern, aufgeschlüsse
- Kumulierte Besucher für die gesamte Sternwarte (alle Kuppeln)
- Führungstage für die gesamte Sternwarte
+- **Grafik** Über diesen Button kann die (bekannte) Statistik-Grafik aufgerufen werden. Sie wird in ein einem gesonderten Fenster angezeigt. Zurück zum Führungsbuch kommt man über den Tab-Wechsel im Browser.
+
---
## 6. Drucken
@@ -176,33 +178,3 @@ Zeigt alle bekannten Objekte mit ID, Name und Datum der letzten Verwendung.
- **Objekt umbenennen**: Stift-Symbol ✎ in der Zeile anklicken, Namen ändern und mit **Speichern** bestätigen oder mit **Abbrechen** verwerfen.
- **Objekt löschen**: × in der Zeile – es erscheint ein Bestätigungsdialog. Das Löschen ist **unwiderruflich** und entfernt das Objekt aus allen bestehenden Führungsbucheinträgen.
-## Neue Features (Statistik Grafik Proxy)
-
-Dieses Release ergänzt eine serverseitige Proxy-Lösung für das interne Statistik-Portal, damit geschützte Diagramme sicher eingeblendet werden können, ohne Zugangsdaten im Browser zu speichern.
-
-- Was neu ist:
- - Server-seitiger Proxy unter `/api/statistik/grafik` und Catch-all `/api/statistik/grafik/*`.
- - Holt die Statistik-Seite mit Basic-Auth (serverseitig) und liefert sie an den Browser weiter.
- - Schreibt die HTML-Antwort so um, dass relative Assets (CSS/JS/Images) über die Proxy-URL geladen werden (es wird ein `` eingefügt).
- - Leitet auch AJAX-POSTs (z. B. `php/statistic.php`) weiter – Methoden und Bodies werden beibehalten.
- - Entfernt framing-blockierende Header (z. B. `X-Frame-Options`, CSP-Meta-Tags) in der proxied HTML-Antwort.
-
-- Wichtige Environment-Variablen (nur serverseitig):
- - `STATISTIK_GRAFIK_URL` — Basis-URL des internen Statistik-Portals (z. B. `https://sternwarte-welzheim.de/intern/statistik`).
- - `STATISTIK_GRAFIK_USER` — Benutzername für Basic-Auth.
- - `STATISTIK_GRAFIK_PASS` — Passwort für Basic-Auth.
-
-- UI-Änderung:
- - Der `Grafik`-Button in der Statistik-Ansicht öffnet die Statistik jetzt in einem neuen Fenster (`window.open('/api/statistik/grafik', '_blank')`). Die vorherige iframe-Integration wurde entfernt, weil manche Browser (insbesondere Firefox und Safari) Probleme mit eingebetteten, geschützten Seiten machen.
-
-- Sicherheit & Hinweise:
- - Setze die `STATISTIK_...` Variablen in deiner Server-Umgebung (Docker secrets oder im Host-Umfeld). Niemals Zugangsdaten ins Repository committen.
- - Die Proxy-Route ist so konfiguriert, dass Assets und AJAX-Aufrufe über den gleichen Proxy laufen, damit die Seite vollständig funktioniert.
-
-Wenn du möchtest, pushe ich die Änderungen an `ANLEITUNG.md` in `origin/main` für dich.
-
-Optionales manuelles Deploy (falls Docker lokal verfügbar):
-
-```bash
-./deploy.sh 1.7.8
-```
diff --git a/app/admin/actions.ts b/app/admin/actions.ts
index 5ca135a..7dbb540 100644
--- a/app/admin/actions.ts
+++ b/app/admin/actions.ts
@@ -2,7 +2,9 @@
import { redirect } from 'next/navigation';
import { getSession } from '@/lib/session';
-import { query } from '@/lib/db';
+import * as phpdb from '@/lib/phpdb';
+
+export type { BeoUser } from '@/lib/phpdb';
export interface ObjektRow {
ID: number;
@@ -13,28 +15,13 @@ export interface ObjektRow {
export async function listObjekte(): Promise {
const session = await getSession();
if (!session || !session.role?.includes('admin')) redirect('/');
- const rows = await query('SELECT ID, Name, LastUsed FROM objekte ORDER BY Name ASC', []);
- return rows as ObjektRow[];
+ return phpdb.listObjekteAdmin();
}
-export interface BeoUser {
- id: number;
- kürzel: string | null;
- name: string;
- vorname: string | null;
- role: string | null;
- hasPw: boolean;
-}
-
-export async function listUsers(): Promise {
+export async function listUsers(): Promise {
const session = await getSession();
if (!session || !session.role?.includes('admin')) redirect('/');
-
- const rows = await query(
- 'SELECT id, `kürzel`, name, vorname, role, (pw IS NOT NULL) AS hasPw FROM beos ORDER BY name, vorname',
- []
- ) as (Omit & { hasPw: number })[];
- return rows.map(r => ({ ...r, hasPw: r.hasPw === 1 }));
+ return phpdb.listUsers();
}
export async function resetPassword(
@@ -46,16 +33,11 @@ export async function resetPassword(
return { error: 'Keine Berechtigung.' };
}
- const idRaw = formData.get('id');
- const id = Number(idRaw);
+ const id = Number(formData.get('id'));
if (!id || isNaN(id)) {
return { error: 'Ungültige Benutzer-ID.' };
}
- await query(
- 'UPDATE beos SET pw = NULL, MustChangePassword = 1 WHERE id = ?',
- [id]
- );
-
+ await phpdb.resetBeoPassword(id);
return { success: 'Passwort wurde zurückgesetzt. Der Benutzer muss sich mit dem Standard-Passwort anmelden und es dann ändern.' };
}
diff --git a/app/api/DB4js_all.php/route.ts b/app/api/DB4js_all.php/route.ts
new file mode 100644
index 0000000..3aa0181
--- /dev/null
+++ b/app/api/DB4js_all.php/route.ts
@@ -0,0 +1,24 @@
+import { NextResponse } from 'next/server';
+
+const DB_URL = (process.env.PHP_DB_URL ?? 'http://localhost:8080/DB4js_all.php')
+ .replace(/\?.*$/, '');
+
+async function proxy(req: Request) {
+ const search = new URL(req.url).search;
+ const target = DB_URL + search;
+ const isReadOnly = req.method === 'GET' || req.method === 'HEAD';
+ const upstream = await fetch(target, {
+ method: req.method,
+ headers: { 'content-type': req.headers.get('content-type') ?? 'application/x-www-form-urlencoded' },
+ body: isReadOnly ? undefined : await req.arrayBuffer(),
+ cache: 'no-store',
+ });
+ const body = await upstream.arrayBuffer();
+ return new NextResponse(Buffer.from(body), {
+ status: upstream.status,
+ headers: { 'content-type': upstream.headers.get('content-type') ?? 'application/json' },
+ });
+}
+
+export const GET = proxy;
+export const POST = proxy;
diff --git a/app/api/beos/route.ts b/app/api/beos/route.ts
index 3ccef69..27cc473 100644
--- a/app/api/beos/route.ts
+++ b/app/api/beos/route.ts
@@ -1,14 +1,12 @@
import { NextResponse } from 'next/server';
-import { query } from '@/lib/db';
import { getSession } from '@/lib/session';
+import * as phpdb from '@/lib/phpdb';
export async function GET() {
const session = await getSession();
if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 });
try {
- 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 AND FIND_IN_SET(\'guide\', role) > 0 ORDER BY name ASC'
- ) as { ID: number; Kuerzel: string; Name: string }[];
+ const rows = await phpdb.getBeos();
return NextResponse.json(rows);
} catch (error) {
console.error('GET /api/beos:', error);
diff --git a/app/api/fahrkosten/route.ts b/app/api/fahrkosten/route.ts
index cff44fa..a765d3c 100644
--- a/app/api/fahrkosten/route.ts
+++ b/app/api/fahrkosten/route.ts
@@ -1,15 +1,8 @@
import { NextResponse } from 'next/server';
-import { query } from '@/lib/db';
import { getSession } from '@/lib/session';
+import * as phpdb from '@/lib/phpdb';
-export interface FahrkostenRow {
- ID: number;
- Kuerzel: string;
- Name: string;
- Anzahl: number;
-}
-
-const EXCLUDED = "'PrF','Beob','BEOS','TD','ToT'";
+export type { FahrkostenRow } from '@/lib/phpdb';
export async function GET(req: Request) {
const session = await getSession();
@@ -22,18 +15,7 @@ export async function GET(req: Request) {
}
try {
- const rows = await query(
- 'SELECT b.id AS ID, b.`kürzel` AS Kuerzel,' +
- ' CONCAT(IFNULL(b.vorname, \'\'), IF(b.vorname IS NOT NULL, \' \', \'\'), b.name) AS Name,' +
- ' COUNT(DISTINCT l.ID) AS Anzahl' +
- ' FROM beos b' +
- ' JOIN logbuch_beos lb ON lb.BeoID = b.id' +
- ' JOIN logbuch l ON l.ID = lb.LogbuchID' +
- ' WHERE l.Beginn >= ? AND l.ArtFuehrung NOT IN (' + EXCLUDED + ')' +
- ' GROUP BY b.id, b.`kürzel`, b.name, b.vorname' +
- ' ORDER BY b.name ASC',
- [ab + ' 00:00:00']
- ) as FahrkostenRow[];
+ const rows = await phpdb.getFahrkosten(ab);
return NextResponse.json(rows);
} catch (error) {
console.error('GET /api/fahrkosten:', error);
diff --git a/app/api/logbuch/[id]/route.ts b/app/api/logbuch/[id]/route.ts
index 2edf820..1d8842c 100644
--- a/app/api/logbuch/[id]/route.ts
+++ b/app/api/logbuch/[id]/route.ts
@@ -1,8 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
-import { query, getPool } from '@/lib/db';
import { getSession } from '@/lib/session';
import { triggerBackup } from '@/lib/backup';
-import type { SelectedObjekt } from '@/types/logbuch';
+import * as phpdb from '@/lib/phpdb';
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await getSession();
@@ -12,66 +11,25 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
const logbuchId = parseInt(id);
try {
- const existingRows = await query('SELECT ID FROM logbuch WHERE ID = ?', [logbuchId]) as { ID: number }[];
- if (existingRows.length === 0) {
- return NextResponse.json({ error: 'Eintrag nicht gefunden' }, { status: 404 });
- }
-
- const isAdmin = session.role?.includes('admin');
- const beoRows = await query('SELECT COUNT(*) AS cnt FROM logbuch_beos WHERE LogbuchID = ? AND BeoID = ?', [logbuchId, session.beoId]) as { cnt: number }[];
- const isBeo = (beoRows[0]?.cnt ?? 0) > 0;
-
- if (!isAdmin && !isBeo) {
- return NextResponse.json({ error: 'Keine Berechtigung zum Ändern dieses Eintrags' }, { status: 403 });
- }
-
const body = await request.json();
const { Kuppel, ArtFuehrung, SonderName, Beginn, Ende, Besucher, beoIds, objekte, Bemerkungen, Wetter } = body;
- await getPool().execute(
- 'UPDATE logbuch SET Kuppel=?, ArtFuehrung=?, SonderName=?, Beginn=?, Ende=?, Besucher=?,' +
- ' Bemerkungen=?, WetterTemp=?, WetterFeuchte=?, WetterDruck=? WHERE ID=?',
- [
- Kuppel, ArtFuehrung, SonderName || null, Beginn, Ende,
- Besucher ?? 0,
- Bemerkungen?.slice(0, 500) || null,
- Wetter?.temp ?? null,
- Wetter?.feuchte ?? null,
- Wetter?.druck ?? null,
- logbuchId,
- ]
- );
-
- await query('DELETE FROM logbuch_beos WHERE LogbuchID = ?', [logbuchId]);
- await query('DELETE FROM logbuch_objekte WHERE LogbuchID = ?', [logbuchId]);
-
- for (const beoId of (beoIds as number[]) || []) {
- await query('INSERT INTO logbuch_beos (LogbuchID, BeoID) VALUES (?, ?)', [logbuchId, beoId]);
- }
-
- for (const obj of (objekte as SelectedObjekt[]) || []) {
- let objektId = obj.ID;
- if (!objektId) {
- const existing = await query('SELECT ID, Name FROM objekte WHERE LOWER(Name) = LOWER(?)', [obj.Name]) as { ID: number; Name: string }[];
- if (existing[0]) {
- objektId = existing[0].ID;
- } else {
- const [ins] = await getPool().execute(
- 'INSERT INTO objekte (Name) VALUES (?)', [obj.Name]
- ) as [{ insertId: number }, unknown];
- objektId = ins.insertId;
- }
- }
- await query('UPDATE objekte SET LastUsed = NOW() WHERE ID = ?', [objektId]);
- await query(
- 'INSERT INTO logbuch_objekte (LogbuchID, ObjektID) VALUES (?, ?)',
- [logbuchId, objektId]
- );
- }
+ await phpdb.updateLogbuch(logbuchId, session.beoId, session.role ?? '', {
+ Kuppel, ArtFuehrung, SonderName, Beginn, Ende,
+ Besucher: Besucher ?? 0,
+ beoIds: beoIds ?? [],
+ objekte: objekte ?? [],
+ Bemerkungen: Bemerkungen ?? null,
+ Wetter: Wetter ?? null,
+ });
triggerBackup();
return NextResponse.json({ ok: true });
- } catch (error) {
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ if (error.message.includes('404')) return NextResponse.json({ error: 'Eintrag nicht gefunden' }, { status: 404 });
+ if (error.message.includes('403')) return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 });
+ }
console.error('PUT /api/logbuch/[id]:', error);
return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 });
}
@@ -85,22 +43,13 @@ export async function DELETE(_request: NextRequest, { params }: { params: Promis
const logbuchId = parseInt(id);
try {
- const existingRows = await query('SELECT ID FROM logbuch WHERE ID = ?', [logbuchId]) as { ID: number }[];
- if (existingRows.length === 0) {
- return NextResponse.json({ error: 'Eintrag nicht gefunden' }, { status: 404 });
- }
-
- const isAdmin = session.role?.includes('admin');
- const beoRows = await query('SELECT COUNT(*) AS cnt FROM logbuch_beos WHERE LogbuchID = ? AND BeoID = ?', [logbuchId, session.beoId]) as { cnt: number }[];
- const isBeo = (beoRows[0]?.cnt ?? 0) > 0;
-
- if (!isAdmin && !isBeo) {
- return NextResponse.json({ error: 'Keine Berechtigung zum Löschen dieses Eintrags' }, { status: 403 });
- }
-
- await query('DELETE FROM logbuch WHERE ID = ?', [logbuchId]);
+ await phpdb.deleteLogbuch(logbuchId, session.beoId, session.role ?? '');
return NextResponse.json({ ok: true });
- } catch (error) {
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ if (error.message.includes('404')) return NextResponse.json({ error: 'Eintrag nicht gefunden' }, { status: 404 });
+ if (error.message.includes('403')) return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 });
+ }
console.error('DELETE /api/logbuch/[id]:', error);
return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 });
}
diff --git a/app/api/logbuch/route.ts b/app/api/logbuch/route.ts
index 6332cb4..b11ace6 100644
--- a/app/api/logbuch/route.ts
+++ b/app/api/logbuch/route.ts
@@ -1,90 +1,23 @@
import { NextRequest, NextResponse } from 'next/server';
-import { query, getPool } from '@/lib/db';
import { getSession } from '@/lib/session';
import { triggerBackup } from '@/lib/backup';
-import type { SelectedObjekt } from '@/types/logbuch';
-
-const LIST_SQL =
- 'SELECT' +
- ' l.ID, l.Kuppel, l.ArtFuehrung,' +
- " DATE_FORMAT(l.Beginn, '%Y-%m-%dT%H:%i') AS Beginn," +
- " DATE_FORMAT(l.Ende, '%Y-%m-%dT%H:%i') AS Ende," +
- ' l.Besucher, l.Bemerkungen, l.SonderName,' +
- ' l.WetterTemp, l.WetterFeuchte, l.WetterDruck,' +
- ' l.created_by, l.created_at,' +
- ' creator.kuerzel AS created_by_kuerzel,' +
- " GROUP_CONCAT(DISTINCT bk.kuerzel ORDER BY bk.kuerzel SEPARATOR ', ') AS BEOs," +
- " GROUP_CONCAT(DISTINCT o.Name ORDER BY o.Name SEPARATOR ', ') AS Objekte" +
- ' FROM logbuch l' +
- ' LEFT JOIN (SELECT id, `kürzel` AS kuerzel FROM beos) creator ON creator.id = l.created_by' +
- ' 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 logbuch_objekte lo ON lo.LogbuchID = l.ID' +
- ' LEFT JOIN objekte o ON o.ID = lo.ObjektID' +
- ' WHERE l.Kuppel = ?' +
- ' GROUP BY l.ID';
+import * as phpdb from '@/lib/phpdb';
export async function GET(request: NextRequest) {
const session = await getSession();
- if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 });
+ if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 });
+
const { searchParams } = new URL(request.url);
const kuppel = searchParams.get('kuppel') || 'West';
- const limit = Math.min(parseInt(searchParams.get('limit') || '10') || 10, 500);
+ const limit = Math.min(parseInt(searchParams.get('limit') || '10') || 10, 500);
const offset = Math.max(0, parseInt(searchParams.get('offset') || '0') || 0);
- const month = searchParams.get('month') || '';
- const order = searchParams.get('order') === 'asc' ? 'ASC' : 'DESC';
-
+ const month = searchParams.get('month') || '';
+ const order = searchParams.get('order') === 'asc' ? 'asc' : 'desc';
const search = (searchParams.get('search') || '').trim();
- if (search) {
- const pattern = '%' + search + '%';
- const searchParams2 = [kuppel, pattern, pattern, pattern];
- const countSQL =
- 'SELECT COUNT(*) AS total FROM (' +
- 'SELECT l.ID FROM logbuch l' +
- ' 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 logbuch_objekte lo ON lo.LogbuchID = l.ID' +
- ' LEFT JOIN objekte o ON o.ID = lo.ObjektID' +
- ' WHERE l.Kuppel = ?' +
- ' GROUP BY l.ID' +
- " HAVING (MAX(l.Bemerkungen) LIKE ? OR GROUP_CONCAT(DISTINCT bk.kuerzel ORDER BY bk.kuerzel SEPARATOR ', ') LIKE ? OR GROUP_CONCAT(DISTINCT o.Name ORDER BY o.Name SEPARATOR ', ') LIKE ?)" +
- ') AS sub';
- const listSQL = LIST_SQL +
- ' HAVING (MAX(l.Bemerkungen) LIKE ? OR BEOs LIKE ? OR Objekte LIKE ?)' +
- ` ORDER BY l.Beginn DESC LIMIT ${limit} OFFSET ${offset}`;
- try {
- const [countRows, entries] = await Promise.all([
- query(countSQL, searchParams2) as Promise<{ total: number }[]>,
- query(listSQL, searchParams2),
- ]);
- return NextResponse.json({ entries, total: (countRows as unknown as { total: number }[])[0]?.total ?? 0 });
- } catch (error) {
- console.error('GET /api/logbuch (search):', error);
- return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 });
- }
- }
-
- let listWhere = 'WHERE l.Kuppel = ?';
- let countWhere = 'WHERE Kuppel = ?';
- let params: (string | number | null)[] = [kuppel];
- if (month && /^\d{4}-\d{2}$/.test(month)) {
- const [y, m] = month.split('-').map(Number);
- const start = `${y}-${String(m).padStart(2, '0')}-01`;
- const nextM = m === 12 ? 1 : m + 1;
- const nextY = m === 12 ? y + 1 : y;
- const end = `${nextY}-${String(nextM).padStart(2, '0')}-01`;
- listWhere += ' AND l.Beginn >= ? AND l.Beginn < ?';
- countWhere += ' AND Beginn >= ? AND Beginn < ?';
- params = [kuppel, start, end];
- }
-
try {
- const [countRows, entries] = await Promise.all([
- query('SELECT COUNT(*) AS total FROM logbuch ' + countWhere, params) as Promise<{ total: number }[]>,
- query(LIST_SQL.replace('WHERE l.Kuppel = ?', listWhere) + ` ORDER BY l.Beginn ${order} LIMIT ${limit} OFFSET ${offset}`, params),
- ]);
- return NextResponse.json({ entries, total: (countRows as unknown as { total: number }[])[0]?.total ?? 0 });
+ const result = await phpdb.listLogbuch({ kuppel, limit, offset, month, search, order });
+ return NextResponse.json(result);
} catch (error) {
console.error('GET /api/logbuch:', error);
return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 });
@@ -99,47 +32,18 @@ export async function POST(request: NextRequest) {
const body = await request.json();
const { Kuppel, ArtFuehrung, SonderName, Beginn, Ende, Besucher, beoIds, objekte, Bemerkungen, Wetter } = body;
- const pool = getPool();
- const [result] = await pool.execute(
- 'INSERT INTO logbuch (Kuppel, ArtFuehrung, SonderName, Beginn, Ende, Besucher, Bemerkungen, WetterTemp, WetterFeuchte, WetterDruck, created_by)' +
- ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
- [
- Kuppel, ArtFuehrung, SonderName || null, Beginn, Ende,
- Besucher ?? 0,
- Bemerkungen?.slice(0, 500) || null,
- Wetter?.temp ?? null,
- Wetter?.feuchte ?? null,
- Wetter?.druck ?? null,
- session.beoId,
- ]
- ) as [{ insertId: number }, unknown];
-
- const logbuchId = result.insertId;
-
- for (const beoId of (beoIds as number[]) || []) {
- await query('INSERT INTO logbuch_beos (LogbuchID, BeoID) VALUES (?, ?)', [logbuchId, beoId]);
- }
-
- for (const obj of (objekte as SelectedObjekt[]) || []) {
- let objektId = obj.ID;
- if (!objektId) {
- const existing = await query('SELECT ID, Name FROM objekte WHERE LOWER(Name) = LOWER(?)', [obj.Name]) as { ID: number; Name: string }[];
- if (existing[0]) {
- objektId = existing[0].ID;
- } else {
- const [ins] = await pool.execute('INSERT INTO objekte (Name) VALUES (?)', [obj.Name]) as [{ insertId: number }, unknown];
- objektId = ins.insertId;
- }
- }
- await query('UPDATE objekte SET LastUsed = NOW() WHERE ID = ?', [objektId]);
- await query(
- 'INSERT INTO logbuch_objekte (LogbuchID, ObjektID) VALUES (?, ?)',
- [logbuchId, objektId]
- );
- }
+ const result = await phpdb.createLogbuch({
+ Kuppel, ArtFuehrung, SonderName, Beginn, Ende,
+ Besucher: Besucher ?? 0,
+ beoIds: beoIds ?? [],
+ objekte: objekte ?? [],
+ Bemerkungen: Bemerkungen ?? null,
+ Wetter: Wetter ?? null,
+ created_by: session.beoId,
+ });
triggerBackup();
- return NextResponse.json({ id: logbuchId }, { status: 201 });
+ return NextResponse.json(result, { status: 201 });
} catch (error) {
console.error('POST /api/logbuch:', error);
return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 });
diff --git a/app/api/objekte/[id]/route.ts b/app/api/objekte/[id]/route.ts
index c9cd217..263dced 100644
--- a/app/api/objekte/[id]/route.ts
+++ b/app/api/objekte/[id]/route.ts
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
-import { query } from '@/lib/db';
import { getSession } from '@/lib/session';
+import * as phpdb from '@/lib/phpdb';
export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await getSession();
@@ -13,8 +13,8 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
const { name } = await req.json();
const trimmed = (name as string)?.trim();
if (!trimmed) return NextResponse.json({ error: 'Name darf nicht leer sein' }, { status: 400 });
- await query('UPDATE objekte SET Name = ? WHERE ID = ?', [trimmed, numId]);
- return NextResponse.json({ ID: numId, Name: trimmed });
+ const result = await phpdb.updateObjekt(numId, trimmed);
+ return NextResponse.json(result);
} catch (error) {
console.error('PUT /api/objekte/[id]:', error);
return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 });
@@ -29,7 +29,7 @@ export async function DELETE(_req: NextRequest, { params }: { params: Promise<{
const { id } = await params;
const numId = Number(id);
if (isNaN(numId)) return NextResponse.json({ error: 'Ungültige ID' }, { status: 400 });
- await query('DELETE FROM objekte WHERE ID = ?', [numId]);
+ await phpdb.deleteObjekt(numId);
return NextResponse.json({ ok: true });
} catch (error) {
console.error('DELETE /api/objekte/[id]:', error);
diff --git a/app/api/objekte/route.ts b/app/api/objekte/route.ts
index 0d50fbe..1e8a934 100644
--- a/app/api/objekte/route.ts
+++ b/app/api/objekte/route.ts
@@ -1,12 +1,12 @@
import { NextRequest, NextResponse } from 'next/server';
-import { query } from '@/lib/db';
import { getSession } from '@/lib/session';
+import * as phpdb from '@/lib/phpdb';
export async function GET() {
const session = await getSession();
if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 });
try {
- const rows = await query('SELECT ID, Name FROM objekte ORDER BY LastUsed DESC LIMIT 100');
+ const rows = await phpdb.getObjekte();
return NextResponse.json(rows);
} catch (error) {
console.error('GET /api/objekte:', error);
@@ -22,8 +22,8 @@ export async function POST(req: NextRequest) {
const { name } = await req.json();
const trimmed = (name as string)?.trim();
if (!trimmed) return NextResponse.json({ error: 'Name darf nicht leer sein' }, { status: 400 });
- const result = await query('INSERT INTO objekte (Name) VALUES (?)', [trimmed]) as { insertId: number };
- return NextResponse.json({ ID: result.insertId, Name: trimmed }, { status: 201 });
+ const result = await phpdb.createObjekt(trimmed);
+ return NextResponse.json(result, { status: 201 });
} catch (error) {
console.error('POST /api/objekte:', error);
return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 });
diff --git a/app/api/statistik/route.ts b/app/api/statistik/route.ts
index 4be1943..fdf7f24 100644
--- a/app/api/statistik/route.ts
+++ b/app/api/statistik/route.ts
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
-import { query } from '@/lib/db';
import { getSession } from '@/lib/session';
+import * as phpdb from '@/lib/phpdb';
export async function GET(request: NextRequest) {
const session = await getSession();
@@ -10,66 +10,8 @@ export async function GET(request: NextRequest) {
const year = parseInt(searchParams.get('year') || String(new Date().getFullYear()), 10);
try {
- const monthlyRows = await query(
- 'SELECT' +
- ' MONTH(Beginn) AS monat,' +
- " COUNT(CASE WHEN ArtFuehrung IN ('RF','SF','SonF','PrF') THEN 1 END) AS tageFuehrungen," +
- " COUNT(CASE WHEN ArtFuehrung = 'Beob' THEN 1 END) AS tageBeob," +
- " COUNT(CASE WHEN ArtFuehrung = 'TD' THEN 1 END) AS tageTD," +
- " COUNT(CASE WHEN ArtFuehrung = 'Sonst' THEN 1 END) AS tageSonst," +
- " COUNT(CASE WHEN ArtFuehrung = 'BEOS' THEN 1 END) AS tageBEOS," +
- " COUNT(CASE WHEN ArtFuehrung = 'ToT' THEN 1 END) AS tagesToT," +
- " COUNT(CASE WHEN ArtFuehrung IN ('RF','SF','SonF','PrF','Beob','TD','Sonst','BEOS','ToT') THEN 1 END) AS tageGesamt," +
- " SUM(CASE WHEN ArtFuehrung = 'RF' THEN Besucher ELSE 0 END) AS besucherRF," +
- " SUM(CASE WHEN ArtFuehrung = 'SF' THEN Besucher ELSE 0 END) AS besucherSF," +
- " SUM(CASE WHEN ArtFuehrung = 'SonF' THEN Besucher ELSE 0 END) AS besucherSonF," +
- " SUM(CASE WHEN ArtFuehrung = 'PrF' THEN Besucher ELSE 0 END) AS besucherPrF," +
- " SUM(CASE WHEN ArtFuehrung = 'ToT' THEN Besucher ELSE 0 END) AS besucherToT," +
- " SUM(CASE WHEN ArtFuehrung IN ('RF','SF','SonF','PrF','ToT') THEN Besucher ELSE 0 END) AS besucherGesamt" +
- ' FROM logbuch' +
- ' WHERE YEAR(Beginn) = ?' +
- ' GROUP BY MONTH(Beginn)' +
- ' ORDER BY monat',
- [year]
- ) as {
- monat: number;
- tageFuehrungen: number; tageBeob: number; tageTD: number; tageSonst: number; tageBEOS: number; tagesToT: number; tageGesamt: number;
- besucherRF: number; besucherSF: number; besucherSonF: number; besucherPrF: number;
- besucherToT: number; besucherGesamt: number;
- }[];
-
- const cumulativeRows = await query(
- "SELECT SUM(CASE WHEN ArtFuehrung IN ('RF','SF','SonF','PrF','ToT') THEN Besucher ELSE 0 END) AS total" +
- ' FROM logbuch WHERE YEAR(Beginn) = ?',
- [year]
- ) as { total: number | null }[];
-
- const tageRows = await query(
- "SELECT COUNT(*) AS tage FROM logbuch WHERE YEAR(Beginn) = ? AND ArtFuehrung IN ('RF','SF','SonF','PrF','Beob','TD','Sonst','BEOS','ToT')",
- [year]
- ) as { tage: number }[];
-
- return NextResponse.json({
- monthly: monthlyRows.map((r) => ({
- monat: Number(r.monat),
- tageFuehrungen: Number(r.tageFuehrungen),
- tageBeob: Number(r.tageBeob),
- tageTD: Number(r.tageTD),
- tageSonst: Number(r.tageSonst),
- tageBEOS: Number(r.tageBEOS),
- tagesToT: Number(r.tagesToT),
- tageGesamt: Number(r.tageGesamt),
- besucherRF: Number(r.besucherRF),
- besucherSF: Number(r.besucherSF),
- besucherSonF: Number(r.besucherSonF),
- besucherPrF: Number(r.besucherPrF),
- besucherToT: Number(r.besucherToT),
- besucherGesamt: Number(r.besucherGesamt),
- })),
- cumulative: Number(cumulativeRows[0]?.total ?? 0),
- tage: Number(tageRows[0]?.tage ?? 0),
- year,
- });
+ const result = await phpdb.getStatistik(year);
+ return NextResponse.json(result);
} catch (error) {
console.error('GET /api/statistik:', error);
return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 });
diff --git a/app/change-password/actions.ts b/app/change-password/actions.ts
index 7723da0..95a9616 100644
--- a/app/change-password/actions.ts
+++ b/app/change-password/actions.ts
@@ -3,7 +3,7 @@
import { redirect } from 'next/navigation';
import { getSession, createSession } from '@/lib/session';
import { hashPassword } from '@/lib/auth';
-import { query } from '@/lib/db';
+import { updateBeoPassword } from '@/lib/phpdb';
export async function changePassword(
_prevState: { error: string } | undefined,
@@ -28,10 +28,7 @@ export async function changePassword(
}
const hashed = await hashPassword(newPassword);
- await query(
- 'UPDATE beos SET pw = ?, MustChangePassword = 0 WHERE id = ?',
- [hashed, session.beoId]
- );
+ await updateBeoPassword(session.beoId, hashed);
await createSession({
kuerzel: session.kuerzel,
diff --git a/compose.yml b/compose.yml
index ff3375d..80435bd 100644
--- a/compose.yml
+++ b/compose.yml
@@ -1,79 +1,14 @@
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
+ PHP_DB_URL: ${PHP_DB_URL}
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
@@ -88,6 +23,7 @@ services:
networks:
- proxy
- gitea-internal
+
networks:
proxy:
name: dockge_default
@@ -95,5 +31,3 @@ networks:
gitea-internal:
name: gitea_gitea-internal
external: true
-volumes:
- db_data: null
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
index e15a605..61cb8b7 100644
--- a/docker-compose.prod.yml
+++ b/docker-compose.prod.yml
@@ -1,64 +1,19 @@
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
- networks:
- - logbuch_net
- 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/
- ports:
- - "127.0.0.1:${PMA_PORT:-8080}:80"
- depends_on:
- logbuch_mysql:
- condition: service_healthy
- networks:
- - logbuch_net
-
logbuch_app:
image: docker.citysensor.de/logbuch:latest
container_name: logbuch_app
restart: unless-stopped
env_file: .env
environment:
- DB_HOST: logbuch_mysql
- DB_PORT: 3306
NODE_ENV: production
BACKUP_SSH_KEY_PATH: /run/secrets/backup_ssh_key
volumes:
- ${BACKUP_SSH_KEY_FILE:-/dev/null}:/run/secrets/backup_ssh_key:ro
ports:
- "127.0.0.1:${APP_PORT:-3000}:3000"
- depends_on:
- logbuch_mysql:
- condition: service_healthy
networks:
- logbuch_net
networks:
logbuch_net:
driver: bridge
-
-volumes:
- db_data:
diff --git a/lib/auth.ts b/lib/auth.ts
index 19771dd..d3c5748 100644
--- a/lib/auth.ts
+++ b/lib/auth.ts
@@ -1,34 +1,13 @@
import bcrypt from 'bcryptjs';
-import { query } from './db';
+import { getBeoByKuerzel, getBeoByName } from './phpdb';
-export interface Beo {
- id: number;
- name: string;
- vorname: string | null;
- kürzel: string | null;
- pw: string | null;
- MustChangePassword: number;
- role: string | null;
-}
-
-export async function getBeoByKuerzel(kuerzel: string): Promise {
- const rows = await query(
- 'SELECT id, name, vorname, `kürzel`, pw, MustChangePassword, role FROM beos WHERE `kürzel` = ?',
- [kuerzel]
- ) as Beo[];
- return rows[0] ?? null;
-}
+export type { Beo } from './phpdb';
+import type { Beo } from './phpdb';
export async function getBeoByLogin(login: string): Promise {
- // First try exact Kürzel match, then case-insensitive Nachname match
const byKuerzel = await getBeoByKuerzel(login);
if (byKuerzel) return byKuerzel;
-
- const rows = await query(
- 'SELECT id, name, vorname, `kürzel`, pw, MustChangePassword, role FROM beos WHERE LOWER(name) = LOWER(?)',
- [login]
- ) as Beo[];
- return rows[0] ?? null;
+ return getBeoByName(login);
}
export async function verifyCredentials(
@@ -41,8 +20,7 @@ export async function verifyCredentials(
if (!beo.pw) {
const defaultPw = process.env.DEFAULT_PASSWORD;
if (!defaultPw) throw new Error('DEFAULT_PASSWORD Umgebungsvariable ist nicht gesetzt!');
- const valid = password === defaultPw;
- return { beo, valid };
+ return { beo, valid: password === defaultPw };
}
const valid = await bcrypt.compare(password, beo.pw);
diff --git a/lib/backup.ts b/lib/backup.ts
index 5ad6890..bbbfcc7 100644
--- a/lib/backup.ts
+++ b/lib/backup.ts
@@ -2,15 +2,14 @@ import { createWriteStream, mkdirSync, unlinkSync } from 'fs';
import { createGzip } from 'zlib';
import { join } from 'path';
import { spawn } from 'child_process';
-import { getPool } from './db';
+import { getBackupData } from './phpdb';
export function triggerBackup(): void {
setImmediate(() => runBackup().catch((e) => console.error('[backup] Fehler:', e)));
}
async function dumpToFile(filePath: string): Promise {
- const dbName = process.env.DB_NAME || 'sternwarte';
- const pool = getPool();
+ const { tables } = await getBackupData();
const gzip = createGzip();
const file = createWriteStream(filePath);
@@ -21,21 +20,12 @@ async function dumpToFile(filePath: string): Promise {
);
const now = new Date().toISOString();
- await write(`-- Führungsbuch Backup ${now}\n-- Datenbank: ${dbName} (ohne Tabelle beos)\n\nSET FOREIGN_KEY_CHECKS=0;\n\n`);
+ await write(`-- Führungsbuch Backup ${now}\n-- Logbuch-Tabellen\n\nSET FOREIGN_KEY_CHECKS=0;\n\n`);
- const [tableRows] = await pool.query('SHOW TABLES') as [Record[], unknown];
- const tables = tableRows
- .map((r) => Object.values(r)[0])
- .filter((t) => t !== 'beos');
-
- for (const table of tables) {
- const [[createRow]] = await pool.query(`SHOW CREATE TABLE \`${table}\``) as [Record[], unknown];
- const createSql = Object.values(createRow)[1];
-
- await write(`DROP TABLE IF EXISTS \`${table}\`;\n`);
+ for (const { name, createSql, rows } of tables) {
+ await write(`DROP TABLE IF EXISTS \`${name}\`;\n`);
await write(`${createSql};\n\n`);
- const [rows] = await pool.query(`SELECT * FROM \`${table}\``) as [Record[], unknown];
if (rows.length > 0) {
const cols = Object.keys(rows[0]).map((c) => `\`${c}\``).join(', ');
const batchSize = 200;
@@ -45,11 +35,10 @@ async function dumpToFile(filePath: string): Promise {
'(' + Object.values(row).map((v) => {
if (v === null) return 'NULL';
if (typeof v === 'number') return String(v);
- if (v instanceof Date) return `'${v.toISOString().slice(0, 19).replace('T', ' ')}'`;
return `'${String(v).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
}).join(', ') + ')'
).join(',\n ');
- await write(`INSERT INTO \`${table}\` (${cols}) VALUES\n ${values};\n`);
+ await write(`INSERT INTO \`${name}\` (${cols}) VALUES\n ${values};\n`);
}
await write('\n');
}
@@ -99,7 +88,6 @@ async function runBackup(): Promise {
'-o', 'ConnectTimeout=15',
];
- // Zielverzeichnis auf Remote anlegen falls nicht vorhanden
await new Promise((resolve, reject) => {
const ssh = spawn('ssh', [...sshOpts, sshHost, `mkdir -p ${remotePath}`]);
ssh.on('error', reject);
@@ -118,7 +106,6 @@ async function runBackup(): Promise {
console.log(`[backup] ${filename} → ${sshHost}:${remotePath}`);
- // Backups älter als 30 Tage auf Remote löschen
await new Promise((resolve) => {
const ssh = spawn('ssh', [
...sshOpts, sshHost,
diff --git a/lib/db.ts b/lib/db.ts
deleted file mode 100644
index dd8bef3..0000000
--- a/lib/db.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import mysql from 'mysql2/promise';
-import type { QueryResult } from 'mysql2/promise';
-
-const dbConfig = {
- host: process.env.DB_HOST || 'mydbase_mysql',
- port: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306,
- user: process.env.DB_USER,
- password: process.env.DB_PASS,
- database: process.env.DB_NAME || 'logbuch',
- charset: 'utf8mb4',
- waitForConnections: true,
- connectionLimit: 10,
- queueLimit: 0,
-};
-
-let pool: mysql.Pool | null = null;
-
-export function getPool() {
- if (!pool) {
- pool = mysql.createPool(dbConfig);
- }
- return pool;
-}
-
-export async function query(sql: string, params?: (string | number | null)[]): Promise {
- const p = getPool();
- const [rows] = await p.execute(sql, params || []);
- return rows as QueryResult;
-}
diff --git a/lib/phpdb.ts b/lib/phpdb.ts
new file mode 100644
index 0000000..1ac62b1
--- /dev/null
+++ b/lib/phpdb.ts
@@ -0,0 +1,185 @@
+import type { BeoOption, LogbuchEintrag, ObjektOption, SelectedObjekt, Wetter } from '@/types/logbuch';
+
+const PHP_DB_URL = process.env.PHP_DB_URL ?? 'http://localhost:8080/DB4js_all.php';
+
+async function call(cmd: string, params: object = {}): Promise {
+ const res = await fetch(PHP_DB_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ cmd, ...params }),
+ cache: 'no-store',
+ });
+ if (!res.ok) {
+ let detail = '';
+ try { detail = await res.text(); } catch { /* ignore */ }
+ throw new Error(`DB4js ${cmd} HTTP ${res.status}: ${detail}`);
+ }
+ const json: unknown = await res.json();
+ if (json && typeof json === 'object' && 'error' in json) {
+ throw new Error(`DB4js ${cmd}: ${(json as { error: string }).error}`);
+ }
+ return json as T;
+}
+
+// ---- Auth / Benutzer ----
+
+export interface Beo {
+ id: number;
+ name: string;
+ vorname: string | null;
+ 'kürzel': string | null;
+ pw: string | null;
+ MustChangePassword: number;
+ role: string | null;
+}
+
+export interface BeoUser {
+ id: number;
+ 'kürzel': string | null;
+ name: string;
+ vorname: string | null;
+ role: string | null;
+ hasPw: boolean;
+}
+
+export async function getBeoByKuerzel(kuerzel: string): Promise {
+ const r = await call<{ beo: Beo | null }>('LB_AUTH_KUERZEL', { kuerzel });
+ return r.beo;
+}
+
+export async function getBeoByName(name: string): Promise {
+ const r = await call<{ beo: Beo | null }>('LB_AUTH_NAME', { name });
+ return r.beo;
+}
+
+export async function updateBeoPassword(id: number, pwHash: string): Promise {
+ await call('LB_UPDATE_PW', { id, pw: pwHash });
+}
+
+export async function resetBeoPassword(id: number): Promise {
+ await call('LB_RESET_PW', { id });
+}
+
+export async function listUsers(): Promise {
+ return call('LB_LIST_USERS');
+}
+
+// ---- Logbuch ----
+
+export interface ListLogbuchParams {
+ kuppel?: string;
+ limit?: number;
+ offset?: number;
+ month?: string;
+ search?: string;
+ order?: string;
+}
+
+export async function listLogbuch(
+ params: ListLogbuchParams
+): Promise<{ entries: LogbuchEintrag[]; total: number }> {
+ return call('LB_LIST_LOGBUCH', params);
+}
+
+export interface CreateLogbuchData {
+ Kuppel: string;
+ ArtFuehrung: string;
+ SonderName?: string | null;
+ Beginn: string;
+ Ende: string;
+ Besucher?: number;
+ beoIds?: number[];
+ objekte?: SelectedObjekt[];
+ Bemerkungen?: string | null;
+ Wetter?: Partial | null;
+ created_by: number;
+}
+
+export async function createLogbuch(data: CreateLogbuchData): Promise<{ id: number }> {
+ return call('LB_CREATE_LOGBUCH', data);
+}
+
+export async function updateLogbuch(
+ id: number,
+ userId: number,
+ userRole: string,
+ data: Omit
+): Promise {
+ await call('LB_UPDATE_LOGBUCH', { id, user_id: userId, user_role: userRole, ...data });
+}
+
+export async function deleteLogbuch(
+ id: number,
+ userId: number,
+ userRole: string
+): Promise {
+ await call('LB_DELETE_LOGBUCH', { id, user_id: userId, user_role: userRole });
+}
+
+// ---- BEOs & Objekte ----
+
+export async function getBeos(): Promise {
+ return call('LB_GET_BEOS');
+}
+
+export async function getObjekte(): Promise {
+ return call('LB_GET_OBJEKTE');
+}
+
+export async function createObjekt(name: string): Promise {
+ return call('LB_CREATE_OBJEKT', { name });
+}
+
+export async function updateObjekt(id: number, name: string): Promise {
+ return call('LB_UPDATE_OBJEKT', { id, name });
+}
+
+export async function deleteObjekt(id: number): Promise {
+ await call('LB_DELETE_OBJEKT', { id });
+}
+
+export async function listObjekteAdmin(): Promise<{ ID: number; Name: string; LastUsed: string | null }[]> {
+ return call('LB_LIST_OBJEKTE_ADMIN');
+}
+
+// ---- Auswertungen ----
+
+export interface FahrkostenRow {
+ ID: number;
+ Kuerzel: string;
+ Name: string;
+ Anzahl: number;
+}
+
+export async function getFahrkosten(ab: string): Promise {
+ return call('LB_FAHRKOSTEN', { ab });
+}
+
+export interface StatistikResult {
+ monthly: {
+ monat: number;
+ tageFuehrungen: number; tageBeob: number; tageTD: number;
+ tageSonst: number; tageBEOS: number; tagesToT: number; tageGesamt: number;
+ besucherRF: number; besucherSF: number; besucherSonF: number;
+ besucherPrF: number; besucherToT: number; besucherGesamt: number;
+ }[];
+ cumulative: number;
+ tage: number;
+ year: number;
+}
+
+export async function getStatistik(year: number): Promise {
+ return call('LB_STATISTIK', { year });
+}
+
+// ---- Backup ----
+
+export interface BackupTable {
+ name: string;
+ createSql: string;
+ rows: Record[];
+}
+
+export async function getBackupData(): Promise<{ tables: BackupTable[] }> {
+ return call('LB_BACKUP_DATA');
+}
diff --git a/package.json b/package.json
index 0b2d796..68c33d8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "logbuch",
- "version": "1.9.1",
+ "version": "1.10.0",
"private": true,
"scripts": {
"dev": "next dev",
@@ -11,8 +11,7 @@
"dependencies": {
"bcryptjs": "^3.0.3",
"jose": "^6.2.2",
- "mysql2": "^3.22.3",
- "next": "16.1.6",
+"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3"
},