Move runtime data into config directory

This commit is contained in:
troogs
2026-10-01 18:20:05 +02:00
parent 944eed3eb3
commit 2e182d63a3
13 changed files with 509 additions and 79 deletions
-1
View File
@@ -1,7 +1,6 @@
# Server Port & Configuration # Server Port & Configuration
PORT=8080 PORT=8080
WEB_DIR=public WEB_DIR=public
BOOKED_DATES_FILE=booked_dates.json
# SMTP Configuration # SMTP Configuration
SMTP_HOST=mail.example.com SMTP_HOST=mail.example.com
@@ -0,0 +1,75 @@
---
agent: agent
description: Split Date Wizard JSON persistence into a fixed config directory, store complete structured bookings, and skip the name step when no names are configured.
---
Implement or review the Date Wizard configuration and booking persistence layout. Preserve the existing Go server, authenticated admin behavior, public booking flow, German user-facing language, and Docker deployment conventions.
## Fixed config directory
All JSON data files belong under one project config directory:
- `config/appsettings.json`: admin password and optional configured names.
- `config/activities.json`: activity definitions.
- `config/dates_available.json`: available date definitions.
- `config/dates_booked.json`: structured booking records.
The application must use these fixed paths and must not require one environment variable per JSON file. Do not add or retain `ACTIVITIES_FILE`, `AVAILABLE_DATES_FILE`, or `BOOKED_DATES_FILE` configuration requirements. The runtime paths should resolve to `/app/config/...` in the container and `config/...` during local execution.
Keep legacy read compatibility where practical for existing deployments, but new writes must use the new fixed files and names. The old root-level JSON files should not remain as the active project data source.
## Docker Compose deployment
The Compose deployment must expose one persistent volume only:
```yaml
volumes:
- /docker/data/date:/app/config
```
The container must not require individual file mounts or data-file environment variables. Startup must provision missing files from image defaults when the mounted config directory is empty. Preserve the existing `.env` usage for server and SMTP settings.
The Dockerfile must include the config templates in the image, and the startup script must initialize the four files beneath `/app/config` without overwriting existing host data.
Document the host initialization and migration process, including migration from the old `booked_dates.json` format.
## Structured booking records
When a booking is submitted, record one object per booked date in `config/dates_booked.json` with:
- `bookedAt`: UTC booking timestamp in a machine-readable format.
- `date`: the selected date.
- `dateType`: the selected date duration/type.
- `name`: the selected person name, or empty when the name step was skipped.
- `activities`: every selected activity with its `name`, `icon`, and `type`.
Do not persist `customDescription` in the booked activity records. Custom descriptions may remain transient in the submission payload and may still be used for the notification email and generated iCalendar content.
Avoid duplicate booking records for the same date. Existing date-only booked-date consumers may continue receiving a list of date strings through the public API. Preserve booked-date protection in the admin date editor.
## Name page shortcut
When `appsettings.json` contains an empty `names` array:
- Skip the name-selection page entirely after activity selection.
- Store an empty selected name and navigate directly to checkout.
- Checkout must accept an empty name and display a clear fallback such as `Nicht angegeben`.
When names are configured, preserve the existing name-selection page and selection behavior.
## Validation
Before editing, inspect the current settings model, file paths, booking submission payload, name-page navigation, Dockerfile, Compose file, startup script, and existing prompt conventions.
After editing:
1. Run `gofmt` on changed Go files.
2. Run the available Go test/build command.
3. Run JavaScript syntax checks for changed frontend scripts.
4. Validate all config JSON files.
5. Check diagnostics and whitespace errors.
6. Smoke-test the public settings and booked-date APIs with the fixed local config paths.
7. Verify that Compose has one config-directory volume and no per-file data environment variables.
8. Do not overwrite user-edited config data or unrelated fixtures.
When reporting the result, summarize the fixed paths, Docker contract, booking JSON shape, name-page behavior, migration compatibility, and validation results.
+1 -1
View File
@@ -18,7 +18,7 @@ WORKDIR /app
# Copy binary and static front-end assets # Copy binary and static front-end assets
COPY --from=builder /app/server /app/server COPY --from=builder /app/server /app/server
COPY --from=builder /app/public /app/public COPY --from=builder /app/public /app/public
COPY --from=builder /app/appsettings.json /app/appsettings.default.json COPY --from=builder /app/config /app/config.default
COPY start.sh /app/start.sh COPY start.sh /app/start.sh
RUN chmod +x /app/start.sh RUN chmod +x /app/start.sh
+22 -8
View File
@@ -8,8 +8,12 @@ Eine spielerische Web-App ("Choose Your Own Adventure") zum Planen gemeinsamer D
```text ```text
datetest/ datetest/
├── config/ # Persistente JSON-Konfiguration und Buchungen
│ ├── appsettings.json # Admin-Passwort und optionale Namen
│ ├── activities.json # Aktivitäten
│ ├── dates_available.json # Verfügbare Termine
│ └── dates_booked.json # Strukturierte Buchungen
├── public/ # Statische Webseiten & Assets ├── public/ # Statische Webseiten & Assets
│ ├── appsettings.json # Konfiguration der Aktivitäten & Termine
│ ├── index.html # Startseite ("Willst du mit mir gehen?") │ ├── index.html # Startseite ("Willst du mit mir gehen?")
│ ├── datetime.html/.js # Datumsauswahl (Kalender) │ ├── datetime.html/.js # Datumsauswahl (Kalender)
│ ├── activity.html/.js # Aktivitätsauswahl │ ├── activity.html/.js # Aktivitätsauswahl
@@ -41,7 +45,6 @@ Trage in der `.env` deine Server- und SMTP-Zugangsdaten ein:
| :--- | :--- | :--- | | :--- | :--- | :--- |
| `PORT` | HTTP-Port des Go-Servers | `8080` | | `PORT` | HTTP-Port des Go-Servers | `8080` |
| `WEB_DIR` | Verzeichnis der statischen Assets | `public` | | `WEB_DIR` | Verzeichnis der statischen Assets | `public` |
| `BOOKED_DATES_FILE` | Speicherpfad für bereits gebuchte Termine | `booked_dates.json` |
| `SMTP_HOST` | Postausgangsserver (SMTP) | `mail.example.com` | | `SMTP_HOST` | Postausgangsserver (SMTP) | `mail.example.com` |
| `SMTP_PORT` | SMTP-Port (Standard meist 587 oder 465) | `587` | | `SMTP_PORT` | SMTP-Port (Standard meist 587 oder 465) | `587` |
| `SMTP_USER` | Benutzername / E-Mail für SMTP-Auth | `absender@example.com` | | `SMTP_USER` | Benutzername / E-Mail für SMTP-Auth | `absender@example.com` |
@@ -51,18 +54,29 @@ Trage in der `.env` deine Server- und SMTP-Zugangsdaten ein:
*(Hinweis: Wenn `SMTP_HOST` oder `TO_EMAIL` leer bleiben, läuft der Server im Testmodus ohne E-Mail-Versand.)* *(Hinweis: Wenn `SMTP_HOST` oder `TO_EMAIL` leer bleiben, läuft der Server im Testmodus ohne E-Mail-Versand.)*
### 2. Aktivitäten & Termine anpassen (`appsettings.json`) ### 2. Einstellungen und Daten
Passe in `appsettings.json` die zur Auswahl stehenden Aktivitäten, verfügbaren Tage und das Admin-Passwort an: Die persistenten Daten liegen in getrennten JSON-Dateien:
- **`adminPassword`**: Passwort für die Terminverwaltung unter `/admin`. Ändere den Standardwert `change-me`, bevor du die Anwendung veröffentlichst. - **`adminPassword`**: Passwort für die Terminverwaltung unter `/admin`. Ändere den Standardwert `change-me`, bevor du die Anwendung veröffentlichst.
- **`activities`**: Liste von Objekten mit `name`, `icon` (Unicode Emoji), `type` (`short` oder `long`) und optional `disabled` (`true` blendet die Aktivität vorübergehend aus). - **`activities.json`**: Liste von Objekten mit `name`, `icon` (Unicode Emoji), `type` (`short`, `long` oder `custom`) und optional `disabled` (`true` blendet die Aktivität vorübergehend aus).
- **`availableDates`**: Liste von verfügbaren Tagen mit `date` (`YYYY-MM-DD`) und `type` (`short` oder `long`). - **`dates_available.json`**: Liste von verfügbaren Tagen mit `date` (`YYYY-MM-DD`) und `type` (`short` oder `long`).
- **`names`**: Optional vordefinierte Namen zur Auswahl (ist die Liste leer, wird ein Standard-Fallback angeboten). - **`dates_booked.json`**: Strukturierte Buchungen mit `bookedAt`, `date`, `dateType`, `name` und den ausgewählten Aktivitäten inklusive Name, Typ und Icon.
- **`names`**: Optional vordefinierte Namen zur Auswahl (ist die Liste leer, wird die Namensseite übersprungen).
Die Termin- und Aktivitätenlisten lassen sich über die passwortgeschützte Verwaltung unter `http://localhost:8080/admin` bearbeiten. Das Passwort bleibt dabei auf dem Server und wird nicht an Besucher ausgeliefert. Die Termin- und Aktivitätenlisten lassen sich über die passwortgeschützte Verwaltung unter `http://localhost:8080/admin` bearbeiten. Das Passwort bleibt dabei auf dem Server und wird nicht an Besucher ausgeliefert.
Bei Docker Compose werden `appsettings.json` und `booked_dates.json` dauerhaft unter `/docker/data/date` gespeichert. Beim ersten Start legt der Container die Einstellungen dort aus der im Image enthaltenen Vorlage an. Ändere danach den Standardwert `change-me` in `/docker/data/date/appsettings.json` oder übernimm vor dem ersten Start eine vorbereitete Konfigurationsdatei. Bei Docker Compose wird `/docker/data/date` als ein Volume nach `/app/config` eingebunden. Die vier JSON-Dateien müssen dort vor dem ersten Start vorhanden sein. Du kannst sie aus dem Projektordner initialisieren:
```bash
mkdir -p /docker/data/date
cp config/appsettings.json /docker/data/date/appsettings.json
cp config/activities.json /docker/data/date/activities.json
cp config/dates_available.json /docker/data/date/dates_available.json
cp config/dates_booked.json /docker/data/date/dates_booked.json
```
Wenn bisher `/docker/data/date/booked_dates.json` verwendet wurde, übernimm die alten Buchungsdaten zuerst in `dates_booked.json`. Das neue Format speichert Buchungen als Objekte statt als reine Datumsstrings.
--- ---
+92
View File
@@ -0,0 +1,92 @@
[
{
"name": "Cocktailabend zu zweit",
"icon": "🚲",
"type": "long"
},
{
"name": "Massage zu Hause",
"icon": "💆",
"type": "short"
},
{
"name": "Ausgedehnte Wanderung",
"icon": "🛋️",
"type": "long"
},
{
"name": "Kinofilm",
"icon": "🎬",
"type": "short"
},
{
"name": "Sushi selber machen",
"icon": "🍣",
"type": "short"
},
{
"name": "Untersetzer aus Ton basteln",
"icon": "🪨",
"type": "long"
},
{
"name": "Freizeitpark",
"icon": "🎢",
"type": "long"
},
{
"name": "Spaziergang im Wald",
"icon": "🌲",
"type": "short"
},
{
"name": "Kirschbier Tasting",
"icon": "🍒",
"type": "short"
},
{
"name": "Nudeln selber machen",
"icon": "🍜",
"type": "long"
},
{
"name": "Chillen im Freibad",
"icon": "🏊",
"type": "short"
},
{
"name": "Film zu Hause + Essen bestellen",
"icon": "🛋️",
"type": "long"
},
{
"name": "Squash spielen",
"icon": "🏸",
"type": "short"
},
{
"name": "Essen im Namaste",
"icon": "🍛",
"type": "short"
},
{
"name": "Museum für Gegenwartskunst",
"icon": "🖼️",
"type": "short"
},
{
"name": "Playstation Co-Op",
"icon": "🎮",
"type": "short"
},
{
"name": "l",
"icon": "✏️",
"type": "custom"
},
{
"name": "asdss",
"icon": "📚",
"type": "short"
}
]
+4
View File
@@ -0,0 +1,4 @@
{
"names": [],
"adminPassword": "change-me"
}
+114
View File
@@ -0,0 +1,114 @@
[
{
"date": "2026-08-18",
"type": "short"
},
{
"date": "2026-08-20",
"type": "short"
},
{
"date": "2026-08-22",
"type": "long"
},
{
"date": "2026-08-23",
"type": "long"
},
{
"date": "2026-08-24",
"type": "short"
},
{
"date": "2026-08-26",
"type": "short"
},
{
"date": "2026-08-27",
"type": "short"
},
{
"date": "2026-08-28",
"type": "short"
},
{
"date": "2026-08-29",
"type": "long"
},
{
"date": "2026-08-31",
"type": "short"
},
{
"date": "2026-09-01",
"type": "short"
},
{
"date": "2026-09-07",
"type": "short"
},
{
"date": "2026-09-08",
"type": "short"
},
{
"date": "2026-09-14",
"type": "short"
},
{
"date": "2026-09-15",
"type": "short"
},
{
"date": "2026-09-17",
"type": "short"
},
{
"date": "2026-09-18",
"type": "short"
},
{
"date": "2026-09-25",
"type": "short"
},
{
"date": "2026-09-26",
"type": "long"
},
{
"date": "2026-09-27",
"type": "short"
},
{
"date": "2026-09-28",
"type": "short"
},
{
"date": "2026-09-29",
"type": "short"
},
{
"date": "2026-09-30",
"type": "short"
},
{
"date": "2026-10-03",
"type": "long"
},
{
"date": "2026-10-04",
"type": "long"
},
{
"date": "2026-10-10",
"type": "long"
},
{
"date": "2026-10-21",
"type": "short"
},
{
"date": "2026-10-22",
"type": "long"
}
]
+27
View File
@@ -0,0 +1,27 @@
[
{
"bookedAt": "",
"date": "2026-10-10",
"dateType": "long",
"name": "",
"activities": []
},
{
"bookedAt": "2026-10-01T16:10:50Z",
"date": "2026-10-22",
"dateType": "long",
"name": "Du weißt, wer ich bin",
"activities": [
{
"name": "Chillen im Freibad",
"icon": "🏊",
"type": "short"
},
{
"name": "Sushi selber machen",
"icon": "🍣",
"type": "short"
}
]
}
]
+1 -4
View File
@@ -9,11 +9,8 @@ services:
- proxy - proxy
env_file: env_file:
- .env - .env
environment:
SETTINGS_FILE: /data/appsettings.json
BOOKED_DATES_FILE: /data/booked_dates.json
volumes: volumes:
- /docker/data/date:/data - /docker/data/date:/app/config
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.date-wizard.rule=Host(`date.allen.beging.de`) || Host(`date.beging.de`)" - "traefik.http.routers.date-wizard.rule=Host(`date.allen.beging.de`) || Host(`date.beging.de`)"
+155 -62
View File
@@ -11,6 +11,7 @@ import (
"net/http" "net/http"
"net/smtp" "net/smtp"
"os" "os"
"path/filepath"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@@ -44,10 +45,24 @@ type AvailableDate struct {
} }
type AppSettings struct { type AppSettings struct {
Activities []Activity `json:"activities"`
AvailableDates []AvailableDate `json:"availableDates"`
Names []string `json:"names"` Names []string `json:"names"`
AdminPassword string `json:"adminPassword"` AdminPassword string `json:"adminPassword"`
Activities []Activity `json:"-"`
AvailableDates []AvailableDate `json:"-"`
}
type Booking struct {
BookedAt string `json:"bookedAt"`
Date string `json:"date"`
DateType string `json:"dateType"`
Name string `json:"name"`
Activities []BookingActivity `json:"activities"`
}
type BookingActivity struct {
Name string `json:"name"`
Icon string `json:"icon"`
Type string `json:"type"`
} }
type PublicSettings struct { type PublicSettings struct {
@@ -61,6 +76,7 @@ type passwordRequest struct {
} }
var settingsMu sync.Mutex var settingsMu sync.Mutex
var bookingsMu sync.Mutex
var sessionsMu sync.Mutex var sessionsMu sync.Mutex
var adminSessions = make(map[string]time.Time) var adminSessions = make(map[string]time.Time)
@@ -486,11 +502,7 @@ func handleAdminBulkDates(w http.ResponseWriter, r *http.Request) {
} }
func getSettingsFilePath() string { func getSettingsFilePath() string {
path := os.Getenv("SETTINGS_FILE") return filepath.Join("config", "appsettings.json")
if path == "" {
path = "appsettings.json"
}
return path
} }
func readSettings() (AppSettings, error) { func readSettings() (AppSettings, error) {
@@ -508,9 +520,42 @@ func readSettingsFile() (AppSettings, error) {
if err := json.Unmarshal(data, &settings); err != nil { if err := json.Unmarshal(data, &settings); err != nil {
return settings, err return settings, err
} }
var legacy struct {
Activities []Activity `json:"activities"`
AvailableDates []AvailableDate `json:"availableDates"`
}
if err := json.Unmarshal(data, &legacy); err != nil {
return settings, err
}
settings.Activities = legacy.Activities
settings.AvailableDates = legacy.AvailableDates
if activities, found, err := readOptionalJSON(getActivitiesFilePath(), &[]Activity{}); err != nil {
return settings, err
} else if found {
settings.Activities = *activities.(*[]Activity)
}
if dates, found, err := readOptionalJSON(getAvailableDatesFilePath(), &[]AvailableDate{}); err != nil {
return settings, err
} else if found {
settings.AvailableDates = *dates.(*[]AvailableDate)
}
return settings, nil return settings, nil
} }
func readOptionalJSON(path string, target interface{}) (interface{}, bool, error) {
data, err := os.ReadFile(path)
if os.IsNotExist(err) {
return target, false, nil
}
if err != nil {
return target, false, err
}
if err := json.Unmarshal(data, target); err != nil {
return target, false, err
}
return target, true, nil
}
func updateAvailableDates(update func([]AvailableDate) ([]AvailableDate, error)) error { func updateAvailableDates(update func([]AvailableDate) ([]AvailableDate, error)) error {
settingsMu.Lock() settingsMu.Lock()
defer settingsMu.Unlock() defer settingsMu.Unlock()
@@ -526,15 +571,7 @@ func updateAvailableDates(update func([]AvailableDate) ([]AvailableDate, error))
sort.Slice(dates, func(i, j int) bool { return dates[i].Date < dates[j].Date }) sort.Slice(dates, func(i, j int) bool { return dates[i].Date < dates[j].Date })
settings.AvailableDates = dates settings.AvailableDates = dates
data, err := json.MarshalIndent(settings, "", " ") return writeJSONFile(getAvailableDatesFilePath(), dates)
if err != nil {
return err
}
temporaryPath := getSettingsFilePath() + ".tmp"
if err := os.WriteFile(temporaryPath, data, 0600); err != nil {
return err
}
return os.Rename(temporaryPath, getSettingsFilePath())
} }
func updateActivities(update func([]Activity) ([]Activity, error)) error { func updateActivities(update func([]Activity) ([]Activity, error)) error {
@@ -551,15 +588,27 @@ func updateActivities(update func([]Activity) ([]Activity, error)) error {
} }
settings.Activities = activities settings.Activities = activities
data, err := json.MarshalIndent(settings, "", " ") return writeJSONFile(getActivitiesFilePath(), activities)
}
func writeJSONFile(path string, value interface{}) error {
data, err := json.MarshalIndent(value, "", " ")
if err != nil { if err != nil {
return err return err
} }
temporaryPath := getSettingsFilePath() + ".tmp" temporaryPath := path + ".tmp"
if err := os.WriteFile(temporaryPath, data, 0600); err != nil { if err := os.WriteFile(temporaryPath, data, 0600); err != nil {
return err return err
} }
return os.Rename(temporaryPath, getSettingsFilePath()) return os.Rename(temporaryPath, path)
}
func getActivitiesFilePath() string {
return filepath.Join("config", "activities.json")
}
func getAvailableDatesFilePath() string {
return filepath.Join("config", "dates_available.json")
} }
func validateActivity(activity Activity) error { func validateActivity(activity Activity) error {
@@ -577,11 +626,22 @@ func validateActivity(activity Activity) error {
func enabledActivities(activities []Activity) []Activity { func enabledActivities(activities []Activity) []Activity {
enabled := make([]Activity, 0, len(activities)) enabled := make([]Activity, 0, len(activities))
var customActivity *Activity
for _, activity := range activities { for _, activity := range activities {
if activity.Type == "custom" {
if !activity.Disabled {
custom := activity
customActivity = &custom
}
continue
}
if !activity.Disabled { if !activity.Disabled {
enabled = append(enabled, activity) enabled = append(enabled, activity)
} }
} }
if customActivity != nil {
enabled = append(enabled, *customActivity)
}
return enabled return enabled
} }
@@ -665,7 +725,7 @@ func handleSubmit(w http.ResponseWriter, r *http.Request) {
log.Printf("Received survey submission from %s for date %s (%s)", payload.Name, payload.Date, payload.Activity) log.Printf("Received survey submission from %s for date %s (%s)", payload.Name, payload.Date, payload.Activity)
if err := recordBookedDate(payload.Date); err != nil { if err := recordBooking(payload); err != nil {
log.Printf("Error recording booked date: %v", err) log.Printf("Error recording booked date: %v", err)
} }
@@ -706,65 +766,98 @@ func handleBookedDates(w http.ResponseWriter, r *http.Request) {
} }
func getBookedDatesFilePath() string { func getBookedDatesFilePath() string {
path := os.Getenv("BOOKED_DATES_FILE") return filepath.Join("config", "dates_booked.json")
if path == "" {
path = "booked_dates.json"
}
return path
} }
func getBookedDates() ([]string, error) { func getBookedDates() ([]string, error) {
filePath := getBookedDatesFilePath() bookings, err := getBookings()
file, err := os.Open(filePath)
if err != nil { if err != nil {
if os.IsNotExist(err) {
return []string{}, nil
}
return nil, err return nil, err
} }
defer file.Close() dates := make([]string, 0, len(bookings))
for _, booking := range bookings {
var rawDates []string if trimmed := strings.TrimSpace(booking.Date); trimmed != "" {
if err := json.NewDecoder(file).Decode(&rawDates); err != nil {
return []string{}, nil
}
dates := make([]string, 0, len(rawDates))
for _, d := range rawDates {
trimmed := strings.TrimSpace(d)
if trimmed != "" {
dates = append(dates, trimmed) dates = append(dates, trimmed)
} }
} }
return dates, nil return dates, nil
} }
func recordBookedDate(dateStr string) error { func getBookings() ([]Booking, error) {
trimmedDate := strings.TrimSpace(dateStr) bookingsMu.Lock()
if trimmedDate == "" { defer bookingsMu.Unlock()
return nil return readBookings()
} }
dates, err := getBookedDates()
if err != nil {
dates = []string{}
}
for _, d := range dates {
if d == trimmedDate {
return nil
}
}
dates = append(dates, trimmedDate)
func readBookings() ([]Booking, error) {
filePath := getBookedDatesFilePath() filePath := getBookedDatesFilePath()
data, err := json.MarshalIndent(dates, "", " ") data, err := os.ReadFile(filePath)
if os.IsNotExist(err) {
legacyPath := os.Getenv("LEGACY_BOOKED_DATES_FILE")
if legacyPath == "" {
legacyPath = filepath.Join("config", "booked_dates.json")
}
data, err = os.ReadFile(legacyPath)
if os.IsNotExist(err) {
return []Booking{}, nil
}
}
if err != nil { if err != nil {
return err return nil, err
}
var bookings []Booking
if err := json.Unmarshal(data, &bookings); err == nil {
return bookings, nil
} }
return os.WriteFile(filePath, data, 0644) var legacyDates []string
if err := json.Unmarshal(data, &legacyDates); err != nil {
return nil, err
}
bookings = make([]Booking, 0, len(legacyDates))
for _, date := range legacyDates {
if trimmed := strings.TrimSpace(date); trimmed != "" {
bookings = append(bookings, Booking{Date: trimmed})
}
}
return bookings, nil
}
func recordBooking(payload SurveyPayload) error {
date := strings.TrimSpace(payload.Date)
if date == "" {
return nil
}
bookingsMu.Lock()
defer bookingsMu.Unlock()
bookings, err := readBookings()
if err != nil {
bookings = []Booking{}
}
for _, booking := range bookings {
if booking.Date == date {
return nil
}
}
bookingActivities := make([]BookingActivity, 0, len(payload.Activities))
for _, activity := range payload.Activities {
bookingActivities = append(bookingActivities, BookingActivity{
Name: activity.Name,
Icon: activity.Icon,
Type: activity.Type,
})
}
bookings = append(bookings, Booking{
BookedAt: time.Now().UTC().Format(time.RFC3339),
Date: date,
DateType: payload.DateType,
Name: payload.Name,
Activities: bookingActivities,
})
return writeJSONFile(getBookedDatesFilePath(), bookings)
} }
func enableCORS(w http.ResponseWriter, r *http.Request) bool { func enableCORS(w http.ResponseWriter, r *http.Request) bool {
+2 -2
View File
@@ -19,13 +19,13 @@
const selectedDate = localStorage.getItem("selectedDate"); const selectedDate = localStorage.getItem("selectedDate");
const selectedName = localStorage.getItem("selectedName"); const selectedName = localStorage.getItem("selectedName");
if (!selectedActivity || !selectedDate || !selectedName) { if (!selectedActivity || !selectedDate) {
window.location.href = "index.html"; window.location.href = "index.html";
return; return;
} }
summaryActivity.textContent = selectedActivity; summaryActivity.textContent = selectedActivity;
summaryName.textContent = selectedName; summaryName.textContent = selectedName || "Nicht angegeben";
const dateObj = new Date(`${selectedDate}T00:00:00`); const dateObj = new Date(`${selectedDate}T00:00:00`);
const formattedDate = dateObj.toLocaleDateString("de-DE", { const formattedDate = dateObj.toLocaleDateString("de-DE", {
+10 -4
View File
@@ -30,15 +30,21 @@
fetch("/api/settings", { cache: "no-store" }) fetch("/api/settings", { cache: "no-store" })
.then((response) => response.json()) .then((response) => response.json())
.then((settings) => renderNames(settings.names || [])) .then((settings) => {
const names = settings.names || [];
if (names.length === 0) {
localStorage.setItem("selectedName", "");
window.location.href = "checkout.html";
return;
}
renderNames(names);
})
.catch(() => { .catch(() => {
nameList.textContent = "Namen konnten nicht geladen werden."; nameList.textContent = "Namen konnten nicht geladen werden.";
}); });
function renderNames(names) { function renderNames(names) {
const options = names.length > 0 ? names : ["Du weißt, wer ich bin"]; names.forEach((name) => {
options.forEach((name) => {
const card = document.createElement("button"); const card = document.createElement("button");
card.type = "button"; card.type = "button";
card.className = "name-card"; card.className = "name-card";
+12 -3
View File
@@ -1,11 +1,20 @@
#!/bin/sh #!/bin/sh
set -eu set -eu
settings_file="${SETTINGS_FILE:-/app/appsettings.json}" config_dir="/app/config"
settings_file="$config_dir/appsettings.json"
if [ ! -f "$settings_file" ]; then if [ ! -f "$settings_file" ]; then
mkdir -p "$(dirname "$settings_file")" mkdir -p "$config_dir"
cp /app/appsettings.default.json "$settings_file" cp /app/config.default/appsettings.json "$settings_file"
fi fi
activities_file="$config_dir/activities.json"
available_dates_file="$config_dir/dates_available.json"
booked_dates_file="$config_dir/dates_booked.json"
if [ ! -f "$activities_file" ] && ! grep -q '"activities"' "$settings_file"; then cp /app/config.default/activities.json "$activities_file"; fi
if [ ! -f "$available_dates_file" ] && ! grep -q '"availableDates"' "$settings_file"; then cp /app/config.default/dates_available.json "$available_dates_file"; fi
if [ ! -f "$booked_dates_file" ] && [ ! -f "$config_dir/booked_dates.json" ]; then cp /app/config.default/dates_booked.json "$booked_dates_file"; fi
exec /app/server exec /app/server