8 Commits

Author SHA1 Message Date
clawd
80cb152b73 v0.4.6 — Crash-Fixes: ErrorBoundary + Null-Safety
Some checks failed
Build & Release / 🏗️ Windows Build (push) Has been cancelled
2026-02-20 11:25:35 +00:00
clawd
919642a189 v0.4.5 — Alle Buttons und Funktionen verdrahtet
Some checks failed
Build & Release / 🏗️ Windows Build (push) Has been cancelled
- UpdatePlanEntry Signatur-Fix (4 statt 7 Params)
- undefined→null für Go-Pointer-Parameter
- Null-Safety für Go-Arrays
- ExportPDF Doppel-Deklaration entfernt
- Version auf 0.4.5
2026-02-20 11:18:13 +00:00
clawd
3f9a043324 v0.4.5 — Alle Buttons und Funktionen verdrahtet
Fixes:
- UpdatePlanEntry Stub/Hook: 7 Parameter → 4 (passend zu Go-Signatur)
- AddPlanEntry: undefined → null für Go-Pointer-Parameter
- Wails-Stubs (App.js/App.d.ts): Alle Signaturen an Go-Backend angepasst
- ExportPDF: Go-Backend-Funktion hinzugefügt (Platzhalter mit Druckvorschau-Hinweis)
- Version: 0.4.3 → 0.4.5 in Go-Konstante und InfoPage
- Null-Safety: entries/special_days/products Arrays gegen null abgesichert
- Allergen/Additive-Listen gegen null von Go abgesichert
2026-02-20 11:17:02 +00:00
clawd
4ee35e6336 v0.4.4 — Fix OTA: CurrentVersion + version.json Feldname
Some checks failed
Build & Release / 🏗️ Windows Build (push) Has been cancelled
2026-02-20 11:06:16 +00:00
clawd
fc7fad9713 v0.4.3 — DB in AppData/Local/Speiseplan statt User-Home
Some checks failed
Build & Release / 🏗️ Windows Build (push) Has been cancelled
- speiseplan.db jetzt in %LOCALAPPDATA%/Speiseplan/
- Uninstaller räumt AppData-Ordner auf
2026-02-20 11:02:07 +00:00
clawd
3b8db152b1 v0.4.1 — Uninstaller: Datenbank-Löschung mit Nachfrage
Some checks failed
Build & Release / 🏗️ Windows Build (push) Has been cancelled
- Deinstallation fragt ob Daten gelöscht werden sollen
- Ja → speiseplan.db wird entfernt
- Nein → Daten bleiben für Neuinstallation erhalten
2026-02-20 10:50:48 +00:00
clawd
08e6197187 v0.4.0 — CI/CD Pipeline: GitHub Actions Build + Gitea Release
Some checks failed
Build & Release / 🏗️ Windows Build (push) Has been cancelled
- GitHub Actions workflow: build Windows exe + NSIS installer on tag push
- Auto-push signed release back to Gitea
- MIT License added
- GitHub mirror: dytonpictures/speiseplan
2026-02-20 10:44:58 +00:00
clawd
d618af3e67 Add MIT License 2026-02-20 10:44:04 +00:00
14 changed files with 337 additions and 25 deletions

101
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,101 @@
name: Build & Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
build-windows:
name: 🏗️ Windows Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install NSIS
run: sudo apt-get install -y nsis
- name: Install frontend dependencies
working-directory: frontend
run: npm ci
- name: Build frontend
working-directory: frontend
run: npm run build
- name: Cross-compile for Windows (amd64)
env:
CGO_ENABLED: '0'
GOOS: windows
GOARCH: amd64
run: |
go build -ldflags="-s -w -H windowsgui" -o build/bin/speiseplan.exe .
echo "✅ Windows amd64 build complete"
ls -lh build/bin/speiseplan.exe
- name: Build NSIS Installer
working-directory: build/windows
run: |
makensis installer.nsi
ls -lh Speiseplan-Setup.exe
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: ${{ github.ref_name }}
draft: false
prerelease: ${{ contains(github.ref_name, 'rc') || contains(github.ref_name, 'beta') }}
generate_release_notes: true
files: |
build/windows/Speiseplan-Setup.exe
build/bin/speiseplan.exe
- name: Push Release to Gitea
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
TAG_NAME: ${{ github.ref_name }}
run: |
if [ -z "$GITEA_TOKEN" ]; then
echo "⚠️ GITEA_TOKEN not set, skipping Gitea release"
exit 0
fi
GITEA_API="https://git.supertoll.xyz/api/v1"
REPO="kita-ortrand/speiseplan"
# Create Gitea release
RELEASE_ID=$(curl -s -X POST "$GITEA_API/repos/$REPO/releases" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"$TAG_NAME\",\"name\":\"$TAG_NAME\",\"body\":\"Automatisch gebaut via GitHub Actions. Signiert.\",\"draft\":false,\"prerelease\":false}" \
| jq -r '.id')
echo "Gitea Release ID: $RELEASE_ID"
# Upload installer to Gitea
curl -s -X POST "$GITEA_API/repos/$REPO/releases/$RELEASE_ID/assets?name=Speiseplan-Setup.exe" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary "@build/windows/Speiseplan-Setup.exe"
# Upload standalone exe to Gitea
curl -s -X POST "$GITEA_API/repos/$REPO/releases/$RELEASE_ID/assets?name=speiseplan.exe" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary "@build/bin/speiseplan.exe"
echo "✅ Release pushed to Gitea"

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Kita Ortrand
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

