Compare commits
6
Commits
944eed3eb3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ff605891c | ||
|
|
ea17db45a6 | ||
|
|
98486b9f01 | ||
|
|
00e5c68ea5 | ||
|
|
07f24e9b34 | ||
|
|
2e182d63a3 |
@@ -4,3 +4,4 @@ Dockerfile
|
|||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
.env
|
.env
|
||||||
*.exe
|
*.exe
|
||||||
|
config/
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
|
*.sh text eol=lf
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
agent: agent
|
||||||
|
description: Manage booked dates in the authenticated Date Wizard admin page, including newest-first display, activity and booking metadata, and confirmation-gated deletion.
|
||||||
|
---
|
||||||
|
|
||||||
|
Implement or review booked-date management for the Date Wizard application. Preserve the existing Go server, JSON booking persistence, authenticated admin behavior, German user-facing language, and visual conventions.
|
||||||
|
|
||||||
|
## Admin navigation
|
||||||
|
|
||||||
|
Add a separate booking-management view to the authenticated `/admin` page. Keep it between the existing `Termine` and `Aktivitäten` navigation items. The view is reached through a visible navigation button and must remain responsive on narrow screens.
|
||||||
|
|
||||||
|
The booking view is initially read-only except for deleting a booking. Do not add editing controls for the booking date, duration, name, activities, or booking timestamp.
|
||||||
|
|
||||||
|
## Booking list
|
||||||
|
|
||||||
|
Load bookings through an authenticated admin endpoint and display them newest first using the booking creation timestamp `bookedAt`.
|
||||||
|
|
||||||
|
Each booking entry must:
|
||||||
|
|
||||||
|
- Display the selected date as the primary information, formatted in German.
|
||||||
|
- Display every selected activity when one or more activities are present.
|
||||||
|
- Display each activity's stored icon and name, including the single-activity case and multiple-activity case.
|
||||||
|
- Display the booker's name only when a non-empty name is available.
|
||||||
|
- Display the date and time at which the booking was created from `bookedAt`.
|
||||||
|
- Display a useful empty state when no bookings exist.
|
||||||
|
- Render all values as text content rather than injecting untrusted booking data as HTML.
|
||||||
|
|
||||||
|
Keep legacy records with missing optional values displayable. Do not fabricate a booking timestamp when `bookedAt` is empty.
|
||||||
|
|
||||||
|
## Backend API
|
||||||
|
|
||||||
|
Use the existing authenticated Go HTTP API and booking storage helpers.
|
||||||
|
|
||||||
|
- Add or preserve an authenticated `GET /api/admin/bookings` endpoint.
|
||||||
|
- Return structured booking records containing `bookedAt`, `date`, `dateType`, `name`, and `activities`.
|
||||||
|
- Sort records newest first by `bookedAt`, while keeping records with missing timestamps valid.
|
||||||
|
- Do not expose the full booking records through the public `/api/booked-dates` endpoint; that endpoint may continue returning only date strings.
|
||||||
|
- Reject unauthenticated requests with the existing JSON authentication response.
|
||||||
|
- Return appropriate errors for malformed requests and storage failures.
|
||||||
|
|
||||||
|
## Booking deletion
|
||||||
|
|
||||||
|
Provide one `Termin löschen` button for every booking entry.
|
||||||
|
|
||||||
|
- Ask for explicit confirmation before sending a delete request.
|
||||||
|
- Include the formatted booking date in the confirmation question.
|
||||||
|
- Add an authenticated `DELETE /api/admin/bookings` endpoint accepting `{ "date": "YYYY-MM-DD" }`.
|
||||||
|
- Delete only the matching booking record from `config/dates_booked.json`.
|
||||||
|
- Return a not-found response when the requested booking does not exist.
|
||||||
|
- Keep the delete button disabled while the request is in progress.
|
||||||
|
- Reload the booking list after successful deletion.
|
||||||
|
- Remove the deleted date from the admin calendar's booked-date state and refresh the affected date displays.
|
||||||
|
- Keep confirmation and deletion errors visible to the administrator.
|
||||||
|
- Do not delete or modify any other booking record.
|
||||||
|
|
||||||
|
## Persistence and safety
|
||||||
|
|
||||||
|
Preserve the existing structured booking format and booking uniqueness rule: at most one booking record exists for a date. Use the existing booking mutex and atomic JSON write helper for deletion. Do not modify `config/appsettings.json` or expose the admin password.
|
||||||
|
|
||||||
|
## Implementation and validation
|
||||||
|
|
||||||
|
Before editing, inspect the existing admin HTML, JavaScript, CSS, Go handlers, booking model, storage helpers, public booked-date endpoint, and prompt conventions. Reuse existing request helpers and styling patterns.
|
||||||
|
|
||||||
|
After editing:
|
||||||
|
|
||||||
|
1. Run `gofmt` on changed Go files.
|
||||||
|
2. Run the available Go test or build command.
|
||||||
|
3. Run a JavaScript syntax check for changed scripts when Node.js is available.
|
||||||
|
4. Check diagnostics for changed files.
|
||||||
|
5. Verify unauthenticated access still shows the login view.
|
||||||
|
6. Verify the booking tab appears between `Termine` and `Aktivitäten`.
|
||||||
|
7. Verify newest-first ordering, date display, single and multiple activity display with icons, optional name display, and booking timestamp display.
|
||||||
|
8. Verify the delete confirmation prevents deletion when cancelled and deletes only the confirmed booking when accepted.
|
||||||
|
9. Verify the calendar booked state updates after deletion.
|
||||||
|
10. Do not alter `config/appsettings.json` or unrelated fixtures when documenting or committing this feature.
|
||||||
|
|
||||||
|
When reporting the result, summarize changed files, user-visible behavior, API behavior, persistence effects, and validation commands that passed or failed.
|
||||||
+2
-3
@@ -18,11 +18,10 @@ 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 start.sh /app/start.sh
|
COPY start.sh /app/start.sh
|
||||||
|
|
||||||
RUN chmod +x /app/start.sh
|
RUN mkdir -p /app/config && chmod +x /app/start.sh
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
ENTRYPOINT ["/app/start.sh"]
|
ENTRYPOINT ["/bin/sh", "/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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,190 +0,0 @@
|
|||||||
{
|
|
||||||
"activities": [
|
|
||||||
{
|
|
||||||
"name": "Picknick auf der Wiese",
|
|
||||||
"icon": "🧺",
|
|
||||||
"type": "short"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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": "Etwas eigenes",
|
|
||||||
"icon": "✏️",
|
|
||||||
"type": "custom"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"availableDates": [
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"names": [],
|
|
||||||
"adminPassword": "change-me"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Cocktailabend zu zweit",
|
||||||
|
"icon": "🍸",
|
||||||
|
"type": "long"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Massage zu Hause",
|
||||||
|
"icon": "💆",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ausgedehnte Wanderung",
|
||||||
|
"icon": "🌲",
|
||||||
|
"type": "long"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kinofilm gucken",
|
||||||
|
"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",
|
||||||
|
"disabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "Etwas eigenes",
|
||||||
|
"icon": "✏️",
|
||||||
|
"type": "custom"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"names": [],
|
||||||
|
"adminPassword": "change-me"
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"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-02",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-10-06",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-10-09",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-10-11",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-10-12",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-10-13",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-10-14",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-10-16",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-10-26",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-10-27",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-10-28",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-10-29",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-10-30",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-11-02",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-11-03",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-11-04",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-11-05",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-11-06",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-11-08",
|
||||||
|
"type": "long"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-11-09",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-11-10",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-11-11",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-11-12",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-11-17",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-11-18",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-11-23",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-11-24",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-11-25",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-11-26",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-11-27",
|
||||||
|
"type": "short"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-11-28",
|
||||||
|
"type": "long"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-11-29",
|
||||||
|
"type": "long"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-11-30",
|
||||||
|
"type": "short"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
+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)
|
||||||
|
|
||||||
@@ -85,6 +101,7 @@ func main() {
|
|||||||
http.HandleFunc("/api/admin/dates", handleAdminDates)
|
http.HandleFunc("/api/admin/dates", handleAdminDates)
|
||||||
http.HandleFunc("/api/admin/dates/bulk", handleAdminBulkDates)
|
http.HandleFunc("/api/admin/dates/bulk", handleAdminBulkDates)
|
||||||
http.HandleFunc("/api/admin/activities", handleAdminActivities)
|
http.HandleFunc("/api/admin/activities", handleAdminActivities)
|
||||||
|
http.HandleFunc("/api/admin/bookings", handleAdminBookings)
|
||||||
http.HandleFunc("/admin", handleAdminPage)
|
http.HandleFunc("/admin", handleAdminPage)
|
||||||
|
|
||||||
// Serve static files from webDir directory
|
// Serve static files from webDir directory
|
||||||
@@ -271,6 +288,65 @@ func handleAdminActivities(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleAdminBookings(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !isAdminRequest(r) {
|
||||||
|
writeJSON(w, http.StatusUnauthorized, Response{Success: false, Message: "Anmeldung erforderlich."})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
bookings, err := getBookings()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error reading bookings: %v", err)
|
||||||
|
http.Error(w, "Bookings could not be loaded", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sort.SliceStable(bookings, func(first, second int) bool {
|
||||||
|
return bookings[first].BookedAt > bookings[second].BookedAt
|
||||||
|
})
|
||||||
|
writeJSON(w, http.StatusOK, bookings)
|
||||||
|
case http.MethodDelete:
|
||||||
|
var request struct {
|
||||||
|
Date string `json:"date"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil || strings.TrimSpace(request.Date) == "" {
|
||||||
|
writeJSON(w, http.StatusBadRequest, Response{Success: false, Message: "Bitte gib ein Datum an."})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
request.Date = strings.TrimSpace(request.Date)
|
||||||
|
|
||||||
|
bookingsMu.Lock()
|
||||||
|
defer bookingsMu.Unlock()
|
||||||
|
bookings, err := readBookings()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error reading bookings: %v", err)
|
||||||
|
http.Error(w, "Bookings could not be loaded", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filtered := make([]Booking, 0, len(bookings))
|
||||||
|
deleted := false
|
||||||
|
for _, booking := range bookings {
|
||||||
|
if booking.Date == request.Date {
|
||||||
|
deleted = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, booking)
|
||||||
|
}
|
||||||
|
if !deleted {
|
||||||
|
writeJSON(w, http.StatusNotFound, Response{Success: false, Message: "Gebuchter Termin wurde nicht gefunden."})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := writeJSONFile(getBookedDatesFilePath(), filtered); err != nil {
|
||||||
|
log.Printf("Error deleting booking: %v", err)
|
||||||
|
http.Error(w, "Booking could not be deleted", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, Response{Success: true, Message: "Gebuchter Termin gelöscht."})
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func handleAdminLogin(w http.ResponseWriter, r *http.Request) {
|
func handleAdminLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
@@ -486,11 +562,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 +580,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 +631,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 +648,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 +686,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 +785,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 +826,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 {
|
||||||
|
|||||||
@@ -262,6 +262,68 @@
|
|||||||
color: var(--color-text-light);
|
color: var(--color-text-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bookings-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
margin: 18px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-row {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 14px 0;
|
||||||
|
border-bottom: 1px solid #e5d8ce;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-date {
|
||||||
|
font-family: var(--font-heading);
|
||||||
|
font-size: 1.08rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-activities {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px 12px;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-activity {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.booking-metadata {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px 14px;
|
||||||
|
color: var(--color-text-light);
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-booking-btn {
|
||||||
|
justify-self: start;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid #c84a62;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: transparent;
|
||||||
|
color: #a83049;
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-booking-btn:hover,
|
||||||
|
.delete-booking-btn:focus-visible {
|
||||||
|
background: #fff0f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-booking-btn:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-calendar {
|
.admin-calendar {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
|
|
||||||
<nav class="admin-nav" aria-label="Verwaltung">
|
<nav class="admin-nav" aria-label="Verwaltung">
|
||||||
<button class="admin-nav-btn is-active" type="button" data-view="datesView">Termine</button>
|
<button class="admin-nav-btn is-active" type="button" data-view="datesView">Termine</button>
|
||||||
|
<button class="admin-nav-btn" type="button" data-view="bookingsView">Gebuchte Termine</button>
|
||||||
<button class="admin-nav-btn" type="button" data-view="activitiesView">Aktivitäten</button>
|
<button class="admin-nav-btn" type="button" data-view="activitiesView">Aktivitäten</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -73,6 +74,17 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="bookingsView" class="admin-panel hidden">
|
||||||
|
<section class="admin-section" aria-labelledby="bookingsTitle">
|
||||||
|
<div class="section-heading">
|
||||||
|
<h2 id="bookingsTitle">Gebuchte Termine</h2>
|
||||||
|
<p>Neueste Buchungen stehen oben.</p>
|
||||||
|
</div>
|
||||||
|
<ul id="bookingsList" class="bookings-list" aria-live="polite"></ul>
|
||||||
|
<p id="emptyBookings" class="empty-dates hidden">Noch keine Termine gebucht.</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="activitiesView" class="admin-panel hidden">
|
<div id="activitiesView" class="admin-panel hidden">
|
||||||
<section class="admin-section" aria-labelledby="activitiesTitle">
|
<section class="admin-section" aria-labelledby="activitiesTitle">
|
||||||
<div class="section-heading">
|
<div class="section-heading">
|
||||||
|
|||||||
+74
-1
@@ -19,6 +19,8 @@
|
|||||||
const emptyActivities = document.getElementById("emptyActivities");
|
const emptyActivities = document.getElementById("emptyActivities");
|
||||||
const activityMessage = document.getElementById("activityMessage");
|
const activityMessage = document.getElementById("activityMessage");
|
||||||
const activityAddRow = document.getElementById("activityAddRow");
|
const activityAddRow = document.getElementById("activityAddRow");
|
||||||
|
const bookingsList = document.getElementById("bookingsList");
|
||||||
|
const emptyBookings = document.getElementById("emptyBookings");
|
||||||
const newActivityIcon = document.getElementById("newActivityIcon");
|
const newActivityIcon = document.getElementById("newActivityIcon");
|
||||||
const newActivityName = document.getElementById("newActivityName");
|
const newActivityName = document.getElementById("newActivityName");
|
||||||
const newActivityType = document.getElementById("newActivityType");
|
const newActivityType = document.getElementById("newActivityType");
|
||||||
@@ -33,6 +35,7 @@
|
|||||||
let viewYear = today.getFullYear();
|
let viewYear = today.getFullYear();
|
||||||
let viewMonth = today.getMonth();
|
let viewMonth = today.getMonth();
|
||||||
let activities = [];
|
let activities = [];
|
||||||
|
let bookings = [];
|
||||||
|
|
||||||
function setMessage(element, message, success) {
|
function setMessage(element, message, success) {
|
||||||
element.textContent = message;
|
element.textContent = message;
|
||||||
@@ -149,6 +152,76 @@
|
|||||||
prevMonthBtn.disabled = viewYear === today.getFullYear() && viewMonth === today.getMonth();
|
prevMonthBtn.disabled = viewYear === today.getFullYear() && viewMonth === today.getMonth();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderBookings() {
|
||||||
|
bookingsList.innerHTML = "";
|
||||||
|
emptyBookings.classList.toggle("hidden", bookings.length > 0);
|
||||||
|
bookings.forEach((booking) => {
|
||||||
|
const row = document.createElement("li");
|
||||||
|
row.className = "booking-row";
|
||||||
|
|
||||||
|
const date = document.createElement("strong");
|
||||||
|
date.className = "booking-date";
|
||||||
|
date.textContent = formatDate(booking.date);
|
||||||
|
row.appendChild(date);
|
||||||
|
|
||||||
|
if (booking.activities && booking.activities.length > 0) {
|
||||||
|
const activityList = document.createElement("div");
|
||||||
|
activityList.className = "booking-activities";
|
||||||
|
booking.activities.forEach((activity) => {
|
||||||
|
const activityItem = document.createElement("span");
|
||||||
|
activityItem.className = "booking-activity";
|
||||||
|
activityItem.textContent = `${activity.icon || ""} ${activity.name}`.trim();
|
||||||
|
activityList.appendChild(activityItem);
|
||||||
|
});
|
||||||
|
row.appendChild(activityList);
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = document.createElement("div");
|
||||||
|
metadata.className = "booking-metadata";
|
||||||
|
if (booking.name && booking.name.trim()) {
|
||||||
|
const name = document.createElement("span");
|
||||||
|
name.textContent = booking.name;
|
||||||
|
metadata.appendChild(name);
|
||||||
|
}
|
||||||
|
if (booking.bookedAt) {
|
||||||
|
const created = document.createElement("time");
|
||||||
|
created.dateTime = booking.bookedAt;
|
||||||
|
created.textContent = `Gebucht am ${new Date(booking.bookedAt).toLocaleString("de-DE", { dateStyle: "medium", timeStyle: "short" })}`;
|
||||||
|
metadata.appendChild(created);
|
||||||
|
}
|
||||||
|
if (metadata.childElementCount > 0) row.appendChild(metadata);
|
||||||
|
|
||||||
|
const deleteButton = document.createElement("button");
|
||||||
|
deleteButton.className = "delete-booking-btn";
|
||||||
|
deleteButton.type = "button";
|
||||||
|
deleteButton.textContent = "Termin löschen";
|
||||||
|
deleteButton.addEventListener("click", () => deleteBooking(booking, deleteButton));
|
||||||
|
row.appendChild(deleteButton);
|
||||||
|
bookingsList.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteBooking(booking, deleteButton) {
|
||||||
|
if (!window.confirm(`Den gebuchten Termin am ${formatDate(booking.date)} wirklich löschen?`)) return;
|
||||||
|
deleteButton.disabled = true;
|
||||||
|
try {
|
||||||
|
await request("/api/admin/bookings", { method: "DELETE", body: JSON.stringify({ date: booking.date }) });
|
||||||
|
bookedDates.delete(booking.date);
|
||||||
|
await loadBookings();
|
||||||
|
renderCalendar();
|
||||||
|
renderDates();
|
||||||
|
} catch (error) {
|
||||||
|
deleteButton.disabled = false;
|
||||||
|
if (error.status === 401) showLogin("Deine Anmeldung ist abgelaufen.");
|
||||||
|
else window.alert(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBookings() {
|
||||||
|
bookings = await request("/api/admin/bookings", { method: "GET" });
|
||||||
|
renderBookings();
|
||||||
|
}
|
||||||
|
|
||||||
async function loadDates() {
|
async function loadDates() {
|
||||||
try {
|
try {
|
||||||
const [loadedDates, loadedBookedDates] = await Promise.all([
|
const [loadedDates, loadedBookedDates] = await Promise.all([
|
||||||
@@ -161,7 +234,7 @@
|
|||||||
draftDates = new Map(dates.map((date) => [date.date, date.type]));
|
draftDates = new Map(dates.map((date) => [date.date, date.type]));
|
||||||
loginView.classList.add("hidden");
|
loginView.classList.add("hidden");
|
||||||
adminView.classList.remove("hidden");
|
adminView.classList.remove("hidden");
|
||||||
await loadActivities();
|
await Promise.all([loadActivities(), loadBookings()]);
|
||||||
renderCalendar();
|
renderCalendar();
|
||||||
renderDates();
|
renderDates();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="summary-item">
|
<div id="summaryNameItem" class="summary-item">
|
||||||
<span class="summary-icon">👤</span>
|
<span class="summary-icon">👤</span>
|
||||||
<div class="summary-details">
|
<div class="summary-details">
|
||||||
<span class="summary-label">Name</span>
|
<span class="summary-label">Name</span>
|
||||||
|
|||||||
+6
-1
@@ -7,6 +7,7 @@
|
|||||||
const summaryActivity = document.getElementById("summaryActivity");
|
const summaryActivity = document.getElementById("summaryActivity");
|
||||||
const summaryDate = document.getElementById("summaryDate");
|
const summaryDate = document.getElementById("summaryDate");
|
||||||
const summaryName = document.getElementById("summaryName");
|
const summaryName = document.getElementById("summaryName");
|
||||||
|
const summaryNameItem = document.getElementById("summaryNameItem");
|
||||||
const activityIcon = document.getElementById("activityIcon");
|
const activityIcon = document.getElementById("activityIcon");
|
||||||
const checkoutBtn = document.getElementById("checkoutBtn");
|
const checkoutBtn = document.getElementById("checkoutBtn");
|
||||||
|
|
||||||
@@ -19,13 +20,17 @@
|
|||||||
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;
|
||||||
|
if (selectedName && selectedName.trim()) {
|
||||||
summaryName.textContent = selectedName;
|
summaryName.textContent = selectedName;
|
||||||
|
} else {
|
||||||
|
summaryNameItem.classList.add("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
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,23 @@
|
|||||||
#!/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")"
|
echo "Missing mounted config file: $settings_file" >&2
|
||||||
cp /app/appsettings.default.json "$settings_file"
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
activities_file="$config_dir/activities.json"
|
||||||
|
available_dates_file="$config_dir/dates_available.json"
|
||||||
|
booked_dates_file="$config_dir/dates_booked.json"
|
||||||
|
|
||||||
|
for config_file in "$activities_file" "$available_dates_file" "$booked_dates_file"; do
|
||||||
|
if [ ! -f "$config_file" ]; then
|
||||||
|
echo "Missing mounted config file: $config_file" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
exec /app/server
|
exec /app/server
|
||||||
Reference in New Issue
Block a user