seepuls/STATUS.md
Till JS bb81c9b9ea
Some checks are pending
CI / validate (push) Waiting to run
docs(status): UI/UX-Feinschliff (Filter/Karten/Wärme/Flaggen) dokumentiert
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:05:17 +02:00

226 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 (~2835), 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 228h +
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=&region=&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 (MiFr 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&region&q&limit` + `/events/:slug`
- `GET /api/v1/venues?country&region&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.