Move runtime data into config directory
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
# Server Port & Configuration
|
||||
PORT=8080
|
||||
WEB_DIR=public
|
||||
BOOKED_DATES_FILE=booked_dates.json
|
||||
|
||||
# SMTP Configuration
|
||||
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
@@ -18,7 +18,7 @@ WORKDIR /app
|
||||
# Copy binary and static front-end assets
|
||||
COPY --from=builder /app/server /app/server
|
||||
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
|
||||
|
||||
RUN chmod +x /app/start.sh
|
||||
|
||||
@@ -8,8 +8,12 @@ Eine spielerische Web-App ("Choose Your Own Adventure") zum Planen gemeinsamer D
|
||||
|
||||
```text
|
||||
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
|
||||
│ ├── appsettings.json # Konfiguration der Aktivitäten & Termine
|
||||
│ ├── index.html # Startseite ("Willst du mit mir gehen?")
|
||||
│ ├── datetime.html/.js # Datumsauswahl (Kalender)
|
||||
│ ├── 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` |
|
||||
| `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_PORT` | SMTP-Port (Standard meist 587 oder 465) | `587` |
|
||||
| `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.)*
|
||||
|
||||
### 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.
|
||||
- **`activities`**: Liste von Objekten mit `name`, `icon` (Unicode Emoji), `type` (`short` oder `long`) 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`).
|
||||
- **`names`**: Optional vordefinierte Namen zur Auswahl (ist die Liste leer, wird ein Standard-Fallback angeboten).
|
||||
- **`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).
|
||||
- **`dates_available.json`**: Liste von verfügbaren Tagen mit `date` (`YYYY-MM-DD`) und `type` (`short` oder `long`).
|
||||
- **`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.
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"names": [],
|
||||
"adminPassword": "change-me"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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
@@ -9,11 +9,8 @@ services:
|
||||
- proxy
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
SETTINGS_FILE: /data/appsettings.json
|
||||
BOOKED_DATES_FILE: /data/booked_dates.json
|
||||
volumes:
|
||||
- /docker/data/date:/data
|
||||
- /docker/data/date:/app/config
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.date-wizard.rule=Host(`date.allen.beging.de`) || Host(`date.beging.de`)"
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"net/http"
|
||||
"net/smtp"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -44,10 +45,24 @@ type AvailableDate struct {
|
||||
}
|
||||
|
||||
type AppSettings struct {
|
||||
Activities []Activity `json:"activities"`
|
||||
AvailableDates []AvailableDate `json:"availableDates"`
|
||||
Names []string `json:"names"`
|
||||
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 {
|
||||
@@ -61,6 +76,7 @@ type passwordRequest struct {
|
||||
}
|
||||
|
||||
var settingsMu sync.Mutex
|
||||
var bookingsMu sync.Mutex
|
||||
var sessionsMu sync.Mutex
|
||||
var adminSessions = make(map[string]time.Time)
|
||||
|
||||
@@ -486,11 +502,7 @@ func handleAdminBulkDates(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func getSettingsFilePath() string {
|
||||
path := os.Getenv("SETTINGS_FILE")
|
||||
if path == "" {
|
||||
path = "appsettings.json"
|
||||
}
|
||||
return path
|
||||
return filepath.Join("config", "appsettings.json")
|
||||
}
|
||||
|
||||
func readSettings() (AppSettings, error) {
|
||||
@@ -508,9 +520,42 @@ func readSettingsFile() (AppSettings, error) {
|
||||
if err := json.Unmarshal(data, &settings); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
settingsMu.Lock()
|
||||
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 })
|
||||
settings.AvailableDates = dates
|
||||
|
||||
data, err := json.MarshalIndent(settings, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
temporaryPath := getSettingsFilePath() + ".tmp"
|
||||
if err := os.WriteFile(temporaryPath, data, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(temporaryPath, getSettingsFilePath())
|
||||
return writeJSONFile(getAvailableDatesFilePath(), dates)
|
||||
}
|
||||
|
||||
func updateActivities(update func([]Activity) ([]Activity, error)) error {
|
||||
@@ -551,15 +588,27 @@ func updateActivities(update func([]Activity) ([]Activity, error)) error {
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
temporaryPath := getSettingsFilePath() + ".tmp"
|
||||
temporaryPath := path + ".tmp"
|
||||
if err := os.WriteFile(temporaryPath, data, 0600); err != nil {
|
||||
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 {
|
||||
@@ -577,11 +626,22 @@ func validateActivity(activity Activity) error {
|
||||
|
||||
func enabledActivities(activities []Activity) []Activity {
|
||||
enabled := make([]Activity, 0, len(activities))
|
||||
var customActivity *Activity
|
||||
for _, activity := range activities {
|
||||
if activity.Type == "custom" {
|
||||
if !activity.Disabled {
|
||||
custom := activity
|
||||
customActivity = &custom
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !activity.Disabled {
|
||||
enabled = append(enabled, activity)
|
||||
}
|
||||
}
|
||||
if customActivity != nil {
|
||||
enabled = append(enabled, *customActivity)
|
||||
}
|
||||
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)
|
||||
|
||||
if err := recordBookedDate(payload.Date); err != nil {
|
||||
if err := recordBooking(payload); err != nil {
|
||||
log.Printf("Error recording booked date: %v", err)
|
||||
}
|
||||
|
||||
@@ -706,65 +766,98 @@ func handleBookedDates(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func getBookedDatesFilePath() string {
|
||||
path := os.Getenv("BOOKED_DATES_FILE")
|
||||
if path == "" {
|
||||
path = "booked_dates.json"
|
||||
}
|
||||
return path
|
||||
return filepath.Join("config", "dates_booked.json")
|
||||
}
|
||||
|
||||
func getBookedDates() ([]string, error) {
|
||||
filePath := getBookedDatesFilePath()
|
||||
file, err := os.Open(filePath)
|
||||
bookings, err := getBookings()
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []string{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var rawDates []string
|
||||
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 := make([]string, 0, len(bookings))
|
||||
for _, booking := range bookings {
|
||||
if trimmed := strings.TrimSpace(booking.Date); trimmed != "" {
|
||||
dates = append(dates, trimmed)
|
||||
}
|
||||
}
|
||||
return dates, nil
|
||||
}
|
||||
|
||||
func recordBookedDate(dateStr string) error {
|
||||
trimmedDate := strings.TrimSpace(dateStr)
|
||||
if trimmedDate == "" {
|
||||
return nil
|
||||
func getBookings() ([]Booking, error) {
|
||||
bookingsMu.Lock()
|
||||
defer bookingsMu.Unlock()
|
||||
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()
|
||||
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 {
|
||||
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 {
|
||||
|
||||
+2
-2
@@ -19,13 +19,13 @@
|
||||
const selectedDate = localStorage.getItem("selectedDate");
|
||||
const selectedName = localStorage.getItem("selectedName");
|
||||
|
||||
if (!selectedActivity || !selectedDate || !selectedName) {
|
||||
if (!selectedActivity || !selectedDate) {
|
||||
window.location.href = "index.html";
|
||||
return;
|
||||
}
|
||||
|
||||
summaryActivity.textContent = selectedActivity;
|
||||
summaryName.textContent = selectedName;
|
||||
summaryName.textContent = selectedName || "Nicht angegeben";
|
||||
|
||||
const dateObj = new Date(`${selectedDate}T00:00:00`);
|
||||
const formattedDate = dateObj.toLocaleDateString("de-DE", {
|
||||
|
||||
+10
-4
@@ -30,15 +30,21 @@
|
||||
|
||||
fetch("/api/settings", { cache: "no-store" })
|
||||
.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(() => {
|
||||
nameList.textContent = "Namen konnten nicht geladen werden.";
|
||||
});
|
||||
|
||||
function renderNames(names) {
|
||||
const options = names.length > 0 ? names : ["Du weißt, wer ich bin"];
|
||||
|
||||
options.forEach((name) => {
|
||||
names.forEach((name) => {
|
||||
const card = document.createElement("button");
|
||||
card.type = "button";
|
||||
card.className = "name-card";
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
settings_file="${SETTINGS_FILE:-/app/appsettings.json}"
|
||||
config_dir="/app/config"
|
||||
settings_file="$config_dir/appsettings.json"
|
||||
|
||||
if [ ! -f "$settings_file" ]; then
|
||||
mkdir -p "$(dirname "$settings_file")"
|
||||
cp /app/appsettings.default.json "$settings_file"
|
||||
mkdir -p "$config_dir"
|
||||
cp /app/config.default/appsettings.json "$settings_file"
|
||||
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
|
||||
Reference in New Issue
Block a user