Honeypot spam protection is overengineered for this project's scope. Removed the acceptance criteria from both stories and added addenda documenting the decision. Updated implementation order and review findings accordingly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
470 lines
41 KiB
Markdown
470 lines
41 KiB
Markdown
# User Stories
|
|
|
|
<!-- This file is managed by the Ralph Loop. Each iteration refines and adds user stories based on Ideen.md. -->
|
|
|
|
## Status
|
|
|
|
- Total stories: 20
|
|
- Complete: 0
|
|
- Remaining: 20
|
|
|
|
## Token Model
|
|
|
|
The following terms are used consistently across all stories:
|
|
|
|
- **Event token**: A public UUID embedded in the event URL. Used by guests to access the event page.
|
|
- **Organizer token**: A separate secret UUID stored in localStorage on the device where the event was created. Used to authenticate organizer actions.
|
|
- **Internal DB ID**: An implementation detail. Never exposed in stories, URLs, or to users.
|
|
|
|
## Stories
|
|
|
|
### US-1: Create an event
|
|
|
|
**As an** event organizer,
|
|
**I want to** create a new event with a title, description, date, time, location, and mandatory expiry date,
|
|
**so that** I can share it with others as a dedicated event page.
|
|
|
|
**Acceptance Criteria:**
|
|
- [ ] The organizer can fill in: title (required), description (optional), date and time (required), location (optional), expiry date (required)
|
|
- [ ] On submission, the server stores the event and returns both a unique, non-guessable event token (UUID) and a separate organizer token (UUID) in the creation response
|
|
- [ ] The organizer is redirected to the event page after creation
|
|
- [ ] The organizer token from the creation response is stored in localStorage to grant organizer access on this device
|
|
- [ ] The event token, title, and date are also stored in localStorage alongside the organizer token, so the local event overview (US-7) can display the event without additional server contact
|
|
- [ ] No account, login, or personal data is required to create an event
|
|
- [ ] The expiry date field is mandatory and cannot be left blank
|
|
- [ ] The event is not discoverable except via its direct link
|
|
|
|
**Dependencies:** T-4
|
|
|
|
**Notes:** Non-guessable tokens (UUIDs) are specified in Ideen.md under security. Expiry date is mandatory per Ideen.md. No registration required per core principles. Per Q-4 resolution: organizer authentication uses the organizer token stored in localStorage on the device where the event was created. The organizer token is separate from the event token — since the event link is designed to be shared in group chats, using the same token for both public access and organizer auth would allow any guest to manage the event.
|
|
|
|
**Addendum (2026-03-04):** Honeypot field removed — overengineered for this project's scope. Expiry date must be in the future at creation time — an event should never exist in an invalid state (resolved during US-1 research).
|
|
|
|
---
|
|
|
|
### US-2: View event landing page
|
|
|
|
**As a** guest,
|
|
**I want to** open a shared event link and see all event details,
|
|
**so that** I know what the event is, when and where it takes place, and who else is attending.
|
|
|
|
**Acceptance Criteria:**
|
|
- [ ] The event page displays: title, description (if provided), date and time, location (if provided)
|
|
- [ ] The page lists the names of all confirmed attendees (those who RSVPed "attending")
|
|
- [ ] The page shows a count of attendees
|
|
- [ ] If the event has expired (past its expiry date), the page renders a clear "this event has ended" state and no RSVP actions are shown
|
|
- [ ] If the event has been cancelled (US-18), the page displays a clear "cancelled" state with the cancellation message (if provided by the organizer), and no RSVP actions are shown [deferred until US-18 is implemented]
|
|
- [ ] If the event token does not match any event on the server (e.g. because it was deleted after expiry per US-12 [deferred until US-12 is implemented], or deleted by the organizer per US-19 [deferred until US-19 is implemented]), the page displays a clear "event not found" message — no partial data or error traces are shown
|
|
- [ ] The page is accessible without any login, account, or access code — only the event link is required
|
|
- [ ] No external resources (CDNs, fonts, tracking scripts) are loaded
|
|
|
|
**Dependencies:** US-1, T-4
|
|
|
|
**Notes:** Ideen.md describes "a kind of landing page for each event — what, when, where." The attendee list is needed here so guests can see who else confirmed. The expired-state requirement follows from the mandatory expiry date in US-1. The cancelled-state requirement follows from US-18: a cancelled event remains visible until its expiry date but clearly communicates its cancelled status. Per Q-3 resolution: the app is a SPA with a RESTful API backend; JavaScript-dependent rendering is acceptable. The "no external resources" criterion derives from the privacy statutes. The "event not found" criterion covers the edge case where a guest navigates to a previously valid event link after the server has deleted the event data (US-12).
|
|
|
|
---
|
|
|
|
### US-3: RSVP to an event
|
|
|
|
**As a** guest,
|
|
**I want to** indicate whether I will attend an event,
|
|
**so that** the organizer and other guests can see who is coming.
|
|
|
|
**Acceptance Criteria:**
|
|
- [ ] The guest can choose "I'm attending" or "I'm not attending"
|
|
- [ ] When selecting "I'm attending", a name is required
|
|
- [ ] When selecting "I'm not attending", providing a name is optional
|
|
- [ ] The RSVP is submitted to the server and persisted server-side
|
|
- [ ] The guest's RSVP choice and name are stored in localStorage to prevent accidental duplicate submissions from the same device
|
|
- [ ] The event token, title, and date are also stored in localStorage alongside the RSVP data, so the local event overview (US-7) can display the event and link to it without server contact
|
|
- [ ] If a prior RSVP exists in localStorage for this event, the form pre-fills with the previous choice and name
|
|
- [ ] Re-submitting from the same device updates the existing RSVP entry rather than creating a duplicate
|
|
- [ ] RSVP submission is not possible after the event's expiry date
|
|
- [ ] RSVP submission is not possible if the event has been cancelled (US-18) [deferred until US-18 is implemented]
|
|
- [ ] No account, login, or data beyond the optionally entered name is required
|
|
|
|
**Dependencies:** US-2, T-4
|
|
|
|
**Notes:** RSVP flow specified in Ideen.md: "Ich komme" (with name) / "Ich komme nicht" (optional with name). LocalStorage device binding is the explicit duplicate-prevention mechanism — not a hard guarantee, but sufficient against accidental duplicates. Ideen.md acknowledges that malicious spam without accounts is an acceptable risk.
|
|
|
|
**Addendum (2026-03-04):** Honeypot field removed — overengineered for this project's scope.
|
|
|
|
---
|
|
|
|
### US-4: Manage guest list as organizer
|
|
|
|
**As an** event organizer,
|
|
**I want to** view all RSVPs for my event and remove individual entries if needed,
|
|
**so that** I have an accurate overview of attendance and can moderate erroneous or spam entries.
|
|
|
|
**Acceptance Criteria:**
|
|
- [ ] An organizer view is accessible from the event page when a valid organizer token for that event is present in localStorage
|
|
- [ ] When no organizer token is present, no organizer-specific UI (link, button, or view) is shown to the visitor
|
|
- [ ] The organizer view lists all RSVPs, showing each entry's name and attending status
|
|
- [ ] The organizer can permanently delete any individual RSVP entry
|
|
- [ ] After deletion, the attendee list on the public event page updates immediately to reflect the removal
|
|
- [ ] The organizer view is not accessible via a guessable URL — it requires the organizer token stored in localStorage during event creation (US-1)
|
|
- [ ] No additional authentication step is required beyond the presence of the organizer token in localStorage
|
|
|
|
**Dependencies:** US-1, T-4
|
|
|
|
**Notes:** The organizer token is established in localStorage during event creation (US-1). Removal capability is specified in Ideen.md: "Einsicht angemeldete Gäste, kann bei Bedarf Einträge entfernen." The organizer view for *editing* event details is a separate story. Per Q-4 resolution: organizer access is confirmed as localStorage-based, using an organizer token separate from the event token. Organizer access is therefore device-bound.
|
|
|
|
---
|
|
|
|
### US-5: Edit event details as organizer
|
|
|
|
**As an** event organizer,
|
|
**I want to** update the details of an event I created,
|
|
**so that** guests always see accurate and up-to-date information if something changes.
|
|
|
|
**Acceptance Criteria:**
|
|
- [ ] The organizer can edit: title (required), description (optional), date and time (required), location (optional), expiry date (required)
|
|
- [ ] The expiry date can only be set to a date in the future — setting it to today or a past date is rejected with a clear validation message directing the organizer to use the delete feature (US-19) instead
|
|
- [ ] Editing is only accessible when a valid organizer token for the event is present in localStorage
|
|
- [ ] The edit form is pre-filled with the current event values
|
|
- [ ] Changes are persisted server-side upon submission
|
|
- [ ] After saving, the organizer is returned to the event page which reflects the updated details
|
|
- [ ] If the organizer token is absent or invalid, the edit UI is not shown and the server rejects the update request
|
|
- [ ] No account or additional authentication step is required beyond the organizer token
|
|
|
|
**Dependencies:** US-1, T-4
|
|
|
|
**Notes:** Ideen.md specifies "Updaten der Veranstaltung" as an organizer capability. Editing the expiry date is purely an edit operation. The expiry date must always be in the future — if the organizer wants an event gone immediately, they use the explicit delete feature (US-19) instead of manipulating the expiry date. Explicit event cancellation is a separate, dedicated action covered by US-18. Visual highlighting of changes on the public event page (Ideen.md: "Änderungen zum ursprünglichen Inhalt werden hervorgehoben") is a separate concern and is covered in US-9. Per Q-4 resolution: organizer authentication confirmed as localStorage-based organizer token.
|
|
|
|
---
|
|
|
|
### US-6: Bookmark an event
|
|
|
|
**As a** guest,
|
|
**I want to** bookmark an event on my current device without submitting an RSVP,
|
|
**so that** I can easily return to the event page later and stay aware of it without committing to attendance.
|
|
|
|
**Acceptance Criteria:**
|
|
- [ ] The event page shows a "Remember" / "Follow" action that requires no name or personal data
|
|
- [ ] Activating the action stores the event token, event title, and event date in localStorage — no server request is made
|
|
- [ ] The bookmark persists across browser sessions on the same device
|
|
- [ ] A second activation of the same action removes the bookmark ("unfollow"), again without any server contact
|
|
- [ ] The bookmark state is independent of the RSVP state: a guest who has already RSVPed on this device can still explicitly bookmark or un-bookmark the event
|
|
- [ ] If the event is expired, the bookmark action is still available (so the guest can still see it in their local overview)
|
|
- [ ] No personal data, IP address, or identifier is transmitted to the server when bookmarking or un-bookmarking
|
|
|
|
**Dependencies:** US-2, T-4
|
|
|
|
**Notes:** Ideen.md describes this as "Veranstaltung merken/folgen — rein lokal, kein Serverkontakt, kein Name nötig." Explicitly designed for two scenarios: (1) a guest who RSVPed on their phone and wants access on their laptop; (2) an undecided guest who wants to remember the event without committing. The bookmark is the prerequisite for the local event overview list (separate story). Because it is entirely client-side, it cannot be abused to fingerprint users. The event date is stored alongside the token and title so that US-7 (local event overview) can display it without making a server request. Locally cached title and date may become stale if the organizer edits the event — this is an acceptable trade-off for the fully-offline local overview; cached values are refreshed when the guest next visits the event page.
|
|
|
|
---
|
|
|
|
### US-7: Local event overview list
|
|
|
|
**As a** user,
|
|
**I want to** see a list of all events I have created, bookmarked, or RSVPed to on this device,
|
|
**so that** I can quickly navigate back to any event without having to find the original link again.
|
|
|
|
**Acceptance Criteria:**
|
|
- [ ] The root page (`/`) lists all events tracked locally on this device, below a project header/branding section
|
|
- [ ] An event appears in the list if it was created from this device (US-1, detected via organizer token in localStorage), bookmarked (US-6), or RSVPed from this device (US-3)
|
|
- [ ] Each entry shows at minimum: event title, date, and the user's relationship to the event (organizer / attending / not attending / bookmarked only)
|
|
- [ ] Each entry is a link that navigates directly to the event page
|
|
- [ ] The list is populated entirely from localStorage — no server request is made to render it
|
|
- [ ] Events whose date has passed are still shown in the list but visually distinguished (e.g. marked as "ended")
|
|
- [ ] If a user navigates to an event from the local overview and the server responds that the event no longer exists (deleted per US-12 or US-19), the app displays an "event no longer exists" message and offers to remove the entry from the local list
|
|
- [ ] If no events are tracked locally, an empty state is shown (not an error)
|
|
- [ ] An individual entry can be removed from the list: for bookmarked-only events this removes the bookmark; for RSVPed events it removes the local record (the server-side RSVP is unaffected); for organizer-created events it removes the local organizer token and event data
|
|
- [ ] When removing an organizer-created event entry, a confirmation warning is shown explaining that this will revoke organizer access on this device
|
|
- [ ] No personal data or event data is transmitted to the server when viewing or interacting with the overview
|
|
|
|
**Dependencies:** None
|
|
|
|
**Notes:** Ideen.md explicitly mentions "Übersichtsliste im LocalStorage: Alle Events die man zugesagt oder gemerkt hat (vgl. spliit)." Per Q-2 resolution: the overview lives at the root page `/` with a project header/branding above the event list. This feature is entirely client-side. Entries are populated from three localStorage sources: the organizer tokens (US-1), the bookmark set (US-6), and the RSVP records (US-3). All upstream stories store the event title and date in localStorage alongside their primary data, enabling this overview to render without any server contact. Locally cached values (title, date) may become stale if the organizer edits the event via US-5; stale data is refreshed when the user next visits the event page. Removing an entry from the local overview does not delete the server-side RSVP — that is intentional and consistent with the no-account design (the RSVP belongs to the organizer's data, not solely the guest's). Removing an organizer-created event entry removes the organizer token from localStorage, meaning the user loses organizer access on this device — the confirmation warning protects against accidental loss. Note: This story has no structural dependencies but requires the frontend scaffold from T-4 (which includes T-1) to be practically implementable. It is only meaningfully testable after US-1, US-3, or US-6 populate localStorage with event data — without those stories, the overview has nothing to display beyond the empty state.
|
|
|
|
---
|
|
|
|
### US-8: Add event to calendar
|
|
|
|
**As a** guest,
|
|
**I want to** add the event to my personal calendar,
|
|
**so that** I am reminded of it and always have the current date, time, and location at hand.
|
|
|
|
**Acceptance Criteria:**
|
|
- [ ] The event page provides a `.ics` file download link that generates a standards-compliant iCalendar (RFC 5545) file
|
|
- [ ] The `.ics` file includes: event title, description (if present), start date and time, location (if present), the public event URL, and a unique UID derived from the event token
|
|
- [ ] The `.ics` file is generated and served server-side; downloading it does not require JavaScript
|
|
- [ ] The event page also provides a `webcal://` subscription link so that calendar applications can subscribe and receive automatic updates when the event is edited (US-5)
|
|
- [ ] The `webcal://` endpoint serves the identical iCalendar content as the `.ics` download, using the same event token in the URL
|
|
- [ ] Both links are available to any visitor holding the event link — no RSVP, login, or personal data required
|
|
- [ ] No personal data, name, or IP address is logged when either link is accessed
|
|
- [ ] If the event has expired, both links remain available so the guest can still obtain the calendar record
|
|
- [ ] If the event has been cancelled (US-18), the `.ics` file and `webcal://` feed include `STATUS:CANCELLED` so that subscribed calendar applications reflect the cancellation on their next sync [deferred until US-18 is implemented]
|
|
|
|
**Dependencies:** US-2, T-4
|
|
|
|
**Notes:** Ideen.md specifies "Kalender-Integration: .ics-Download + optional webcal:// für Live-Updates bei Änderungen." The `webcal://` subscription is especially valuable alongside US-5 (Edit event details): when the organizer updates the date or location, subscribed guests see the change reflected in their calendar on the next sync without having to revisit the event page. The UID in the `.ics` file must be stable across regenerations (derived from the event token) so that calendar applications update the existing entry rather than creating duplicates.
|
|
|
|
---
|
|
|
|
### US-9: Highlight changed event details
|
|
|
|
**As a** guest,
|
|
**I want to** see which event details have changed since I last visited the event page,
|
|
**so that** I immediately notice important updates like a rescheduled date, a new time, or a changed location.
|
|
|
|
**Acceptance Criteria:**
|
|
- [ ] When the organizer saves an edit (US-5), the server records which fields changed (title, description, date/time, location) and stores the timestamp of that edit alongside the event
|
|
- [ ] When a guest opens the event page, any field that was modified in the most recent organizer edit is visually highlighted (e.g. a "recently changed" indicator next to the field)
|
|
- [ ] The highlight is only shown to guests who have not visited the event page since the most recent edit — determined by comparing the event's `last_edited_at` timestamp against a `last_seen_at` value stored in localStorage per event token
|
|
- [ ] On first visit (no `last_seen_at` in localStorage), no highlight is shown — the event is new to the guest, so highlighting individual fields would be misleading
|
|
- [ ] After the event page is rendered, the guest's `last_seen_at` in localStorage is updated to match the current `last_edited_at`, so the highlight disappears on the next visit
|
|
- [ ] The highlight mechanism is entirely client-side: the `last_seen_at` timestamp is stored and read locally; no visit data is transmitted to the server
|
|
- [ ] If the organizer makes multiple successive edits, only the fields changed in the most recent edit are highlighted; earlier intermediate changes are not tracked
|
|
- [ ] If the event has not been edited since creation, no highlights are shown
|
|
|
|
**Dependencies:** US-2, US-5, T-4
|
|
|
|
**Notes:** Ideen.md specifies "Änderungen zum ursprünglichen Inhalt (z.b. geändertes datum/ort) werden iwi hervorgehoben." The comparison is against the most recent edit (not the original creation values) — simpler and more actionable for guests. Storing the set of changed field names server-side (alongside `last_edited_at`) is necessary because the client cannot reconstruct which fields changed from timestamps alone. The highlight logic runs client-side using only locally stored state; no server round-trip is required beyond the normal event page load.
|
|
|
|
---
|
|
|
|
### US-10a: Post update messages as organizer
|
|
|
|
**As an** event organizer,
|
|
**I want to** post short update messages on the event page and manage them,
|
|
**so that** guests are informed of announcements or notes without requiring a separate communication channel.
|
|
|
|
**Acceptance Criteria:**
|
|
- [ ] From the organizer view, the organizer can compose and submit a plain-text update message
|
|
- [ ] Each submitted message is stored server-side, associated with the event, and timestamped at the time of posting
|
|
- [ ] All update messages for an event are displayed on the public event page in reverse chronological order (newest first), each with a human-readable timestamp
|
|
- [ ] Update messages cannot be posted after the event's expiry date
|
|
- [ ] The organizer can delete any previously posted update message from the organizer view; deletion is permanent and the message is immediately removed from the public event page
|
|
- [ ] If the organizer token is absent or invalid, the compose and delete UI is not shown and the server rejects any attempt to post or delete update messages
|
|
- [ ] No account or additional authentication step is required beyond the organizer token
|
|
- [ ] No personal data or IP address is logged when update messages are fetched or posted
|
|
|
|
**Dependencies:** US-1, US-2, T-4
|
|
|
|
**Notes:** Ideen.md specifies "Veranstalter kann Updatenachrichten im Event posten." This story covers the server-side feature: posting, displaying, and deleting update messages. The client-side read-state tracking (badge/indicator for new updates) is a separate concern covered in US-10b. Per Q-3 resolution: the app is a SPA; JavaScript-dependent rendering is acceptable for update messages. Cancelled events (US-18): posting update messages is not blocked by cancellation, only by expiry (AC 4). This is intentional — the organizer may want to post post-cancellation communication (e.g. a rescheduling notice or explanation). The cancellation message (US-18) is a static one-time message, while update messages are a stream of announcements serving a different purpose.
|
|
|
|
---
|
|
|
|
### US-10b: New-update indicator for guests
|
|
|
|
**As a** guest,
|
|
**I want to** see a visual indicator when there are update messages I haven't seen yet,
|
|
**so that** I immediately notice new announcements without having to read through all messages.
|
|
|
|
**Acceptance Criteria:**
|
|
- [ ] Guests who open the event page and have unread updates (i.e. updates posted since their last visit) see a visual indicator — a badge or highlighted section — drawing attention to the new messages
|
|
- [ ] "Read" state is tracked entirely in localStorage: on page load, the timestamp of the newest update is compared to a `updates_last_seen_at` value stored locally per event token; if the update is newer, the indicator is shown
|
|
- [ ] After the event page is rendered, `updates_last_seen_at` in localStorage is set to the current latest update timestamp, so the indicator clears on the next visit
|
|
- [ ] On first visit (no `updates_last_seen_at` in localStorage), no "new update" indicator is shown; updates are displayed as-is without a "new" badge
|
|
- [ ] No server request is made to record that a guest read the updates — tracking is purely local
|
|
|
|
**Dependencies:** US-10a
|
|
|
|
**Notes:** Ideen.md specifies "pro Device wird via LocalStorage gemerkt was man schon gesehen hat (Badge/Hervorhebung für neue Updates)." This story is the client-side read-state complement to US-10a (which covers posting, displaying, and deleting messages). The first-visit exclusion (no badge on first open) is intentional — a guest who has never seen the event before would find all updates misleading to label as "new". The `updates_last_seen_at` key is separate from the `last_seen_at` key used in US-9. This story is distinct from US-9 (change highlighting for edited event fields): US-9 highlights structural field changes (date, location, title), while this story covers awareness of new free-form announcements.
|
|
|
|
---
|
|
|
|
### US-11: Generate a QR code for an event
|
|
|
|
**As an** event organizer,
|
|
**I want to** generate and download a QR code for my event,
|
|
**so that** I can print it on posters or flyers and let people access the event page by scanning it.
|
|
|
|
**Acceptance Criteria:**
|
|
- [ ] The event page displays a QR code that encodes the public event URL
|
|
- [ ] The QR code is generated entirely server-side — no external QR code service is called
|
|
- [ ] The QR code is downloadable as a file suitable for printing (e.g. SVG or high-resolution PNG)
|
|
- [ ] The QR code download is a direct link to a server endpoint — the actual file download does not require client-side generation
|
|
- [ ] The QR code is accessible to any visitor holding the event link, not only the organizer
|
|
- [ ] No personal data, IP address, or identifier is transmitted to any third party when the QR code is generated or downloaded
|
|
- [ ] The QR code remains available and downloadable after the event has expired
|
|
|
|
**Dependencies:** US-2, T-4
|
|
|
|
**Notes:** Ideen.md specifies "QR Code generieren (z.B. für Plakate/Flyer)." The QR code must be server-side generated — calling an external service would violate the no-external-dependencies-that-phone-home statute. The code encodes only the public event URL; no additional metadata is embedded. Making it available to all visitors (not just the organizer) reflects the use case: the organizer can hand printed material to guests, and guests who received a physical flyer can share the event link digitally by re-scanning. Per Q-3 resolution: the app is a SPA; client-side rendering of the QR code display is acceptable. The download mechanism remains a direct server endpoint link.
|
|
|
|
---
|
|
|
|
### US-12: Automatic data deletion after expiry date
|
|
|
|
**As a** guest,
|
|
**I want** all event data — including my RSVP and any other stored personal information — to be automatically and permanently deleted after the event's expiry date,
|
|
**so that** I can trust that data I submitted is not retained on the server longer than necessary.
|
|
|
|
**Acceptance Criteria:**
|
|
- [ ] The server runs a periodic cleanup process that deletes all data associated with events whose expiry date has passed
|
|
- [ ] The cleanup deletes the event record itself along with all associated RSVPs, update messages (US-10a), field-change metadata (US-9), stored header images (US-16) [deferred until US-16 is implemented], and cancellation state (US-18 if applicable)
|
|
- [ ] After deletion, the event's public URL returns a clear "event not found" response — no partial data is ever served
|
|
- [ ] The cleanup process runs automatically without manual operator intervention (e.g. a scheduled job or on-request lazy cleanup triggered by access attempts)
|
|
- [ ] No log entry records the names, RSVPs, or any personal data of the deleted event's guests — the deletion is silent from a logging perspective
|
|
- [ ] Extending the expiry date via US-5 (before it has passed) delays the deletion accordingly — the cleanup always uses the current stored expiry date
|
|
- [ ] The cleanup is not triggered early: data is retained until the expiry date has passed, not before
|
|
|
|
**Dependencies:** US-1, T-4
|
|
|
|
**Notes:** Ideen.md specifies "Ablaufdatum als Pflichtfeld, nach dem alle gespeicherten Daten gelöscht werden." This is a privacy guarantee, not merely a housekeeping task. The mandatory expiry date in US-1 is only meaningful if the server actually enforces deletion. The implementation strategy (scheduled cron job, lazy cleanup on access, or both) is an architectural decision to be made during implementation. What matters at the story level is the observable behavior: data is gone after expiry, and no residual records remain. LocalStorage entries on guests' devices are unaffected by server-side deletion — that is intentional and consistent with the client-side-only nature of localStorage.
|
|
|
|
---
|
|
|
|
### US-13: Limit the number of active events per instance
|
|
|
|
**As a** self-hoster,
|
|
**I want to** configure a maximum number of simultaneously active events via a server environment variable,
|
|
**so that** I can prevent storage exhaustion and limit potential abuse on my instance without modifying code.
|
|
|
|
**Acceptance Criteria:**
|
|
- [ ] The server reads a configurable environment variable (e.g. `MAX_ACTIVE_EVENTS`) at startup to determine the event cap
|
|
- [ ] If the configured limit is reached, any attempt to create a new event is rejected with a clear error response indicating the instance is at capacity
|
|
- [ ] The error is surfaced to the user on the event creation form — not as a silent failure
|
|
- [ ] If the environment variable is unset or empty, no limit is applied (unlimited events by default, suitable for personal or trusted-group instances)
|
|
- [ ] Only non-expired events count toward the limit; expired events awaiting cleanup are not counted
|
|
- [ ] The limit is enforced server-side; it cannot be bypassed by the client
|
|
- [ ] No personal data is logged when the limit is hit — only the rejection response is returned
|
|
- [ ] The `MAX_ACTIVE_EVENTS` environment variable is documented in the README's self-hosting section (configuration table)
|
|
|
|
**Dependencies:** US-1, T-4
|
|
|
|
**Notes:** Ideen.md lists "Max aktive Events als serverseitige Konfiguration (env variable)" under security/abuse-prevention measures. This is a deployment-time configuration intended for the self-hoster, not a user-facing feature in the traditional sense. The story is written from the self-hoster's perspective because they are the ones who configure and benefit from this capability. The environment variable approach aligns with the Dockerfile-based deployment model described in CLAUDE.md. Only non-expired events count toward the limit — consistent with US-12 (expired events are deleted and must not permanently consume capacity).
|
|
|
|
---
|
|
|
|
### US-14: Install as Progressive Web App
|
|
|
|
**As a** guest,
|
|
**I want to** install the app on my device from the browser,
|
|
**so that** it feels like a native app and I can launch it directly from my home screen.
|
|
|
|
**Acceptance Criteria:**
|
|
- [ ] The app serves a valid web app manifest with at minimum: app name, icons in multiple sizes, standalone display mode, theme color, and a start URL
|
|
- [ ] The app meets browser installability requirements (manifest + registered service worker) so that the browser's "Add to Home Screen" / install prompt is available on supported mobile and desktop browsers
|
|
- [ ] When launched from the home screen, the app opens in standalone mode — without browser address bar or navigation chrome
|
|
- [ ] The app displays the configured icon and name on the device's home screen and in the OS app switcher
|
|
- [ ] On repeat visits, previously loaded pages and app assets load quickly due to service worker caching
|
|
- [ ] No external resources are fetched by the manifest or service worker — all assets are self-hosted
|
|
- [ ] The manifest's start URL points to the root page (`/`), which serves the local event overview (US-7), so returning users see their tracked events immediately
|
|
|
|
**Dependencies:** T-4
|
|
|
|
**Notes:** Ideen.md states "Soll als PWA im Browser laufen / Damit es sich wie eine normale app anfühlt." The PWA requirement is about installability and native-app feel — the app should be indistinguishable from a native app when launched from the home screen. Per Q-2 resolution: the local event overview lives at `/` and serves as the start URL. Per Q-3 resolution: the app is a SPA with a RESTful API; the service worker caching strategy will be determined during implementation. Note: While this story depends only on T-4 structurally, the service worker and manifest are only meaningfully testable after other stories (e.g. US-2, US-7) provide actual pages and assets to cache and serve from the home screen.
|
|
|
|
---
|
|
|
|
### US-15: Choose event color theme
|
|
|
|
**As an** event organizer,
|
|
**I want to** choose a visual color theme for my event page,
|
|
**so that** the event page reflects the mood or style of the event and stands out visually.
|
|
|
|
**Acceptance Criteria:**
|
|
- [ ] During event creation or editing (US-5), the organizer can select from a set of predefined color themes (e.g. color schemes or visual styles)
|
|
- [ ] A default theme is applied if the organizer makes no selection
|
|
- [ ] Theme selection is persisted server-side alongside the event data
|
|
- [ ] The guest-facing event page renders with the selected color theme
|
|
- [ ] Themes affect only the individual event page, not the app's global UI (navigation, local overview, forms)
|
|
- [ ] The customization UI is part of the event creation and editing forms
|
|
- [ ] No external resources are required for any predefined theme — all styles are self-contained
|
|
|
|
**Dependencies:** US-1, US-2, T-4
|
|
|
|
**Notes:** Ideen.md mentions "Irgendwie auch Designbar, sofern man das will." Per Q-1 resolution: predefined themes for event pages. This is event-level styling — each event can have its own visual identity. The app's global appearance (including dark/light mode, US-17) is a separate concern. No external service or API key is needed for predefined themes. The interaction between event-level color themes and the app-level dark/light mode (US-17) must be considered during implementation: predefined themes should remain readable and visually coherent regardless of whether the user has dark or light mode active on the surrounding app chrome (see also US-17 notes).
|
|
|
|
---
|
|
|
|
### US-16: Select event header image from Unsplash
|
|
|
|
**As an** event organizer,
|
|
**I want to** search for and select a header image for my event page via an integrated image search,
|
|
**so that** the event page has a visually appealing header that matches the event's theme.
|
|
|
|
**Acceptance Criteria:**
|
|
- [ ] During event creation or editing (US-5), the organizer can search for a header image via an integrated Unsplash search
|
|
- [ ] The Unsplash search is server-proxied: the client sends the search query to the app's backend, which calls the Unsplash API and returns results — the client never contacts Unsplash directly
|
|
- [ ] When an image is selected, the server downloads and stores the image locally on disk; it is served from the app's own domain, not from Unsplash's CDN
|
|
- [ ] Proper Unsplash attribution (photographer name, link to Unsplash) is displayed alongside the header image on the event page, as required by the Unsplash API terms
|
|
- [ ] The organizer can remove a previously selected header image
|
|
- [ ] The guest-facing event page renders with the selected header image (if set)
|
|
- [ ] No guest data, IP address, or identifier is transmitted to Unsplash or any third party from the guest's browser
|
|
- [ ] If the server has no Unsplash API key configured, the image search feature is unavailable — the option is simply not shown, no error
|
|
- [ ] If the API key is removed from the config after images were already stored, existing images continue to render from disk; only the search/select UI becomes unavailable; the event page never breaks due to a missing API key
|
|
- [ ] When the event expires and is deleted (US-12), the stored image file is deleted along with all other event data
|
|
- [ ] The `UNSPLASH_API_KEY` environment variable and the persistent volume requirement for image storage are documented in the README's self-hosting section (configuration table and storage notes)
|
|
|
|
**Dependencies:** US-1, US-2, T-4
|
|
|
|
**Notes:** Per Q-1 resolution: Unsplash image search as an optional feature alongside predefined themes (US-15). The Unsplash integration is privacy-safe because it is server-proxied: guests never contact Unsplash; the server fetches and stores images locally. The Unsplash API requires attribution (photographer name and Unsplash link) — this is a legal/terms requirement. The Unsplash API key is an optional deployment configuration for the self-hoster; if not configured, only predefined themes (US-15) are available. Image storage on disk requires the hoster to configure a persistent volume — standard Docker practice, documented in README alongside the docker-compose example.
|
|
|
|
---
|
|
|
|
### US-17: Dark/light mode
|
|
|
|
**As a** user,
|
|
**I want to** switch between dark and light mode for the app's interface,
|
|
**so that** I can use the app comfortably in different lighting conditions and according to my personal preference.
|
|
|
|
**Acceptance Criteria:**
|
|
- [ ] The app respects the user's operating system / browser preference (`prefers-color-scheme`) as the default mode on first visit
|
|
- [ ] A visible toggle allows the user to manually switch between dark and light mode
|
|
- [ ] The user's manual preference is stored in localStorage and takes precedence over the system preference on subsequent visits
|
|
- [ ] The toggle is accessible from any page in the app (e.g. in a header or navigation element)
|
|
- [ ] Dark/light mode affects the app's global UI: navigation, local event overview, forms, and all non-event-page chrome
|
|
- [ ] Event pages use their own color theme (US-15) which is independent of the app-level dark/light mode
|
|
- [ ] The mode switch is purely client-side — no server request is made, no preference data is transmitted
|
|
- [ ] Both modes meet accessibility contrast requirements (WCAG AA minimum)
|
|
|
|
**Dependencies:** None
|
|
|
|
**Notes:** This is app-level theming — completely separate from the event-level color themes (US-15). Dark/light mode affects the overall UI (navigation, local overview, forms, etc.), not individual event pages. The interaction between app-level dark/light mode and event-level color themes (US-15) should be considered during implementation to ensure readability in both modes. Note: This story has no structural dependencies but requires the frontend scaffold from T-4 (which includes T-1) to be practically implementable.
|
|
|
|
---
|
|
|
|
### US-18: Cancel an event as organizer
|
|
|
|
**As an** event organizer,
|
|
**I want to** explicitly cancel my event and optionally provide a reason,
|
|
**so that** guests clearly see the event is cancelled and understand why.
|
|
|
|
**Acceptance Criteria:**
|
|
- [ ] The organizer view provides a dedicated "Cancel event" action, separate from editing event details (US-5)
|
|
- [ ] When cancelling, the organizer can optionally enter a cancellation message (reason/explanation)
|
|
- [ ] When cancelling, the organizer can optionally adjust the event's expiry date (to control how long the cancellation notice remains visible before data deletion per US-12); the adjusted date must be in the future — consistent with US-5's expiry date constraint and US-19's role as the immediate-removal mechanism
|
|
- [ ] A confirmation step is required before cancellation is finalized
|
|
- [ ] After cancellation, the event page clearly displays a "cancelled" state with the cancellation message if provided (US-2)
|
|
- [ ] RSVPs are no longer possible on a cancelled event — the RSVP form is hidden and the server rejects any RSVP submissions (US-3)
|
|
- [ ] The event remains visible and accessible via its link until its expiry date, so guests can still see what happened and why
|
|
- [ ] After cancellation, the organizer can still edit the cancellation message but cannot "un-cancel" the event
|
|
- [ ] Cancellation is only accessible when a valid organizer token for the event is present in localStorage
|
|
- [ ] If the organizer token is absent or invalid, the cancel action is not shown and the server rejects cancellation requests
|
|
- [ ] The cancellation state and message are persisted server-side
|
|
- [ ] No account or additional authentication step is required beyond the organizer token
|
|
|
|
**Dependencies:** US-1, T-4
|
|
|
|
**Notes:** The overseer identified that cancellation was previously conflated with shortening the expiry date in US-5, which was unintuitive and conflated two fundamentally different actions. Editing the expiry date (US-5) is now purely an edit operation; cancellation is an explicit, dedicated action. The event remains visible after cancellation (unlike immediate deletion) until the expiry date passes, giving guests time to see the cancellation notice. The optional expiry date adjustment during cancellation lets the organizer control how long the notice stays visible — e.g. keep it up for a week so everyone sees it, or shorten it to trigger faster cleanup. The "no un-cancel" constraint keeps the model simple: cancellation is a one-way state transition. If the organizer made a mistake, they can create a new event.
|
|
|
|
---
|
|
|
|
### US-19: Delete an event as organizer
|
|
|
|
**As an** event organizer,
|
|
**I want to** immediately and permanently delete my event and all its data,
|
|
**so that** I can remove an event entirely when it was created by mistake or is no longer needed.
|
|
|
|
**Acceptance Criteria:**
|
|
- [ ] The organizer view provides a dedicated "Delete event" action, separate from editing (US-5) and cancelling (US-18)
|
|
- [ ] A confirmation warning is displayed before deletion, clearly stating that the action is immediate, permanent, and irreversible — all event data including RSVPs, update messages, and images will be lost
|
|
- [ ] Upon confirmation, the server permanently deletes the event record and all associated data: RSVPs, update messages (US-10a), field-change metadata (US-9), stored header images (US-16), and cancellation state (US-18 if applicable)
|
|
- [ ] After deletion, the event's public URL returns a "event not found" response (consistent with US-2 and US-12 behavior)
|
|
- [ ] After successful deletion, the app removes the event's organizer token and metadata from localStorage and redirects the organizer to the root page (`/`)
|
|
- [ ] Deletion is accessible only when a valid organizer token for the event is present in localStorage
|
|
- [ ] If the organizer token is absent or invalid, the delete action is not shown and the server rejects deletion requests
|
|
- [ ] No personal data or event data is logged during deletion — the deletion is silent from a logging perspective
|
|
- [ ] No account or additional authentication step is required beyond the organizer token
|
|
- [ ] The event can be deleted regardless of its current state (active, cancelled, or expired)
|
|
|
|
**Dependencies:** US-1, T-4
|
|
|
|
**Notes:** The overseer identified that using the expiry date as a deletion mechanism (setting it to today or a past date in US-5) was unintuitive and conflated two different actions. US-5 now enforces that the expiry date can only be set to a future date. If the organizer wants the event gone immediately, they use this explicit deletion feature. Unlike cancellation (US-18), which keeps the event visible with a cancellation notice until the expiry date, deletion removes the event entirely and immediately. This is the organizer's "nuclear option" — useful when the event was created by mistake, contains wrong information, or is no longer needed at all. The deletion behavior is identical to what US-12 does automatically after expiry, but triggered manually and immediately by the organizer.
|
|
|