Move runtime data into config directory
This commit is contained in:
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
- 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`)"
|
||||||
|
|||||||
@@ -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 readBookings()
|
||||||
|
}
|
||||||
|
|
||||||
|
func readBookings() ([]Booking, error) {
|
||||||
|
filePath := getBookedDatesFilePath()
|
||||||
|
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 nil, err
|
||||||
|
}
|
||||||
|
var bookings []Booking
|
||||||
|
if err := json.Unmarshal(data, &bookings); err == nil {
|
||||||
|
return bookings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
dates, err := getBookedDates()
|
bookingsMu.Lock()
|
||||||
|
defer bookingsMu.Unlock()
|
||||||
|
bookings, err := readBookings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dates = []string{}
|
bookings = []Booking{}
|
||||||
}
|
}
|
||||||
|
for _, booking := range bookings {
|
||||||
for _, d := range dates {
|
if booking.Date == date {
|
||||||
if d == trimmedDate {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dates = append(dates, trimmedDate)
|
bookingActivities := make([]BookingActivity, 0, len(payload.Activities))
|
||||||
|
for _, activity := range payload.Activities {
|
||||||
filePath := getBookedDatesFilePath()
|
bookingActivities = append(bookingActivities, BookingActivity{
|
||||||
data, err := json.MarshalIndent(dates, "", " ")
|
Name: activity.Name,
|
||||||
if err != nil {
|
Icon: activity.Icon,
|
||||||
return err
|
Type: activity.Type,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(filePath, data, 0644)
|
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
@@ -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
@@ -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";
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user