für Docker angepasst

This commit is contained in:
rxf
2026-02-22 22:11:52 +01:00
parent e14af11e5c
commit e28cca1c46
21 changed files with 1494 additions and 94 deletions

53
.dockerignore Normal file
View File

@@ -0,0 +1,53 @@
# dependencies
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# testing
coverage
# next.js
.next/
out/
build
dist
# misc
.DS_Store
*.pem
# debug
*.log
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
# typescript
*.tsbuildinfo
# git
.git
.gitignore
# editor
.vscode
.idea
# docker
Dockerfile
.dockerignore
docker-compose.yml
# SQL scripts
*.sql
# other
README.md

10
.env.example Normal file
View File

@@ -0,0 +1,10 @@
# Docker Compose Environment Variables
# Kopieren Sie diese Datei nach .env und passen Sie die Werte an
# MySQL Datenbankzugangsdaten
DB_USER=root
DB_PASS=IhrMySQLPasswort
DB_NAME=RXF
# Build-Datum (wird automatisch beim Build gesetzt, Format: DD.MM.YYYY)
BUILD_DATE=$(date +%d.%m.%Y)

1
.gitignore vendored
View File

@@ -32,6 +32,7 @@ yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
!.env.example
# vercel
.vercel

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
audit-level=high

317
DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,317 @@
# Deployment Anleitung - Werte Next.js App
## Voraussetzungen
Auf dem Server benötigt:
- Docker (Version 20.10+)
- Docker Compose (Version 2.0+)
- MySQL Server (bereits installiert und laufend)
- RXF Datenbank mit Tabelle Werte_BZG muss bereits existieren
## Deployment Schritte
### 1. Projekt auf den Server übertragen
```bash
# Auf dem Server ein Verzeichnis erstellen
mkdir -p /opt/werte-next
cd /opt/werte-next
# Projekt per Git oder rsync übertragen
# Option A: Git
git clone <repository-url> .
# Option B: rsync (von lokalem Rechner aus)
rsync -av --exclude 'node_modules' --exclude '.next' \
/Users/rxf/REXFUE_APPS/werte_next/ user@server:/opt/werte-next/
```
### 2. Umgebungsvariablen konfigurieren
```bash
# .env-Datei aus Beispiel erstellen
cp .env.example .env
# .env-Datei mit Ihren Zugangsdaten bearbeiten
nano .env
```
Passen Sie die Werte in der `.env`-Datei an:
```bash
DB_USER=root
DB_PASS=IhrMySQLPasswort
DB_NAME=RXF
```
**Wichtig**: Die `.env`-Datei enthält sensible Daten und wird NICHT ins Git-Repository committed!
### 3. Docker Images bauen und Container starten
```bash
cd /opt/werte-next
# Images bauen und Container im Hintergrund starten
docker-compose up -d --build
```
### 4. Container-Status prüfen
```bash
# Container-Status anzeigen
docker-compose ps
# Logs anschauen
docker-compose logs -f werte-app
```
### 5. Datenbank prüfen
```bash
# Direkt auf Host-MySQL verbinden
mysql -uroot -p<IhrPasswort> RXF
# Tabelle prüfen
SHOW TABLES;
DESCRIBE Werte_BZG;
SELECT COUNT(*) FROM Werte_BZG;
# Oder von außen (falls MySQL externen Zugriff erlaubt)
mysql -h <server-ip> -uroot -p<IhrPasswort> RXF
```
### 6. Anwendung im Browser öffnen
```
http://<server-ip>:3000
```
## Konfiguration anpassen
### Port ändern
In `docker-compose.yml` den Port für die App ändern:
```yaml
ports:
- "8080:3000" # Zugriff über Port 8080
```
### Datenbankverbindung anpassen
In `docker-compose.yml` die Umgebungsvariablen ändern:
```yaml
environment:
- DB_HOST=host.docker.internal # Für Host-MySQL
- DB_USER=root # MySQL-Benutzer
- DB_PASS=IhrPasswort # MySQL-Passwort
- DB_NAME=RXF # Datenbankname
```
**Hinweis**: `host.docker.internal` zeigt auf den Docker-Host. Dies wird durch die `extra_hosts` Konfiguration ermöglicht.
## Verwaltung
### Container stoppen
```bash
docker-compose stop
```
### Container starten
```bash
docker-compose start
```
### Container neustarten
```bash
docker-compose restart
```
### Container entfernen (Daten bleiben erhalten)
```bash
docker-compose down
```
### Alles löschen inkl. Daten
```bash
docker-compose down -v
```
### Updates einspielen
```bash
# Neue Version holen
git pull # oder rsync
# Neu bauen und starten
docker-compose up -d --build
```
### Logs anschauen
```bash
# App-Logs
docker-compose logs -f werte-app
# Letzte 100 Zeilen
docker-compose logs --tail=100 werte-app
```
## Backup
### Datenbank-Backup erstellen
```bash
# Direkt auf dem Host (empfohlen)
# Passwort wird interaktiv abgefragt
mysqldump -uroot -p RXF > backup_$(date +%Y%m%d).sql
# Oder mit gzip komprimieren
mysqldump -uroot -p RXF | gzip > backup_$(date +%Y%m%d).sql.gz
# Nur die Werte_BZG Tabelle
mysqldump -uroot -p RXF Werte_BZG > werte_backup_$(date +%Y%m%d).sql
```
### Datenbank wiederherstellen
```bash
# Aus Backup wiederherstellen
mysql -uroot -p RXF < backup_20260222.sql
# Aus komprimiertem Backup
gunzip < backup_20260222.sql.gz | mysql -uroot -p RXF
```
## Reverse Proxy (Optional)
Für Produktionsumgebungen empfiehlt sich ein Reverse Proxy wie Nginx:
### Nginx-Konfiguration Beispiel
```nginx
server {
listen 80;
server_name werte.example.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
### Mit SSL (Let's Encrypt)
```bash
# Certbot installieren
apt install certbot python3-certbot-nginx
# Zertifikat erstellen
certbot --nginx -d werte.example.com
```
## Troubleshooting
### Container startet nicht
```bash
# Detaillierte Logs anschauen
docker-compose logs werte-app
# Container neu bauen
docker-compose build --no-cache werte-app
docker-compose up -d
```
### Datenbankverbindung schlägt fehl
```bash
# Prüfen ob MySQL auf dem Host läuft
sudo systemctl status mysql
# oder
sudo service mysql status
# MySQL Error Log prüfen
sudo tail -f /var/log/mysql/error.log
# Verbindung vom Docker-Container testen
docker exec -it werte-next-app sh
# Im Container:
ping host.docker.internal
```
**Hinweis**: Wenn `host.docker.internal` nicht funktioniert, können Sie stattdessen die Server-IP verwenden:
```yaml
environment:
- DB_HOST=192.168.1.100 # Ersetzen mit tatsächlicher Server-IP
```
### App ist langsam
```bash
# Ressourcen-Nutzung prüfen
docker stats
# Container neu starten
docker-compose restart werte-app
```
## Systemd Service (Optional)
Für automatischen Start beim Server-Reboot:
```bash
# Service-Datei erstellen
sudo nano /etc/systemd/system/werte-next.service
```
Inhalt:
```ini
[Unit]
Description=Werte Next.js Application
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/werte-next
ExecStart=/usr/bin/docker-compose up -d
ExecStop=/usr/bin/docker-compose down
TimeoutStartSec=0
[Install]
WantedBy=multi-user.target
```
Service aktivieren:
```bash
sudo systemctl enable werte-next
sudo systemctl start werte-next
sudo systemctl status werte-next
```
## Monitoring
CPU und Memory-Nutzung überwachen:
```bash
docker stats werte-next-app
```
## Support
Bei Problemen:
- Logs prüfen: `docker-compose logs -f`
- Container neu starten: `docker-compose restart`
- Issues auf GitHub melden

51
Dockerfile Normal file
View File

@@ -0,0 +1,51 @@
# Multi-stage build for Next.js application
FROM node:22-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Copy package files
COPY package.json package-lock.json* ./
RUN npm ci
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Set build date as build argument
ARG BUILD_DATE
ENV NEXT_PUBLIC_BUILD_DATE=${BUILD_DATE}
# Disable telemetry during build
ENV NEXT_TELEMETRY_DISABLED=1
# Build the application
RUN npm run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy necessary files
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

189
README.md
View File

@@ -1,36 +1,185 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
# Werte Next.js - Gesundheitsdaten Tracking
## Getting Started
Eine moderne Next.js-Anwendung zur Erfassung und Verwaltung von Gesundheitsdaten, basierend auf dem ursprünglichen Werte-Projekt.
First, run the development server:
## Features
-**Eingabeformular** für Gesundheitsdaten:
- Datum und Zeit
- Blutzucker (mg/dl)
- Mahlzeiten-Informationen
- Gewicht (kg)
- Blutdruck (systolisch/diastolisch in mmHg)
- Puls
-**Anzeige der letzten 10 Einträge** mit Löschfunktion
-**Responsive Design** mit Tailwind CSS
-**TypeScript** für Type-Safety
-**MySQL-Datenbankanbindung** (Docker-Container)
## Technologie-Stack
- **Framework**: Next.js 16.x (App Router)
- **Sprache**: TypeScript
- **Styling**: Tailwind CSS
- **Datenbank**: MySQL 5.6 (Docker)
- **ORM**: mysql2
## Voraussetzungen
- Node.js 18.x oder höher
- Docker (für MySQL-Container)
- Die MySQL-Datenbank muss bereits laufen (siehe docker-compose.yml im übergeordneten Verzeichnis)
## Installation
1. **Dependencies installieren**:
```bash
npm install
```
2. **Umgebungsvariablen konfigurieren**:
Die Datei `.env.local` ist bereits mit den korrekten Datenbankzugangsdaten angelegt:
```
DB_HOST=mydbase_mysql
DB_USER=root
DB_PASS=SFluorit
DB_NAME=RXF
```
3. **Datenbank prüfen**:
Stelle sicher, dass die MySQL-Datenbank läuft und die Tabelle `RXF.Werte_BZG` existiert.
## Entwicklung
Development-Server starten:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
Die Anwendung ist dann unter [http://localhost:3000](http://localhost:3000) erreichbar.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
## Produktion
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
### Lokale Produktion
## Learn More
Build erstellen:
To learn more about Next.js, take a look at the following resources:
```bash
npm run build
```
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
Produktions-Server starten:
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
```bash
npm start
```
## Deploy on Vercel
### Docker Deployment
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Für das Deployment auf einem externen Server ist die Anwendung vollständig containerisiert. Alle Details zur Installation und Verwaltung finden Sie in der **[DEPLOYMENT.md](DEPLOYMENT.md)**.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
Kurzanleitung:
```bash
# Docker-Container bauen und starten
docker-compose up -d --build
# Status prüfen
docker-compose ps
# Logs anschauen
docker-compose logs -f
```
Die Anwendung ist dann unter `http://<server-ip>:3000` erreichbar.
## Projekt-Struktur
```
werte_next/
├── app/
│ ├── api/
│ │ └── werte/
│ │ ├── route.ts # GET/POST API für Einträge
│ │ └── [id]/
│ │ └── route.ts # DELETE API für einzelne Einträge
│ ├── layout.tsx # Root Layout
│ ├── page.tsx # Hauptseite
│ └── globals.css # Globale Styles
├── components/
│ ├── WerteForm.tsx # Eingabeformular
│ └── WerteList.tsx # Liste der letzten Einträge
├── lib/
│ └── db.ts # Datenbankverbindung
├── types/
│ └── werte.ts # TypeScript-Typen
├── .env.local # Umgebungsvariablen (lokal, gitignored)
└── package.json
```
## API-Endpunkte
### GET /api/werte
Holt die letzten N Einträge aus der Datenbank.
**Query Parameters**:
- `limit` (optional): Anzahl der Einträge (Standard: 10)
**Response**:
```json
{
"success": true,
"data": [...]
}
```
### POST /api/werte
Erstellt einen neuen Eintrag.
**Request Body**:
```json
{
"Datum": "2026-02-21",
"Zeit": "14:30",
"Zucker": 120,
"Essen": "nüchtern",
"Gewicht": 75.5,
"DruckS": 130,
"DruckD": 85,
"Puls": 72
}
```
### DELETE /api/werte/[id]
Löscht einen Eintrag anhand der ID.
## Design
Das Design orientiert sich am ursprünglichen Werte-Projekt:
- **Hintergrundfarbe**: #FFFFDD (hellgelb)
- **Eingabebereich**: #CCCCFF (hellblau)
- **Buttons**: #85B7D7 (blau)
- **Schriftarten**: Arial, Verdana, Helvetica
## Unterschiede zum Original
- **Modernes Framework**: Next.js statt PHP
- **TypeScript**: Type-Safety für bessere Wartbarkeit
- **React**: Komponentenbasierte Architektur
- **API Routes**: RESTful API statt direkter PHP-Skripte
- **Vereinfachte Features**: Fokus auf Eingabe und letzte 10 Einträge (Listen- und Statistik-Tabs wurden weggelassen wie gewünscht)
## Bekannte Einschränkungen
- Die Tabs "Liste" und "Statistik" aus dem Original wurden bewusst nicht implementiert
- Die jqGrid-Funktionalität wurde durch eine einfache Tabelle ersetzt
## Support
Bei Fragen oder Problemen: [rxf@gmx.de](mailto:rxf@gmx.de)
## Lizenz
Private Nutzung