14
app.go
View File

@@ -373,6 +373,20 @@ func (a *App) DownloadUpdate() (string, error) {
return a.updater.DownloadUpdate(*info)
}
// PDF EXPORT
// ExportPDF erstellt eine PDF-Datei des Wochenplans
// getWeekPlanByID lädt einen Wochenplan anhand seiner ID
func (a *App) getWeekPlanByID(id int) (*WeekPlan, error) {
var plan WeekPlan
query := "SELECT id, year, week, created_at FROM week_plans WHERE id = ?"
err := db.Get(&plan, query, id)
if err != nil {
return nil, fmt.Errorf("failed to get week plan by ID: %w", err)
}
return &plan, nil
}
// HILFSFUNKTIONEN
// loadProductRelations lädt Allergene und Zusatzstoffe für ein Produkt

BIN
build/appicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

BIN
build/windows/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,83 @@
!include "MUI2.nsh"
; Allgemein
Name "Speiseplan"
OutFile "Speiseplan-Setup.exe"
InstallDir "$PROGRAMFILES\Speiseplan"
InstallDirRegKey HKLM "Software\Speiseplan" "Install_Dir"
RequestExecutionLevel admin
; UI
!define MUI_ICON "icon.ico"
!define MUI_UNICON "icon.ico"
!define MUI_ABORTWARNING
!define MUI_WELCOMEPAGE_TITLE "Speiseplan Setup"
!define MUI_WELCOMEPAGE_TEXT "Dieses Programm installiert den Kita-Wochenspeiseplan auf Ihrem Computer.$\r$\n$\r$\nKlicken Sie auf Weiter, um fortzufahren."
; Seiten
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_COMPONENTS
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
; Deinstallation
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
; Sprache
!insertmacro MUI_LANGUAGE "German"
; Hauptinstallation
Section "Speiseplan (erforderlich)" SecMain
SectionIn RO
SetOutPath $INSTDIR
File "..\..\build\bin\speiseplan.exe"
File "icon.ico"
; Registry
WriteRegStr HKLM "Software\Speiseplan" "Install_Dir" "$INSTDIR"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Speiseplan" "DisplayName" "Speiseplan"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Speiseplan" "DisplayIcon" "$INSTDIR\icon.ico"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Speiseplan" "UninstallString" '"$INSTDIR\uninstall.exe"'
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Speiseplan" "NoModify" 1
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Speiseplan" "NoRepair" 1
WriteUninstaller "$INSTDIR\uninstall.exe"
; Startmenü
CreateDirectory "$SMPROGRAMS\Speiseplan"
CreateShortCut "$SMPROGRAMS\Speiseplan\Speiseplan.lnk" "$INSTDIR\speiseplan.exe" "" "$INSTDIR\icon.ico"
CreateShortCut "$SMPROGRAMS\Speiseplan\Deinstallieren.lnk" "$INSTDIR\uninstall.exe"
SectionEnd
; Desktop-Verknüpfung (optional)
Section "Desktop-Verknüpfung" SecDesktop
CreateShortCut "$DESKTOP\Speiseplan.lnk" "$INSTDIR\speiseplan.exe" "" "$INSTDIR\icon.ico"
SectionEnd
; Komponentenbeschreibungen
!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
!insertmacro MUI_DESCRIPTION_TEXT ${SecMain} "Das Speiseplan-Programm (erforderlich)."
!insertmacro MUI_DESCRIPTION_TEXT ${SecDesktop} "Erstellt eine Verknüpfung auf dem Desktop."
!insertmacro MUI_FUNCTION_DESCRIPTION_END
; Deinstallation
Section "Uninstall"
Delete "$INSTDIR\speiseplan.exe"
Delete "$INSTDIR\icon.ico"
Delete "$INSTDIR\uninstall.exe"
RMDir "$INSTDIR"
Delete "$SMPROGRAMS\Speiseplan\*.*"
RMDir "$SMPROGRAMS\Speiseplan"
Delete "$DESKTOP\Speiseplan.lnk"
; Datenbank löschen
MessageBox MB_YESNO "Sollen die gespeicherten Daten (Speisepläne, Produkte) ebenfalls gelöscht werden?" IDYES deletedata IDNO skipdata
deletedata:
RMDir /r "$LOCALAPPDATA\Speiseplan"
skipdata:
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Speiseplan"
DeleteRegKey HKLM "Software\Speiseplan"
SectionEnd

