fix(events): production wiring + polling resilience (quick wins)

Five small follow-ups on Phase 1b:

- docker-compose.macmini.yml: add the mana-events container with the
  same shape as mana-credits, expose port 3065, add a Traefik route
  for events.mana.how, and inject PUBLIC_MANA_EVENTS_URL into the
  mana-web container so the SvelteKit SSR + browser both reach it.
- mana-events: background sweeper that deletes rsvp_rate_buckets
  rows older than 2h every hour. Without it, long-published events
  accumulate one row per traffic-hour forever (FK cascade only fires
  on snapshot delete).
- PublicRsvpList: track consecutiveFailures and only show the error
  banner after two failures in a row, so a single mid-poll network
  hiccup doesn't flash a 30s error the user can't act on.
- apps/mana/apps/web: declare postgres as a devDep (already imported
  by the e2e spec via pnpm hoisting, now explicit).
This commit is contained in:
Till JS 2026-04-07 18:53:29 +02:00
parent 354cbcb176
commit 640242500e
5 changed files with 116 additions and 5 deletions

View file

@ -31,6 +31,7 @@
"autoprefixer": "^10.4.20",
"fake-indexeddb": "^6.2.5",
"postcss": "^8.4.49",
"postgres": "^3.4.9",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.2",
"prettier-plugin-tailwindcss": "^0.6.10",

View file

@ -11,19 +11,26 @@
let rsvps = $state<PublicRsvpRecord[]>([]);
let loading = $state(false);
let lastError = $state<string | null>(null);
let lastErrorMessage = $state<string | null>(null);
let consecutiveFailures = $state(0);
let lastFetchedAt = $state<Date | null>(null);
// Surface the error only after two failures in a row so a single network
// hiccup mid-poll doesn't flash a scary banner the user can't act on.
const showError = $derived(consecutiveFailures >= 2 && lastErrorMessage !== null);
async function fetchRsvps() {
if (!isPublished) return;
loading = true;
try {
const res = await eventsApi.getRsvps(eventId);
rsvps = res.rsvps;
lastError = null;
lastErrorMessage = null;
consecutiveFailures = 0;
lastFetchedAt = new Date();
} catch (e) {
lastError = e instanceof Error ? e.message : 'Fehler beim Laden';
lastErrorMessage = e instanceof Error ? e.message : 'Fehler beim Laden';
consecutiveFailures++;
} finally {
loading = false;
}
@ -62,8 +69,8 @@
</button>
</div>
{#if lastError}
<p class="error">{lastError}</p>
{#if showError}
<p class="error">{lastErrorMessage}</p>
{:else if rsvps.length === 0 && !loading}
<p class="empty">Noch keine Antworten via Share-Link.</p>
{:else}