View File

@@ -0,0 +1,56 @@
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { CreateWerteEntry } from '@/types/werte';
const TABLE = 'RXF.Werte_BZG';
export async function PUT(
request: NextRequest,
context: { params: Promise<{ id: string }> }
) {
try {
const { id } = await context.params;
const body: CreateWerteEntry = await request.json();
const sql = `UPDATE ${TABLE} SET
Datum = '${body.Datum}',
Zeit = '${body.Zeit}',
Zucker = ${body.Zucker || 'NULL'},
Essen = ${body.Essen ? `'${body.Essen.replace(/'/g, "''")}'` : 'NULL'},
Gewicht = ${body.Gewicht || 'NULL'},
DruckS = ${body.DruckS || 'NULL'},
DruckD = ${body.DruckD || 'NULL'},
Puls = ${body.Puls || 'NULL'}
WHERE ID = ${parseInt(id, 10)}`;
const result = await query(sql);
return NextResponse.json({ success: true, data: result });
} catch (error) {
console.error('Database error:', error);
return NextResponse.json(
{ success: false, error: 'Failed to update entry' },
{ status: 500 }
);
}
}
export async function DELETE(
request: NextRequest,
context: { params: Promise<{ id: string }> }
) {
try {
const { id } = await context.params;
const sql = `DELETE FROM ${TABLE} WHERE ID = ${parseInt(id, 10)}`;
const result = await query(sql);
return NextResponse.json({ success: true, data: result });
} catch (error) {
console.error('Database error:', error);
return NextResponse.json(
{ success: false, error: 'Failed to delete entry' },
{ status: 500 }
);
}
}

63
app/api/werte/route.ts Normal file
View File

@@ -0,0 +1,63 @@
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { CreateWerteEntry } from '@/types/werte';
const TABLE = 'RXF.Werte_BZG';
// GET - Fetch entries
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const limit = parseInt(searchParams.get('limit') || '10', 10);
const sql = `SELECT ID, DATE_FORMAT(Datum, '%Y-%m-%d') as Datum, Zeit, Zucker, Essen, Gewicht, DruckD, DruckS, Puls FROM ${TABLE} ORDER BY Datum DESC, Zeit DESC LIMIT ${limit}`;
const rows = await query(sql);
return NextResponse.json(
{ success: true, data: rows },
{
headers: {
'Cache-Control': 'no-store, no-cache, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0',
},
}
);
} catch (error) {
console.error('Database error:', error);
return NextResponse.json(
{ success: false, error: 'Failed to fetch entries' },
{ status: 500 }
);
}
}
// POST - Create new entry
export async function POST(request: NextRequest) {
try {
const body: CreateWerteEntry = await request.json();
const sql = `INSERT INTO ${TABLE} (Datum, Zeit, Zucker, Essen, Gewicht, DruckS, DruckD, Puls) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`;
const params = [
body.Datum,
body.Zeit,
body.Zucker || null,
body.Essen || null,
body.Gewicht || null,
body.DruckS || null,
body.DruckD || null,
body.Puls || null,
];
const result = await query(sql, params);
return NextResponse.json({ success: true, data: result });
} catch (error) {
console.error('Database error:', error);
return NextResponse.json(
{ success: false, error: 'Failed to create entry' },
{ status: 500 }
);
}
}

View File

@@ -1,3 +1,4 @@
/* stylelint-disable at-rule-no-unknown */
@import "tailwindcss";
:root {

View File

@@ -13,8 +13,8 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Werte - Log",
description: "Gesundheitsdaten Tracking Anwendung",
};
export default function RootLayout({
@@ -23,7 +23,7 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<html lang="de">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>

View File

@@ -1,64 +1,110 @@
import Image from "next/image";
'use client';
import { useState, useEffect } from 'react';
import WerteForm from '@/components/WerteForm';
import WerteList from '@/components/WerteList';
import { WerteEntry } from '@/types/werte';
import packageJson from '@/package.json';
export default function Home() {
const [entries, setEntries] = useState<WerteEntry[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [selectedEntry, setSelectedEntry] = useState<WerteEntry | null>(null);
const version = packageJson.version;
const buildDate = process.env.NEXT_PUBLIC_BUILD_DATE || new Date().toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
useEffect(() => {
let isMounted = true;
// Fetch initial data
(async () => {
try {
const response = await fetch('/api/werte?limit=14', {
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache',
},
});
const data = await response.json();
if (data.success && isMounted) {
setEntries(data.data);
}
} catch (error) {
console.error('Error fetching entries:', error);
}
if (isMounted) {
setIsLoading(false);
}
})();
return () => {
isMounted = false;
};
}, []);
const refreshEntries = async () => {
try {
const response = await fetch('/api/werte?limit=14', {
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache',
},
});
const data = await response.json();
if (data.success) {
setEntries(data.data);
}
} catch (error) {
console.error('Error fetching entries:', error);
}
};
const handleSuccess = () => {
setSelectedEntry(null);
// Small delay to ensure database commit is complete
setTimeout(() => {
refreshEntries();
}, 100);
};
const handleDelete = (id: number) => {
setEntries(entries.filter(entry => entry.ID !== id));
};
const handleEdit = (entry: WerteEntry) => {
setSelectedEntry(entry);
// Scroll to top
window.scrollTo({ top: 0, behavior: 'smooth' });
};
return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
<div className="min-h-screen bg-white py-4 px-4">
<main className="max-w-6xl mx-auto border-2 border-black rounded-lg p-6 bg-[#FFFFDD]">
<h1 className="text-3xl font-bold text-center mb-6">Werte - Log</h1>
<div className="mb-8">
<h2 className="text-xl font-semibold mb-4">Eingabe</h2>
<WerteForm onSuccess={handleSuccess} selectedEntry={selectedEntry} />
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
<div className="bg-white border border-black rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Letzte 14 Einträge</h2>
{isLoading ? (
<div className="text-center py-4">Lade Daten...</div>
) : (
<WerteList entries={entries} onDelete={handleDelete} onEdit={handleEdit} />
)}
</div>
<footer className="mt-8 flex justify-between items-center text-sm text-gray-600 px-4">
<a href="mailto:rxf@gmx.de" className="hover:underline">
rxf@gmx.de
</a>
<div>
Version {version} - {buildDate}
</div>
</footer>
</main>
</div>
);

274
components/WerteForm.tsx Normal file
View File

@@ -0,0 +1,274 @@
'use client';
import { useState, useEffect } from 'react';
import { CreateWerteEntry, WerteEntry } from '@/types/werte';
interface WerteFormProps {
onSuccess: () => void;
selectedEntry?: WerteEntry | null;
}
export default function WerteForm({ onSuccess, selectedEntry }: WerteFormProps) {
const [formData, setFormData] = useState<CreateWerteEntry>({
Datum: '',
Zeit: '',
Zucker: '',
Essen: 'nüchtern',
Gewicht: '',
DruckS: '',
DruckD: '',
Puls: '',
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [weekday, setWeekday] = useState('');
const [editId, setEditId] = useState<number | null>(null);
useEffect(() => {
if (selectedEntry) {
// Load selected entry for editing
const dateStr = selectedEntry.Datum.toString().split('T')[0];
const timeStr = selectedEntry.Zeit.toString().substring(0, 5);
setFormData({
Datum: dateStr,
Zeit: timeStr,
Zucker: selectedEntry.Zucker || '',
Essen: selectedEntry.Essen || '',
Gewicht: selectedEntry.Gewicht || '',
DruckS: selectedEntry.DruckS || '',
DruckD: selectedEntry.DruckD || '',
Puls: selectedEntry.Puls || '',
});
setEditId(selectedEntry.ID || null);
// Parse date to avoid timezone issues
const [year, month, day] = dateStr.split('T')[0].split('-');
const date = new Date(Number(year), Number(month) - 1, Number(day));
updateWeekday(date);
} else {
// Initialize with current date and time for new entry
const now = new Date();
const dateStr = now.toISOString().split('T')[0];
const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
setFormData(prev => ({
...prev,
Datum: dateStr,
Zeit: timeStr,
}));
setEditId(null);
updateWeekday(now);
}
}, [selectedEntry]);
const updateWeekday = (date: Date) => {
const weekdays = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
setWeekday(weekdays[date.getDay()]);
};
const handleDateChange = (dateStr: string) => {
setFormData(prev => ({ ...prev, Datum: dateStr }));
// Parse date to avoid timezone issues
const [year, month, day] = dateStr.split('-');
const date = new Date(Number(year), Number(month) - 1, Number(day));
updateWeekday(date);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
try {
const url = editId ? `/api/werte/${editId}` : '/api/werte';
const method = editId ? 'PUT' : 'POST';
const response = await fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
if (response.ok) {
// Reset form but keep date and time
const now = new Date();
const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
setFormData(prev => ({
...prev,
Zeit: timeStr,
Zucker: '',
Essen: 'nüchtern',
Gewicht: '',
DruckS: '',
DruckD: '',
Puls: '',
}));
setEditId(null);
onSuccess();
} else {
alert('Fehler beim Speichern!');
}
} catch (error) {
console.error('Error:', error);
alert('Fehler beim Speichern!');
} finally {
setIsSubmitting(false);
}
};
const handleReset = () => {
const now = new Date();
const dateStr = now.toISOString().split('T')[0];
const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
setFormData({
Datum: dateStr,
Zeit: timeStr,
Zucker: '',
Essen: 'nüchtern',
Gewicht: '',
DruckS: '',
DruckD: '',
Puls: '',
});
setEditId(null);
updateWeekday(now);
};
return (
<div className="bg-[#CCCCFF] border border-black p-6 rounded-lg mb-6">
{editId && (
<div className="mb-4 p-3 bg-blue-100 border border-blue-400 rounded text-sm text-blue-800">
<strong>Bearbeitungsmodus:</strong> Sie bearbeiten einen bestehenden Eintrag. Klicken Sie auf &quot;Aktualisieren&quot;, um die Änderungen zu speichern, oder &quot;Abbrechen&quot;, um zur Neuerfassung zurückzukehren.
</div>
)}
<form onSubmit={handleSubmit}>
<table className="w-full text-center">
<thead>
<tr className="border-b-2 border-gray-400">
<th className="p-2">Datum</th>
<th className="p-2">Zeit</th>
<th className="p-2">Zucker</th>
<th className="p-2">Essen</th>
<th className="p-2">Gewicht</th>
<th className="p-2">Druck sys</th>
<th className="p-2">Druck dia</th>
<th className="p-2">Puls</th>
</tr>
<tr className="border-b-2 border-gray-400">
<th className="p-2 font-normal text-sm">{weekday}</th>
<th className="p-2"></th>
<th className="p-2 font-normal text-sm">mg/dl</th>
<th className="p-2"></th>
<th className="p-2 font-normal text-sm">kg</th>
<th className="p-2 font-normal text-sm">mmHg</th>
<th className="p-2 font-normal text-sm">mmHg</th>
<th className="p-2 font-normal text-sm">bpm</th>
</tr>
</thead>
<tbody>
<tr>
<td className="p-2">
<input
type="date"
className="w-full px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none"
value={formData.Datum}
onChange={(e) => handleDateChange(e.target.value)}
required
/>
</td>
<td className="p-2">
<input
type="time"
className="w-full px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none"
value={formData.Zeit}
onChange={(e) => setFormData(prev => ({ ...prev, Zeit: e.target.value }))}
required
/>
</td>
<td className="p-2">
<input
type="number"
className="w-20 px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none"
value={formData.Zucker}
onChange={(e) => setFormData(prev => ({ ...prev, Zucker: e.target.value }))}
maxLength={4}
/>
</td>
<td className="p-2">
<textarea
className="w-full px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none resize-none align-middle"
style={{ height: '28px' }}
value={formData.Essen}
onChange={(e) => setFormData(prev => ({ ...prev, Essen: e.target.value }))}
rows={1}
/>
</td>
<td className="p-2">
<input
type="number"
step="0.1"
className="w-20 px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none"
value={formData.Gewicht}
onChange={(e) => setFormData(prev => ({ ...prev, Gewicht: e.target.value }))}
maxLength={4}
/>
</td>
<td className="p-2">
<input
type="number"
className="w-20 px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none"
value={formData.DruckS}
onChange={(e) => setFormData(prev => ({ ...prev, DruckS: e.target.value }))}
maxLength={4}
/>
</td>
<td className="p-2">
<input
type="number"
className="w-20 px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none"
value={formData.DruckD}
onChange={(e) => setFormData(prev => ({ ...prev, DruckD: e.target.value }))}
maxLength={4}
/>
</td>
<td className="p-2">
<input
type="number"
className="w-20 px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none"
value={formData.Puls}
onChange={(e) => setFormData(prev => ({ ...prev, Puls: e.target.value }))}
maxLength={4}
/>
</td>
</tr>
</tbody>
</table>
<div className="flex justify-center gap-4 mt-6">
<button
type="submit"
disabled={isSubmitting}
className="bg-[#85B7D7] hover:bg-[#6a9fc5] text-black font-medium py-2 px-8 rounded-lg transition-colors disabled:opacity-50"
>
{isSubmitting ? (editId ? 'Aktualisieren...' : 'Speichern...') : (editId ? 'Aktualisieren' : 'Speichern')}
</button>
<button
type="button"
onClick={handleReset}
className="bg-[#85B7D7] hover:bg-[#6a9fc5] text-black font-medium py-2 px-8 rounded-lg transition-colors"
>
{editId ? 'Abbrechen' : 'Löschen'}
</button>
</div>
</form>
</div>
);
}

135
components/WerteList.tsx Normal file
View File

@@ -0,0 +1,135 @@
'use client';
import { WerteEntry } from '@/types/werte';
interface WerteListProps {
entries: WerteEntry[];
onDelete?: (id: number) => void;
onEdit?: (entry: WerteEntry) => void;
}
export default function WerteList({ entries, onDelete, onEdit }: WerteListProps) {
const formatValue = (value: number | string | null | undefined) => {
if (value === null || value === undefined || value === '' || value === '0' || value === '0.0') {
return '-';
}
return value;
};
const formatDate = (dateStr: string) => {
// Convert YYYY-MM-DD to DD.MM.YYYY
if (!dateStr) return '-';
// Parse the date string directly (MySQL returns YYYY-MM-DD)
const parts = dateStr.toString().split('T')[0].split('-');
if (parts.length === 3) {
return `${parts[2]}.${parts[1]}.${parts[0]}`;
}
return dateStr;
};
const formatTime = (timeStr: string) => {
// Convert HH:MM:SS to HH:MM
if (!timeStr) return '-';
const timeString = timeStr.toString();
return timeString.substring(0, 5);
};
const getWeekday = (dateStr: string) => {
if (!dateStr) return '-';
const weekdays = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
// Parse date parts to avoid timezone issues
const [year, month, day] = dateStr.toString().split('T')[0].split('-');
const date = new Date(Number(year), Number(month) - 1, Number(day));
return weekdays[date.getDay()];
};
const handleDelete = async (id: number) => {
if (!confirm('Eintrag wirklich löschen?')) {
return;
}
try {
const response = await fetch(`/api/werte/${id}`, {
method: 'DELETE',
});
if (response.ok && onDelete) {
onDelete(id);
} else {
alert('Fehler beim Löschen!');
}
} catch (error) {
console.error('Error:', error);
alert('Fehler beim Löschen!');
}
};
if (entries.length === 0) {
return (
<div className="text-center py-8 text-gray-500">
Keine Einträge vorhanden
</div>
);
}
return (
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="bg-[#CCCCFF] border-b-2 border-gray-400">
<th className="p-2 text-center">Datum</th>
<th className="p-2 text-center">Tag</th>
<th className="p-2 text-center">Zeit</th>
<th className="p-2 text-center">Zucker<br/><span className="text-xs font-normal">mg/dl</span></th>
<th className="p-2 text-center">Essen</th>
<th className="p-2 text-center">Gewicht<br/><span className="text-xs font-normal">kg</span></th>
<th className="p-2 text-center">Druck sys<br/><span className="text-xs font-normal">mmHg</span></th>
<th className="p-2 text-center">Druck dia<br/><span className="text-xs font-normal">mmHg</span></th>
<th className="p-2 text-center">Puls<br/><span className="text-xs font-normal">bpm</span></th>
{onDelete && <th className="p-2 text-center">Aktion</th>}
</tr>
</thead>
<tbody>
{entries.map((entry, index) => (
<tr
key={entry.ID}
className={`border-b border-gray-300 hover:bg-gray-50 ${index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}`}
>
<td className="p-2 text-center">{formatDate(entry.Datum)}</td>
<td className="p-2 text-center">{getWeekday(entry.Datum)}</td>
<td className="p-2 text-center">{formatTime(entry.Zeit)}</td>
<td className="p-2 text-center">{formatValue(entry.Zucker)}</td>
<td className="p-2 text-center">{formatValue(entry.Essen)}</td>
<td className="p-2 text-center">{formatValue(entry.Gewicht)}</td>
<td className="p-2 text-center">{formatValue(entry.DruckS)}</td>
<td className="p-2 text-center">{formatValue(entry.DruckD)}</td>
<td className="p-2 text-center">{formatValue(entry.Puls)}</td>
{(onDelete || onEdit) && (
<td className="p-2 text-center">
<div className="flex gap-2 justify-center">
{onEdit && (
<button
onClick={() => onEdit(entry)}
className="text-blue-600 hover:text-blue-800 text-sm"
>
Editieren
</button>
)}
{onDelete && (
<button
onClick={() => entry.ID && handleDelete(entry.ID)}
className="text-red-600 hover:text-red-800 text-sm"
>
Löschen
</button>
)}
</div>
</td>
)}
</tr>
))}
</tbody>
</table>
</div>
);
}

18
create_table.sql Normal file
View File

@@ -0,0 +1,18 @@
-- Create database and table Werte_BZG for health data tracking
CREATE DATABASE IF NOT EXISTS RXF CHARACTER SET utf8 COLLATE utf8_general_ci;
USE RXF;
CREATE TABLE IF NOT EXISTS Werte_BZG (
ID INT AUTO_INCREMENT PRIMARY KEY,
Datum DATE NOT NULL,
Zeit TIME NOT NULL,
Zucker INT NULL COMMENT 'Blood sugar in mg/dl',
Essen TEXT NULL COMMENT 'Meal information',
Gewicht DECIMAL(5,1) NULL COMMENT 'Weight in kg',
DruckS INT NULL COMMENT 'Systolic blood pressure in mmHg',
DruckD INT NULL COMMENT 'Diastolic blood pressure in mmHg',
Puls INT NULL COMMENT 'Pulse rate',
INDEX idx_datum_zeit (Datum, Zeit)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;

36
docker-compose.yml Normal file
View File

@@ -0,0 +1,36 @@
services:
werte-app:
build:
context: .
args:
BUILD_DATE: ${BUILD_DATE:-$(date +%d.%m.%Y)}
container_name: werte-next-app
restart: unless-stopped
# Port wird nicht nach außen exponiert - Traefik greift über das Docker-Netzwerk zu
expose:
- 3000
environment:
- NODE_ENV=production
- DB_HOST=${DB_HOST}
- DB_USER=${DB_USER}
- DB_PASS=${DB_PASS}
- DB_NAME=${DB_NAME}
labels:
- traefik.enable=true
- traefik.http.routers.werte.entrypoints=http
- traefik.http.routers.werte.rule=Host(`werte.fuerst-stuttgart.de`)
- traefik.http.middlewares.werte-https-redirect.redirectscheme.scheme=https
- traefik.http.routers.werte.middlewares=werte-https-redirect
- traefik.http.routers.werte-secure.entrypoints=https
- traefik.http.routers.werte-secure.rule=Host(`werte.fuerst-stuttgart.de`)
- traefik.http.routers.werte-secure.tls=true
- traefik.http.routers.werte-secure.tls.certresolver=letsencrypt
- traefik.http.routers.werte-secure.service=werte
- traefik.http.services.werte.loadbalancer.server.port=3000
networks:
- proxy
networks:
proxy:
name: dockge_default
external: true

29
lib/db.ts Normal file
View File

@@ -0,0 +1,29 @@
import mysql from 'mysql2/promise';
import type { QueryResult } from 'mysql2/promise';
// Database configuration
const dbConfig = {
host: process.env.DB_HOST || 'mydbase_mysql',
user: process.env.DB_USER || 'root',
password: process.env.DB_PASS || 'SFluorit',
database: process.env.DB_NAME || 'RXF',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
};
// Create a connection pool
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<QueryResult> {
const pool = getPool();
const [rows] = await pool.execute(sql, params || []);
return rows as QueryResult;
}

View File

@@ -1,7 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
output: 'standalone',
};
export default nextConfig;

163
package-lock.json generated
View File

@@ -1,13 +1,14 @@
{
"name": "werte_next",
"version": "0.1.0",
"version": "0.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "werte_next",
"version": "0.1.0",
"version": "0.1.1",
"dependencies": {
"mysql2": "^3.17.4",
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3"
@@ -1766,24 +1767,37 @@
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"version": "9.0.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz",
"integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
"brace-expansion": "^5.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
@@ -2396,6 +2410,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/aws-ssl-profiles": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
"license": "MIT",
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/axe-core": {
"version": "4.11.1",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz",
@@ -2775,6 +2798,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -3233,6 +3265,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -3654,6 +3687,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/generate-function": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
"license": "MIT",
"dependencies": {
"is-property": "^1.0.2"
}
},
"node_modules/generator-function": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
@@ -3918,6 +3960,22 @@
"hermes-estree": "0.25.1"
}
},
"node_modules/iconv-lite": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -4240,6 +4298,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-property": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
"license": "MIT"
},
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@@ -4838,6 +4902,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -4861,6 +4931,21 @@
"yallist": "^3.0.2"
}
},
"node_modules/lru.min": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
"integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
"license": "MIT",
"engines": {
"bun": ">=1.0.0",
"deno": ">=1.30.0",
"node": ">=8.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wellwelwel"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -4906,9 +4991,9 @@
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -4935,6 +5020,37 @@
"dev": true,
"license": "MIT"
},
"node_modules/mysql2": {
"version": "3.17.4",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.17.4.tgz",
"integrity": "sha512-RnfuK5tyIuaiPMWOCTTl4vQX/mQXqSA8eoIbwvWccadvPGvh+BYWWVecInMS5s7wcLUkze8LqJzwB/+A4uwuAA==",
"license": "MIT",
"dependencies": {
"aws-ssl-profiles": "^1.1.2",
"denque": "^2.1.0",
"generate-function": "^2.3.1",
"iconv-lite": "^0.7.2",
"long": "^5.3.2",
"lru.min": "^1.1.4",
"named-placeholders": "^1.1.6",
"sql-escaper": "^1.3.3"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/named-placeholders": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
"integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
"license": "MIT",
"dependencies": {
"lru.min": "^1.1.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -5630,6 +5746,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -5861,6 +5983,21 @@
"node": ">=0.10.0"
}
},
"node_modules/sql-escaper": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz",
"integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==",
"license": "MIT",
"engines": {
"bun": ">=1.0.0",
"deno": ">=2.0.0",
"node": ">=12.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
}
},
"node_modules/stable-hash": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "werte_next",
"version": "0.1.0",
"version": "0.1.1",
"private": true,
"scripts": {
"dev": "next dev",
@@ -9,6 +9,7 @@
"lint": "eslint"
},
"dependencies": {
"mysql2": "^3.17.4",
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3"

22
types/werte.ts Normal file
View File

@@ -0,0 +1,22 @@
export interface WerteEntry {
ID?: number;
Datum: string;
Zeit: string;
Zucker?: number | string;
Essen?: string;
Gewicht?: number | string;
DruckS?: number | string;
DruckD?: number | string;
Puls?: number | string;
}
export interface CreateWerteEntry {
Datum: string;
Zeit: string;
Zucker?: number | string;
Essen?: string;
Gewicht?: number | string;
DruckS?: number | string;
DruckD?: number | string;
Puls?: number | string;
}