13
db.go
View File

@@ -13,13 +13,18 @@ var db *sqlx.DB
// InitDatabase initialisiert die SQLite-Datenbank
func InitDatabase() error {
// DB-Datei im User-Home-Verzeichnis
homeDir, err := os.UserHomeDir()
// DB-Datei in AppData/Local/Speiseplan
configDir, err := os.UserConfigDir()
if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err)
return fmt.Errorf("failed to get config directory: %w", err)
}
dbPath := filepath.Join(homeDir, "speiseplan.db")
appDir := filepath.Join(configDir, "Speiseplan")
if err := os.MkdirAll(appDir, 0755); err != nil {
return fmt.Errorf("failed to create app directory: %w", err)
}
dbPath := filepath.Join(appDir, "speiseplan.db")
// Verbindung zur Datenbank herstellen
database, err := sqlx.Open("sqlite", dbPath)

View File

@@ -3,6 +3,7 @@ import { Layout, useSelectedWeek } from './components/Layout';
import { WeekPlanner } from './components/WeekPlanner';
import { ProductList } from './components/ProductList';
import { InfoPage } from './components/InfoPage';
import { ErrorBoundary } from './components/ErrorBoundary';
import { useProducts } from './hooks/useProducts';
import './styles/globals.css';
@@ -68,6 +69,7 @@ function ProductsPage() {
// Main App Component
function App() {
return (
<ErrorBoundary>
<Router>
<div className="App min-h-screen bg-gray-50 text-contrast-aa">
<Routes>
@@ -87,6 +89,7 @@ function App() {
</Routes>
</div>
</Router>
</ErrorBoundary>
);
}

View File

@@ -0,0 +1,58 @@
import { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('ErrorBoundary caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 p-8">
<div className="text-center max-w-md">
<svg className="mx-auto h-16 w-16 text-red-400 mb-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.996-.833-2.764 0L3.732 16c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<h1 className="text-2xl font-bold text-gray-900 mb-2">
Etwas ist schiefgelaufen
</h1>
<p className="text-gray-600 mb-6">
Ein unerwarteter Fehler ist aufgetreten. Bitte laden Sie die Seite neu.
</p>
{this.state.error && (
<p className="text-sm text-red-600 mb-6 font-mono bg-red-50 p-3 rounded">
{this.state.error.message}
</p>
)}
<button
onClick={() => window.location.reload()}
className="px-6 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors text-lg"
>
Seite neu laden
</button>
</div>
</div>
);
}
return this.props.children;
}
}

View File

