Some checks are pending
CI / validate (push) Waiting to run
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
226 lines
14 KiB
Markdown
226 lines
14 KiB
Markdown
# Seepuls — Status
|
||
|
||
Letzter Stand: **2026-06-03**. **LIVE 🚀**
|
||
|
||
## 2026-06-03 — UI/UX-Feinschliff (Filter, Karten, Wärme, Flaggen)
|
||
|
||
Review-Punkte umgesetzt, alle live auf `seepuls.com` (build + astro-check
|
||
grün, je per Headless-Screenshot verifiziert):
|
||
|
||
- **Filter nach Dimension getrennt** — statt einer Pillen-Wolke jetzt
|
||
beschriftete Achsen „Art"/„Wo" (Orte-Seite zusätzlich „Ausstattung"),
|
||
eigener Reset je Achse, Orts-Chips eckiger zur visuellen Trennung.
|
||
- **Event-Karten** — Tickets als rechtsbündiger Akzent-Button (statt
|
||
grauem Fußnoten-Link); Kategorie-Tag nach links zur Datums-Zeile,
|
||
Merk-Herz allein rechts; „zuletzt geprüft" → dezentes ✓ mit Tooltip;
|
||
Datum durchgehend ausgeschrieben (`formatDateLong`).
|
||
- **Merk-Badge** — dunkler Text statt `#fff` (Kontrast auf hellem Cyan);
|
||
`[hidden]` greift jetzt (zeigte sonst „0", da `inline-flex` das
|
||
UA-`[hidden]` überschrieb).
|
||
- **Kontrast** — Dark-Meta-/Sekundärtext über WCAG-AA gehoben
|
||
(muted-foreground 62→70 %, text-muted-Alpha 0.7→0.85).
|
||
- **Wärmeres Dark-Theme** — Grund von kühlem Teal (Hue 205) auf warmes
|
||
Anthrazit (~28–35), Cyan bleibt komplementärer Akzent.
|
||
- **Sticky-Section-Header** — Zähler-Zahl jetzt in Serife wie das Label;
|
||
Hintergrund 85→96 % opak (kein Durchschein); `scroll-margin-top`.
|
||
- **Länder-Flaggen** als robuste Inline-SVG (DE/CH/AT/LI) statt Emoji
|
||
(neue `Flag.astro`, in Karten + Filter + Event-Detail).
|
||
|
||
## 2026-06-02 — Suche real, Amenities/Region ausgespielt, Web-Push-Folgen
|
||
|
||
Vier Verbesserungen aus dem Review umgesetzt (alle type-check grün,
|
||
112 API- + 34 Web-Tests grün; Deploy steht noch aus):
|
||
|
||
- **Suche real gemacht** — `/api/v1/search` war ein Stub (`results: []`),
|
||
ist jetzt eine echte unified Suche (kommende Events + Orte, ILIKE über
|
||
Titel/Beschreibung/Venue-Name bzw. Name/Beschreibung/Stadt). Event-`q`
|
||
trifft jetzt auch den Venue-Namen; die Startseite zeigt bei aktiver
|
||
Suche eine „Orte zu …"-Sektion. (Relevanz-Ranking/pg_trgm bewusst als
|
||
Folge-Schritt — bei der Datenmenge unnötig, vermeidet Migrations-Footgun.)
|
||
- **Amenities/Öffnungszeiten/Region ausgespielt** — Venue-Liste liefert
|
||
jetzt openingHours/amenities/smoking/heroUrl; neuer `amenity`-Filter
|
||
(jsonb `@>`, AND) + `q` in der Venues-API. Orte-Seite: Ausstattungs-Chips,
|
||
Region-Chips, „Jetzt geöffnet"-Badge (`isOpenNow`, Über-Mitternacht-fest)
|
||
+ Amenity-Badges pro Zeile. Region-Chips auch auf der Startseite.
|
||
- **Folgen → Web Push (login-frei)** — siehe `docs/NOTIFICATIONS.md`.
|
||
Neuer Kanal-agnostischer Kern: `push_endpoints` + `venue_follows` +
|
||
`notification_outbox` (Migration `0005`), Crawler-Auslöser (nur NEUE
|
||
Events, best-effort), `notification-worker` (Drain + Ruhezeiten 22–8h +
|
||
Pruning toter Endpoints), öffentliche `/api/v1/push/*`-Routen, Service
|
||
Worker + `lib/push.ts` + Schalter auf `/gemerkt`. iOS-Hinweis (PWA).
|
||
`web-push`-Lib (lädt unter Bun). VAPID-Env in `.env.example`.
|
||
**Offen vor Live:** VAPID-Keys erzeugen+SOPS, VVT-Eintrag, Migration
|
||
0005 + echter Zustell-Smoke nach Deploy. APNs/E-Mail/iCal-Folge-Feed
|
||
als weitere Kanäle vorgesehen.
|
||
|
||
## 2026-05-27 — iCal-Export, Rate-Limit, Lazy-Map, Web-Tests
|
||
|
||
## 2026-05-27 — iCal-Export, Rate-Limit, Lazy-Map, Web-Tests
|
||
|
||
Alles live auf `seepuls.com` / `api.seepuls.com` + verifiziert:
|
||
|
||
- **iCal (RFC 5545)** — löst das Marketing-Versprechen „JSON + iCal" ein.
|
||
- `GET /api/v1/events/:slug.ics` — Einzel-Event-Download (Content-Disposition attachment).
|
||
- `GET /api/v1/events/feed.ics?country=®ion=&category=&q=&from=&to=` — abonnierbarer, gefilterter Feed (`Cache-Control: max-age=900`).
|
||
- Generator `apps/api/src/services/ical.ts` (Escaping, UTC-Dates, 75-Oktett-Folding, Pflicht-Quellen-Attribution in DESCRIPTION). 14 Vitest-Tests.
|
||
- Web: „📅 Zum Kalender"-Button auf Event-Detail + `<link rel="alternate" type="text/calendar">`-Discovery + sichtbarer Abo-Link auf der Startseite (spiegelt aktive Filter).
|
||
- **Rate-Limit** (`apps/api/src/middleware/rate-limit.ts`) — In-Memory Fixed-Window pro `CF-Connecting-IP`-Hash, `429` + `Retry-After`/`RateLimit-*`. Nur auf browser-direkte Vektoren: `/takedown` 10/10min, `/image-proxy` 100/min. **NICHT** auf SSR-gespiegelte Read-Routes (sonst Ein-IP-Drosselung). 5 Tests.
|
||
- **Kategorie-Filter-Fix** — `category` war auf der Startseite ein No-Op (Param gelesen, nie an die API gereicht; events-API kannte ihn nicht). Jetzt in `listQuery` + Venue-Filter, durch Web durchgereicht, greift auch für `feed.ics`.
|
||
- **Bucket-Bug-Fix** — Events an Wochenmitte-Tagen (Mi–Fr bei Wochenstart Mo) fielen in kein Zeit-Bucket und verschwanden still von der Startseite. Neues „Diese Woche"-Bucket macht die Ranges lückenlos. Regressions-Test über alle Wochentage.
|
||
- **MapLibre lazy** — `/karte` lädt die ~1 MB `maplibre-gl` nicht mehr beim Initial-Load, sondern per dynamischem `import()` (Auto-Load nach Idle bei guter Verbindung, Tap-to-load bei Save-Data/2G). Initiales karte-Script: 1,9 kB.
|
||
- **Web-Test-Harness** — Vitest in `apps/web` (vorher 0 Tests): 26 Tests für `time-buckets`, `format`, `feed`.
|
||
|
||
Deploy-Falle dokumentiert in Memory `reference_seepuls_deploy_pattern.md`: `docker compose -p seepuls` ist Pflicht (sonst API auf falschem Netz → DB-ECONNREFUSED).
|
||
|
||
Offen aus dem Review: RSS-Feeds (FEEDS.md Welle 2, getrennt von iCal), Feed-Discovery auf Region-*Pfad*-Seiten, `/submissions`-Rate-Limit (SSR-proxied).
|
||
|
||
## Phase
|
||
|
||
**α-0 ✅ + α-1 ✅ + α-2 ✅ + α-3 ✅ + α-3.5 ✅ + α-4 ✅ + α-4.5 ✅ + α-5 ✅ (Live-Deploy)**
|
||
|
||
**+ ι-0..ι-3 ✅ LIVE seit 2026-05-18 ~23:55** auf prod. Schema-Migration
|
||
applied (`venues.hero_url`+`gallery_urls`+`image_probes`), Container
|
||
rebuilt + restartet, Smokes alle grün:
|
||
- `GET /api/v1/image-proxy?url=http://127.0.0.1/...` → 403 (SSRF)
|
||
- `GET /api/v1/image-proxy?url=https://example.com/...` → 403 (off-whitelist)
|
||
- `GET /api/v1/image-proxy?url=https://apollokreuzlingen.ch/.../LogoAPOLLORGB6.png` → 200, image/png 3243 B
|
||
- `image_probes` populated: 1× ok + 2× blocked persisted
|
||
- Web `/venue/apollo-kreuzlingen` rendert SeepulsImage + onerror-Stage-1
|
||
- AGGREGATOR_POLICY-Compliance: anwaltliche §5-Review weiter offen
|
||
|
||
Plan in [`docs/IMAGES.md`](docs/IMAGES.md). Verbleibend: ι-3.5 (mana-media-
|
||
Cache nach 2 Wochen ι-3-Beobachtung) und ι-5 (Spotlight-Thumbs, gebündelt
|
||
mit σ-4 in seepuls-native).
|
||
|
||
**Live seit 2026-05-15** auf `seepuls.mana.how` und `seepuls-api.mana.how`:
|
||
- `GET https://seepuls.mana.how/` → 200 (Astro-Web mit Empty-State)
|
||
- `GET https://seepuls-api.mana.how/healthz` → `{"status":"ok","app":"seepuls"}`
|
||
- `GET https://seepuls-api.mana.how/api/v1/events?country=DE` → leere Liste
|
||
- `GET https://seepuls-api.mana.how/.well-known/mana-app.json` → Manifest
|
||
- 3 Container healthy auf mana-server: `seepuls-postgres`, `seepuls-api`, `seepuls-web`
|
||
- DB-Schema gepusht, 4 countries + 4 regions geseedet
|
||
|
||
## 🚀 LIVE-CRAWL aktiv seit 2026-05-29
|
||
|
||
Betreiber-Entscheidung (Till): Compliance akzeptiert, Live-Gang freigegeben.
|
||
`mana-compliance`-Veto-Check lief vorher (fand 1 harten Blocker: `endpoints.takedown`
|
||
fehlte im Manifest → behoben + `base_url` auf `api.seepuls.com` korrigiert).
|
||
|
||
**Stand:** `SEEPULS_SCHEDULER=on`, **14 aktive Venue-Sources** crawlen (1/60s-Tick,
|
||
24h-Intervall). Events fließen — Konzert/Theater/Club + programm-führende Museen
|
||
liefern echte datierte Events (Apollo 10, bodensee-planetarium 24, alm-konstanz 20…).
|
||
|
||
**Beim Live-Gang aufgedeckt + gefixt:**
|
||
- `scheduler.ts` Tick crashte jeden Tick (`orderBy(asc(sql\`… nulls first\`))` →
|
||
`syntax error at or near "asc"`); latenter Bug, da Scheduler nie zuvor live war.
|
||
Fix: `sql\`… asc nulls first\``.
|
||
- Docker-Build war kaputt: `@mana/shared-share-protocol` (devDep) ließ sich ohne
|
||
Verdaccio-Auth im Build nicht ziehen → `--prod` (skip devDeps), Build wieder dep-arm.
|
||
- zebra-kino entfernt: robots.txt `Disallow: /` (nur Googlebot) → nicht crawlbar.
|
||
|
||
**Offene Auflagen (kein Live-Stopper, AGGREGATOR_POLICY ✓✓):**
|
||
- ETag/If-Modified-Since real verdrahten (Schema-Felder sonst tot).
|
||
- 429/503 mit `Retry-After` statt Pauschal-Backoff behandeln.
|
||
- Museums-Ausstellungen ohne klares `starts_at` (seemuseum, KULA-Wix) werden vom
|
||
strikten parseDate gedroppt → 0 Events; ggf. Ausstellungs-Date-Range-Handling.
|
||
- β-1 anwaltliche §5-Review bleibt Tills bewusste menschliche Entscheidung.
|
||
|
||
## Was steht (Code-fertig)
|
||
|
||
| Phase | Inhalt |
|
||
|---|---|
|
||
| **α-0** | Repo-Skelett (Hono+Bun), Drizzle-Schema (8 Tabellen), docker-compose Postgres :5441, db:push grün |
|
||
| **α-1a** | robots.txt-Parser + 24h-Cache (5xx → Full-Disallow), Crawl-Policy mit `max(1.1s, robots.crawl-delay)` |
|
||
| **α-1b** | JWT-Auth (jose+JWKS), mana-llm/mana-research/mana-geocoding Clients, Venue-Extraction-Pipeline, `POST /admin/venues/from-url` mit Reviewer-Stop |
|
||
| **α-2** | Event-Source-Verwaltung (add/list/patch/probe/run), event-extraction-Pipeline mit strict-first parseDate, external_id_hash für Dedupe |
|
||
| **α-3** | Re-Crawl-Scheduler (setInterval 60s, max 1 source/tick), DRY-Extraktion `crawlAndPersistEventSource()`, exp. Backoff bei fail (6h × 2^n, cap 7d) |
|
||
| **α-3.5** | Seed (DE/CH/AT/LI + 4 Regions inkl. Bodensee), Public Read-API GET /events + /venues mit Filter (country/region/from/to/q) |
|
||
| **α-3.6** | **Stadt-Quellen** (`scope='city'`): stadtweite Sammelkalender (Konstanz/Kreuzlingen) als event_source ohne feste Venue. Jedes Event wird zur Crawl-Zeit auf eine Venue aufgelöst (matchVenues, sonst hidden pending-review-Venue). Dedupe-Hash jetzt **stadt-basiert** (geoKeyForHash) statt venue-id-basiert → ein Event dedupliziert über Venue-Quelle ↔ Stadt-Kalender ↔ Screenshot-Submit hinweg. |
|
||
| **α-3.7** | **Venue-Event-Sources via YAML kuratierbar**: optionaler `eventSource`-Block im Venue-YAML → Importer legt idempotent eine inaktive `scope='venue'`-Source an (`es_yaml_<slug>`). Strategie-Entscheidung: **venue-direkt zuerst** (eigene Programmseiten), Stadt/Guidle-Discovery als bewusste 2. Phase. Seed: **15 verifizierte** Programm-URLs (Theater/Kino/Kulturhaus/Musik/Club/Museum, beide Städte). Bewusst ausgelassen: stadttheater/spiegelhalle/werkstatt (Fremd-Ticketshop jetticket), dreispitz + konstanz.de-Museen (Guidle/§87a), cinestar (Kette), galerie-mensing (Multi-Location, nicht KN-spezifisch), Bars/Clubs mit nur Social/JS (berrys/horstklub/steg4/backstage/newfax …). |
|
||
|
||
**Tests**: 107/107 grün.
|
||
**Type-check**: grün durchgehend.
|
||
**Commits**: 8 lokal in seepuls. **mana@94e62b8** mit AGGREGATOR_POLICY gepusht.
|
||
|
||
**Deploy-Schritte α-3.6**: Migration `0004_lowly_thor.sql` (event_sources: scope
|
||
+ city/region_slug/country_code, venue_id nullable) wird beim API-Boot
|
||
auto-appliziert. Falls schon Events in prod stehen: einmalig
|
||
`docker exec seepuls-api bun run src/jobs/rehash-events.ts` (rechnet
|
||
bestehende Events auf den stadt-basierten Hash um; no-op bei leer).
|
||
|
||
**Stadt-Discovery (Phase 2) — Entscheidung 2026-05-29**: Die stadtweiten
|
||
Kalender von Konstanz/Kreuzlingen sind beide Guidle-gespeist (`guidle.com`)
|
||
und JS-gerendert. HTML-Scrapen davon trifft §87a-Datenbankschutz (DE).
|
||
Sauberer Weg = Guidle-**Export-Schnittstelle** (offizielles XML/JSON,
|
||
`support@guidle.com`), braucht aber Channel-Zugang via Guidle oder die
|
||
regionalen Tourismus-Orgs — eine **Outreach-Aufgabe, kein Code**. Der
|
||
`scope='city'`-Mechanismus (α-3.6) ist gebaut und wartet nur auf diesen
|
||
Zugang + einen `guidle-xml`-Parser (`parserHint`). Bewusst NICHT blind
|
||
gebaut. Bis dahin: venue-direkt (Phase 1, α-3.7).
|
||
|
||
## API-Oberfläche
|
||
|
||
### Public (kein Auth)
|
||
- `GET /healthz` · `/readyz` · `/healthz/details`
|
||
- `GET /.well-known/mana-app.json` — Manifest
|
||
- `GET /api/v1/events?from&to&country®ion&q&limit` + `/events/:slug`
|
||
- `GET /api/v1/venues?country®ion&limit` + `/venues/:slug`
|
||
- `GET /api/v1/search?q` (Stub)
|
||
- `POST /api/v1/takedown` — Take-Down-Antrag
|
||
|
||
### Admin (JWT-gated, mana-auth)
|
||
- `POST /api/v1/admin/venues/from-url` — Venue per Crawl anlegen (Reviewer-Stop)
|
||
- `GET/POST /api/v1/admin/venues/:venueId/event-sources` — Venue-Sources (scope='venue')
|
||
- `POST /api/v1/admin/event-sources` — **Stadt-Quelle anlegen** (scope='city'; body: url, city, regionSlug, countryCode)
|
||
- `GET/PATCH /api/v1/admin/event-sources/:id`
|
||
- `POST /api/v1/admin/event-sources/:id/probe` — Vorschau ohne DB-Write (zeigt venue_name pro Event)
|
||
- `POST /api/v1/admin/event-sources/:id/run` — manueller Crawl-Run
|
||
|
||
### Service-Key-gated (mana-admin)
|
||
- `GET /api/v1/dsgvo/export?email=` · `DELETE /dsgvo/users/by-email/:email`
|
||
|
||
## Was fehlt vor α-1 ✅
|
||
|
||
- **α-1c — Integrations-Smoke**: braucht laufenden mana-research +
|
||
mana-llm + mana-geocoding. Aktueller Stand lokal: nur mana-auth
|
||
online. Optionen:
|
||
1. Stack lokal hochfahren (Mac Mini-Compose oder `mana/` docker-compose)
|
||
2. Prod-Stack ansprechen (`research.mana.how`, `llm.mana.how`,
|
||
`geocoding.mana.how`) — braucht Service-Key für seepuls
|
||
- **mana-research-Response-Form** angenommen, nicht verifiziert.
|
||
|
||
## Pre-Live-Gates (β-1 + β-2) — nicht-verhandelbar
|
||
|
||
- **β-1**: AGGREGATOR_POLICY anwaltliche Review.
|
||
- **β-2**: Take-Down-Postfach besetzt (kontakt@mana.how), Claim-Flow
|
||
live, `mana-compliance`-Subagent-Veto-Check grün.
|
||
|
||
## Phase α-4 — Astro-Web (nächster autonomer Schritt)
|
||
|
||
Greenfield Astro-Setup unter `apps/web/`:
|
||
- Liste `/` (heutige + kommende Events, Filter-Bar)
|
||
- `/heute`, `/wochenende`, `/region/:slug`, `/land/:code`
|
||
- Detail `/event/:slug`, `/venue/:slug`
|
||
- Karten-Ansicht via Leaflet-Island
|
||
- Filter als URL-State (teilbar)
|
||
- SSR via Node-Adapter, damit neue Events nach Re-Crawl ohne
|
||
Rebuild sichtbar werden
|
||
- Attribution-Block pro Event (Quell-Domain + Crawl-Datum)
|
||
|
||
## Infrastruktur
|
||
|
||
- **Forgejo-Repo**: `git.mana.how/till/seepuls` ✅ (angelegt 2026-05-15, Repo-ID 21, alle 12+ Commits gepusht)
|
||
- **DNS + Cloudflared-Tunnel**: `seepuls.mana.how` + `seepuls-api.mana.how` ✅ (CNAMEs live, Tunnel-Config in `managarten/cloudflared-config.yml` committed `21e8bcaa5` und reloaded). Beide liefern aktuell HTTP 502 — kein Container hört auf :3095/:3096. Erwartet bis Deploy.
|
||
|
||
## Offene Punkte
|
||
|
||
- **mana-llm Port-Drift** (PORTS.md 3062 vs CLAUDE.md 3025) → seepuls
|
||
folgt 3025 (Service-CLAUDE).
|
||
- **`.npmrc`-Warning** bei pnpm install (`${NPM_AUTH_TOKEN}` ohne
|
||
env) — kosmetisch, harmlos.
|
||
- **Service-Key bei mana-auth** für seepuls anlegen (POST
|
||
/admin/apps) — nötig für α-1c.
|
||
- **Deploy auf mana-server** (`~/projects/seepuls/`-Compose) — sobald
|
||
ein Container hochkommt, sind seepuls.mana.how + seepuls-api.mana.how
|
||
sofort live (Cloudflared+DNS warten).
|
||
- **AGGREGATOR_POLICY anwaltliche Review** offen.
|