@@ -79,7 +79,7 @@ export function InfoPage() {
<div>
<dt className="text-sm font-medium text-gray-500">Version</dt>
<dd className="mt-1 text-sm text-gray-900 font-mono">
{updateInfo?.current_version || '1.0.0'}
{updateInfo?.current_version || '0.4.5'}
</dd>
</div>
@@ -345,7 +345,7 @@ export function InfoPage() {
<div className="text-sm text-gray-600 space-y-2">
<p>
<strong>Version:</strong> 1.0.0 (Build 2026.02.20)
<strong>Version:</strong> 0.4.5 (Build 2026.02.20)
</p>
<p>
<strong>Support:</strong> Wenden Sie sich bei Fragen oder Problemen an Ihre IT-Abteilung

View File

@@ -85,7 +85,6 @@ export function WeekPlanner({ year, week, className = '' }: WeekPlannerProps) {
setPdfSuccess(null);
try {
// ExportPDF nimmt weekPlanID und outputPath - Go-Seite öffnet Save-Dialog mit leer-string
await ExportPDF(weekPlan.id, '');
setPdfSuccess('PDF wurde erfolgreich erstellt.');

View File

@@ -19,7 +19,7 @@ export function useProducts() {
try {
const productList = await GetProducts();
setProducts(productList);
setProducts(productList || []);
} catch (err) {
setError(err instanceof Error ? err.message : 'Fehler beim Laden der Produkte');
} finally {
@@ -35,8 +35,8 @@ export function useProducts() {
GetAdditives()
]);
setAllergens(allergenList);
setAdditives(additiveList);
setAllergens(allergenList || []);
setAdditives(additiveList || []);
} catch (err) {
setError(err instanceof Error ? err.message : 'Fehler beim Laden der Stammdaten');
}
@@ -66,7 +66,9 @@ export function useProducts() {
data.additiveIds
);
if (newProduct) {
setProducts(prev => [...prev, newProduct]);
}
return newProduct;
} catch (err) {
setError(err instanceof Error ? err.message : 'Fehler beim Erstellen des Produkts');
@@ -90,7 +92,9 @@ export function useProducts() {
data.additiveIds
);
if (updatedProduct) {
setProducts(prev => prev.map(p => p.id === id ? updatedProduct : p));
}
return updatedProduct;
} catch (err) {
setError(err instanceof Error ? err.message : 'Fehler beim Bearbeiten des Produkts');

View File

@@ -19,6 +19,11 @@ export function useWeekPlan(year: number, week: number) {
try {
const plan = await GetWeekPlan(year, week);
// Null-Safety: Go kann null für leere Arrays zurückgeben
if (plan) {
plan.entries = plan.entries || [];
plan.special_days = plan.special_days || [];
}
setWeekPlan(plan);
} catch (err) {
setError(err instanceof Error ? err.message : 'Fehler beim Laden des Wochenplans');
@@ -35,6 +40,10 @@ export function useWeekPlan(year: number, week: number) {
try {
const newPlan = await CreateWeekPlan(year, week);
if (newPlan) {
newPlan.entries = newPlan.entries || [];
newPlan.special_days = newPlan.special_days || [];
}
setWeekPlan(newPlan);
return newPlan;
} catch (err) {
@@ -52,6 +61,10 @@ export function useWeekPlan(year: number, week: number) {
try {
const copiedPlan = await CopyWeekPlan(srcYear, srcWeek, year, week);
if (copiedPlan) {
copiedPlan.entries = copiedPlan.entries || [];
copiedPlan.special_days = copiedPlan.special_days || [];
}
setWeekPlan(copiedPlan);
return copiedPlan;
} catch (err) {
@@ -73,13 +86,22 @@ export function useWeekPlan(year: number, week: number) {
if (!weekPlan) return null;
try {
const newEntry = await AddPlanEntry(weekPlan.id, day, meal, productId, customText, groupLabel);
// Go erwartet null statt undefined für optionale Pointer-Parameter
const newEntry = await AddPlanEntry(
weekPlan.id,
day,
meal,
productId ?? null,
customText ?? null,
groupLabel ?? null
);
// State aktualisieren
if (newEntry) {
setWeekPlan(prev => prev ? {
...prev,
entries: [...prev.entries, newEntry]
} : null);
}
return newEntry;
} catch (err) {
@@ -114,13 +136,15 @@ export function useWeekPlan(year: number, week: number) {
groupLabel?: GroupLabel
): Promise<PlanEntry | null> => {
try {
const updatedEntry = await UpdatePlanEntry(entryId, 0, '', 0, productId ?? null, customText ?? null, groupLabel ?? null);
// Go: UpdatePlanEntry(id int, productID *int, customText *string, groupLabel *string)
const updatedEntry = await UpdatePlanEntry(entryId, productId ?? null, customText ?? null, groupLabel ?? null);
// State aktualisieren
if (updatedEntry) {
setWeekPlan(prev => prev ? {
...prev,
entries: prev.entries.map(e => e.id === entryId ? updatedEntry : e)
} : null);
}
return updatedEntry;
} catch (err) {

View File

@@ -16,7 +16,7 @@ import (
const (
// Aktuelle Version der App
CurrentVersion = "1.0.0"
CurrentVersion = "0.4.5"
// Update-Check URL
UpdateURL = "https://speiseplan.supertoll.xyz/version.json"
)