Skip to content

feat: add interactive Leaflet map and format styles and code - #143

Open
dmnktoe wants to merge 3 commits into
mainfrom
feat-interactive-map
Open

dmnktoe wants to merge 3 commits into
mainfrom
feat-interactive-map

Conversation

@dmnktoe

@dmnktoe dmnktoe commented Mar 12, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

Release Notes

  • New Features
    • Launched an interactive map page to visualize contributions by city
    • Interactive map displays city contribution counts using color-coded markers and popups
    • View aggregated statistics showing total cities and contributions
    • Browse a detailed city list with individual contribution counts
    • Added map navigation link to the main menu

@coderabbitai

coderabbitai Bot commented Mar 12, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Introduces an interactive map feature with backend handlers serving a map page and city contribution API endpoint, a repository function querying city contribution counts, and a comprehensive frontend implementation using Leaflet with geocoding integration, styling, and navigation updates.

Changes

Cohort / File(s) Summary
Backend Handlers & Routes
internal/handlers/app/map.go, internal/handlers/routes.go
Adds MapHandler rendering the /karte map page with footer stats and city data, MapDataHandler returning city contributions as JSON via /api/map-data, and corresponding route registrations. Includes error logging and sentry reporting.
Data Repository
internal/repository/map.go
Introduces CityContrib struct and GetCityContribCounts function querying contributions table, grouping by city, ordering by count descending, with proper error handling and resource cleanup.
Frontend Map Module
src/lib/map.ts, src/main.ts
Adds initMap() function fetching city data, geocoding via Nominatim with rate limiting, and rendering interactive Leaflet map with colored markers, popups, legend, and bound fitting. Integrates into main initialization.
Styling & CSS
src/styles.css, web/static/style.css
Imports Leaflet CSS; adds comprehensive styles for map page layout, hero section, Leaflet container, legend, markers, popups, stats cards, city list, and responsive adjustments for smaller viewports.
Templates & Navigation
web/templates/app/map.html, web/templates/components/header.html
New map.html template rendering page header, map container, stats, and conditional city list; adds 🗺️ navigation link to /karte in header.
Dependencies
package.json
Adds leaflet and @types/leaflet as runtime and dev dependencies respectively.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Handler as Backend Handler
    participant Repo as Repository
    participant DB as Database
    participant API as Nominatim API
    participant Leaflet

    Client->>Handler: GET /karte
    Handler->>Repo: GetCityContribCounts()
    Repo->>DB: Query contributions by city
    DB-->>Repo: City contribution rows
    Repo-->>Handler: []CityContrib
    Handler-->>Client: Render map.html with city data

    Client->>Client: DOM Ready
    Client->>Client: initMap()
    Client->>Handler: GET /api/map-data
    Handler->>Repo: GetCityContribCounts()
    Repo->>DB: Query contributions by city
    DB-->>Repo: City contribution rows
    Repo-->>Handler: []CityContrib
    Handler-->>Client: JSON response

    Client->>API: Geocode city names (with rate limiting)
    API-->>Client: Coordinates for each city
    Client->>Leaflet: Create markers with popups & legend
    Client->>Leaflet: Fit map bounds to geocoded cities
    Leaflet-->>Client: Interactive map rendered
Loading

Possibly Related PRs

  • feat: improvements #3: Modifies handler code to fetch and thread footer statistics into template context, similar pattern to MapHandler fetching and passing data to templates.

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🗺️ Hops across the globe with glee,
Leaflet markers dancing free,
Cities grouped by contribution's might,
A cartographic delight! 🐰✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding an interactive Leaflet map feature. It is concise, clear, and specific enough for teammates to understand the primary contribution.
Docstring Coverage ✅ Passed Docstring coverage is 90.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-interactive-map
📝 Coding Plan for PR comments
  • Generate coding plan

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (5)
src/lib/map.ts (1)

48-59: Consider caching geocoded coordinates server-side.

With 1.1s delay per city to respect Nominatim's rate limit, loading time scales linearly. For 30 cities, users wait ~33 seconds. Consider:

  1. Caching lat/lon in the database after first geocode
  2. Returning coordinates from /api/map-data directly
  3. Or at minimum, showing a progress indicator during geocoding
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/map.ts` around lines 48 - 59, The geocoding loop in geocodeAll (which
calls geocodeCity and returns GeocodedCity[]) causes linear delays; modify the
server-side flow to cache coordinates for CityContrib entries: when handling
/api/map-data, first check the DB record for existing lat/lon and return those
immediately; update geocodeAll to skip API calls for cities with cached coords
and persist newly fetched coords back to the DB (use CityContrib identifier to
save lat/lon), and if you cannot complete caching in this change, add a progress
indicator in the client for the /api/map-data request to reflect ongoing
geocoding.
web/templates/components/header.html (1)

27-27: Consider adding accessible text for screen readers.

The emoji-only link 🗺️ lacks descriptive text for screen reader users. Consider adding a visually hidden label similar to how other sites handle icon-only navigation.

♿ Proposed accessibility improvement
-      <a href="/karte" {{ if eq .CurrentPath "/karte" }}class="nav-active"{{ end }}>🗺️</a>
+      <a href="/karte" {{ if eq .CurrentPath "/karte" }}class="nav-active"{{ end }}>🗺️<span class="sr-only">Karte</span></a>

Note: The static analysis warnings about HTML escaping and tag pairing are false positives—HTMLHint doesn't recognize Go template syntax.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/templates/components/header.html` at line 27, The emoji-only navigation
link in header.html (the anchor with href="/karte" and the template condition
checking .CurrentPath) is not accessible to screen readers; update that anchor
to include descriptive accessible text by adding either an aria-label="Map" (or
localized equivalent) or a visually hidden <span> (e.g., class="sr-only")
containing a short label like "Map" while keeping the emoji visible, ensuring
the template condition (if eq .CurrentPath "/karte") and any existing classes
remain unchanged.
web/templates/app/map.html (1)

9-9: Give the map region a label and non-JS fallback.

This is an empty mount node right now, so a failed Leaflet/init load leaves a blank box and assistive tech gets no name for the region. Add an aria-label/aria-labelledby and a small fallback such as loading text or a <noscript> message.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/templates/app/map.html` at line 9, The map mount node with id "map" and
class "leaflet-map-container" is currently an empty div which leaves a blank,
unlabeled region if Leaflet/JS fails; add an accessible label (aria-label or
aria-labelledby) to that div and include non-JS fallback content (e.g., a short
"Loading map…" text inside the div or a <noscript> message) so assistive tech
gets a name and users without JS see a message — ensure the JS that mounts the
Leaflet map still replaces or preserves the content appropriately (targeting the
"map" element).
web/static/style.css (2)

2047-2064: These selectors don't match the shipped markup.

web/templates/app/map.html uses .page-header and .page-title, but this block styles .map-hero, .map-hero-left, and .map-title. As written, those rules never affect the new page. Remove them or align the template/classes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/static/style.css` around lines 2047 - 2064, The CSS selectors .map-hero,
.map-hero-left, and .map-title do not match the shipped markup (.page-header and
.page-title); update the stylesheet so the rules apply to the live page by
either removing the unused .map-* rules or renaming them to the markup classes
(e.g., replace .map-hero with .page-header, .map-hero-left with the appropriate
page header child class, and .map-title with .page-title) and keep the same
properties, or alternatively update the template classes to .map-hero/.map-title
if you prefer to keep the CSS; ensure you change all occurrences of .map-hero,
.map-hero-left, and .map-title so styles take effect.

2240-2258: Add a :focus-visible state for the city cards.

These are primary navigation links on the new page, but only hover gets a custom state. Mirror the hover affordance on :focus-visible so keyboard navigation is obvious.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/static/style.css` around lines 2240 - 2258, The city card lacks a
keyboard-visible focus state; add a :focus-visible rule for .map-city-item that
mirrors the hover affordance by setting the same border-color (rgba(0, 0, 0,
0.18)) and box-shadow (var(--shadow-sm)) so keyboard users see the same visual
cue as hover; apply outline: none only if you replicate an equivalent visible
focus style, and keep the selector as .map-city-item:focus-visible to target
keyboard focus specifically.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/handlers/app/map.go`:
- Line 22: Replace uses of context.Background() passed to repository methods
with the incoming HTTP request context so DB queries cancel when the client
disconnects; specifically, change calls like
repository.GetCityContribCounts(context.Background()) (and the similar call at
the other occurrence) to use c.Request().Context() instead, ensuring you pass
the Echo handler's context (c) into GetCityContribCounts and any analogous
repository methods so cancellation/timeout propagation works correctly.
- Around line 54-55: The handler currently returns err.Error() in the JSON
response after calling sentryhelper.CaptureException(c, err), which can leak
sensitive internals; change the response body to a generic message (e.g.,
"internal server error" or "an internal error occurred") while keeping
sentryhelper.CaptureException(c, err) to report the full error internally, and
ensure the c.JSON call still returns http.StatusInternalServerError — update the
return in the code that builds the response map so it does not include
err.Error().

In `@internal/repository/map.go`:
- Around line 30-38: The code iterates over rows into []CityContrib using
rows.Next and rows.Scan but never checks rows.Err(), so iteration/scan/network
errors are swallowed; after the for rows.Next() loop (which fills result via
CityContrib and rows.Scan) call rows.Err(), and if non-nil return that error (or
wrap it) instead of returning the partial result and nil; update the function
that builds and returns result to propagate this error so callers can detect
iteration failures.

In `@src/lib/map.ts`:
- Around line 164-168: Update the Leaflet attribution URL to the correct domain:
replace "https://leaflet.js.com" with "https://leafletjs.com" in the L.tileLayer
attribution string (look for the L.tileLayer(...).addTo(map) call in this file)
so the attribution link points to the proper Leaflet website.
- Around line 76-81: The popup HTML interpolates unsanitized city.name into
marker.bindPopup, allowing XSS; change the popup creation to escape city.name
before interpolation (e.g., add a small helper escapeHtml that replaces & < > "
' / with entities and call it when building the string) or build the popup using
DOM APIs (createElement + textContent) and then set
marker.bindPopup(popupElement.outerHTML); update the code locations using
marker.bindPopup and ensure all uses of city.name in popup content go through
the escape helper or DOM text assignment.

In `@web/templates/app/map.html`:
- Line 29: The city count label currently always uses the plural "Beiträge";
update the template in map.html (the span with class "map-city-count" that
renders {{ .Count }}) to choose the correct German form by checking the count
(e.g., if .Count equals 1 render "Beitrag", otherwise "Beiträge") using the
template's conditional/if syntax so "1 Beitrag" displays correctly while all
other counts stay as "Beiträge".

---

Nitpick comments:
In `@src/lib/map.ts`:
- Around line 48-59: The geocoding loop in geocodeAll (which calls geocodeCity
and returns GeocodedCity[]) causes linear delays; modify the server-side flow to
cache coordinates for CityContrib entries: when handling /api/map-data, first
check the DB record for existing lat/lon and return those immediately; update
geocodeAll to skip API calls for cities with cached coords and persist newly
fetched coords back to the DB (use CityContrib identifier to save lat/lon), and
if you cannot complete caching in this change, add a progress indicator in the
client for the /api/map-data request to reflect ongoing geocoding.

In `@web/static/style.css`:
- Around line 2047-2064: The CSS selectors .map-hero, .map-hero-left, and
.map-title do not match the shipped markup (.page-header and .page-title);
update the stylesheet so the rules apply to the live page by either removing the
unused .map-* rules or renaming them to the markup classes (e.g., replace
.map-hero with .page-header, .map-hero-left with the appropriate page header
child class, and .map-title with .page-title) and keep the same properties, or
alternatively update the template classes to .map-hero/.map-title if you prefer
to keep the CSS; ensure you change all occurrences of .map-hero, .map-hero-left,
and .map-title so styles take effect.
- Around line 2240-2258: The city card lacks a keyboard-visible focus state; add
a :focus-visible rule for .map-city-item that mirrors the hover affordance by
setting the same border-color (rgba(0, 0, 0, 0.18)) and box-shadow
(var(--shadow-sm)) so keyboard users see the same visual cue as hover; apply
outline: none only if you replicate an equivalent visible focus style, and keep
the selector as .map-city-item:focus-visible to target keyboard focus
specifically.

In `@web/templates/app/map.html`:
- Line 9: The map mount node with id "map" and class "leaflet-map-container" is
currently an empty div which leaves a blank, unlabeled region if Leaflet/JS
fails; add an accessible label (aria-label or aria-labelledby) to that div and
include non-JS fallback content (e.g., a short "Loading map…" text inside the
div or a <noscript> message) so assistive tech gets a name and users without JS
see a message — ensure the JS that mounts the Leaflet map still replaces or
preserves the content appropriately (targeting the "map" element).

In `@web/templates/components/header.html`:
- Line 27: The emoji-only navigation link in header.html (the anchor with
href="/karte" and the template condition checking .CurrentPath) is not
accessible to screen readers; update that anchor to include descriptive
accessible text by adding either an aria-label="Map" (or localized equivalent)
or a visually hidden <span> (e.g., class="sr-only") containing a short label
like "Map" while keeping the emoji visible, ensuring the template condition (if
eq .CurrentPath "/karte") and any existing classes remain unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aae2c06f-99af-43ba-9f0c-38208432ef19

📥 Commits

Reviewing files that changed from the base of the PR and between a0d0d92 and 2c563f8.

📒 Files selected for processing (10)
  • internal/handlers/app/map.go
  • internal/handlers/routes.go
  • internal/repository/map.go
  • package.json
  • src/lib/map.ts
  • src/main.ts
  • src/styles.css
  • web/static/style.css
  • web/templates/app/map.html
  • web/templates/components/header.html

func MapHandler(c echo.Context) error {
stats := utils.GetFooterStats()

cities, err := repository.GetCityContribCounts(context.Background())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Use request context for proper cancellation propagation.

context.Background() won't cancel the DB query if the client disconnects. Use c.Request().Context() to propagate cancellation signals.

♻️ Proposed fix
 func MapHandler(c echo.Context) error {
 	stats := utils.GetFooterStats()

-	cities, err := repository.GetCityContribCounts(context.Background())
+	cities, err := repository.GetCityContribCounts(c.Request().Context())
 func MapDataHandler(c echo.Context) error {
-	cities, err := repository.GetCityContribCounts(context.Background())
+	cities, err := repository.GetCityContribCounts(c.Request().Context())

Also applies to: 52-52

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/handlers/app/map.go` at line 22, Replace uses of
context.Background() passed to repository methods with the incoming HTTP request
context so DB queries cancel when the client disconnects; specifically, change
calls like repository.GetCityContribCounts(context.Background()) (and the
similar call at the other occurrence) to use c.Request().Context() instead,
ensuring you pass the Echo handler's context (c) into GetCityContribCounts and
any analogous repository methods so cancellation/timeout propagation works
correctly.

Comment on lines +54 to +55
sentryhelper.CaptureException(c, err)
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid exposing internal error details in API response.

err.Error() may contain sensitive information (DB connection details, query errors). Return a generic message instead.

🛡️ Proposed fix
 	if err != nil {
 		sentryhelper.CaptureException(c, err)
-		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to load map data"})
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
sentryhelper.CaptureException(c, err)
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
if err != nil {
sentryhelper.CaptureException(c, err)
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to load map data"})
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/handlers/app/map.go` around lines 54 - 55, The handler currently
returns err.Error() in the JSON response after calling
sentryhelper.CaptureException(c, err), which can leak sensitive internals;
change the response body to a generic message (e.g., "internal server error" or
"an internal error occurred") while keeping sentryhelper.CaptureException(c,
err) to report the full error internally, and ensure the c.JSON call still
returns http.StatusInternalServerError — update the return in the code that
builds the response map so it does not include err.Error().

Comment on lines +30 to +38
var result []CityContrib
for rows.Next() {
var c CityContrib
if err := rows.Scan(&c.Name, &c.Count); err != nil {
continue
}
result = append(result, c)
}
return result, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Check rows.Err() after iteration to catch scan/network errors.

The loop may exit early due to iteration errors (e.g., connection loss). Without checking rows.Err(), these errors are silently ignored and partial data is returned as if successful.

🐛 Proposed fix
 	for rows.Next() {
 		var c CityContrib
 		if err := rows.Scan(&c.Name, &c.Count); err != nil {
 			continue
 		}
 		result = append(result, c)
 	}
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
 	return result, nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/repository/map.go` around lines 30 - 38, The code iterates over rows
into []CityContrib using rows.Next and rows.Scan but never checks rows.Err(), so
iteration/scan/network errors are swallowed; after the for rows.Next() loop
(which fills result via CityContrib and rows.Scan) call rows.Err(), and if
non-nil return that error (or wrap it) instead of returning the partial result
and nil; update the function that builds and returns result to propagate this
error so callers can detect iteration failures.

Comment thread src/lib/map.ts
Comment on lines +76 to +81
marker.bindPopup(`
<div class="map-popup">
<strong>${city.name}</strong>
<span>${city.count} Beitrag${city.count !== 1 ? "e" : ""}</span>
</div>
`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Sanitize city name to prevent XSS.

city.name originates from user-contributed data stored in the database. If a malicious city name like <img src=x onerror=alert(1)> is stored, it will be rendered unsanitized in the popup.

🛡️ Proposed fix using text escaping
+function escapeHtml(str: string): string {
+  const div = document.createElement("div");
+  div.textContent = str;
+  return div.innerHTML;
+}
+
 function buildMarker(city: GeocodedCity): L.CircleMarker {
   // ...
   marker.bindPopup(`
     <div class="map-popup">
-      <strong>${city.name}</strong>
+      <strong>${escapeHtml(city.name)}</strong>
       <span>${city.count} Beitrag${city.count !== 1 ? "e" : ""}</span>
     </div>
   `);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/map.ts` around lines 76 - 81, The popup HTML interpolates unsanitized
city.name into marker.bindPopup, allowing XSS; change the popup creation to
escape city.name before interpolation (e.g., add a small helper escapeHtml that
replaces & < > " ' / with entities and call it when building the string) or
build the popup using DOM APIs (createElement + textContent) and then set
marker.bindPopup(popupElement.outerHTML); update the code locations using
marker.bindPopup and ensure all uses of city.name in popup content go through
the escape helper or DOM text assignment.

Comment thread src/lib/map.ts
Comment on lines +164 to +168
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution:
'© <a href="https://leaflet.js.com">Leaflet</a> | © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 18,
}).addTo(map);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix typo in Leaflet attribution URL.

The URL leaflet.js.com should be leafletjs.com.

🔧 Proposed fix
   L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
     attribution:
-      '© <a href="https://leaflet.js.com">Leaflet</a> | © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
+      '© <a href="https://leafletjs.com">Leaflet</a> | © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
     maxZoom: 18,
   }).addTo(map);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/map.ts` around lines 164 - 168, Update the Leaflet attribution URL to
the correct domain: replace "https://leaflet.js.com" with
"https://leafletjs.com" in the L.tileLayer attribution string (look for the
L.tileLayer(...).addTo(map) call in this file) so the attribution link points to
the proper Leaflet website.

{{ range .Cities }}
<a href="/?city={{ .Name }}" class="map-city-item">
<span class="map-city-name">{{ .Name }}</span>
<span class="map-city-count">{{ .Count }} Beiträge</span>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Pluralize the contribution label.

1 Beiträge reads incorrectly in German. Switch the suffix for the singular case so the city list copy stays correct.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/templates/app/map.html` at line 29, The city count label currently always
uses the plural "Beiträge"; update the template in map.html (the span with class
"map-city-count" that renders {{ .Count }}) to choose the correct German form by
checking the count (e.g., if .Count equals 1 render "Beitrag", otherwise
"Beiträge") using the template's conditional/if syntax so "1 Beitrag" displays
correctly while all other counts stay as "Beiträge".

@dmnktoe dmnktoe left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Interaktive Leaflet-Karte

Die Struktur ist sauber (Repository → Handler → Template → TS-Modul folgt der bestehenden Aufteilung), aber es gibt zwei Punkte, die vor einem Merge weg müssen, und einen dritten, der die Karte in der Praxis unbenutzbar macht.

🔴 Blocker

1. XSS im Marker-Popup — src/lib/map.ts

marker.bindPopup(`
  <div class="map-popup">
    <strong>${city.name}</strong>

bindPopup mit einem String parst den Inhalt als HTML. city.name kommt aus contributions.user_city, und das ist ungefiltertes Nutzereingabe-Feld: in internal/handlers/app/upload.go:284 wird player_city nur durch strings.TrimSpace geschickt, es gibt keine serverseitige Whitelist gegen die Meilisearch-Städte. Die Autocomplete-Validierung in city-autocomplete.ts ist rein clientseitig und damit umgehbar.

Ein Beitrag mit dem Ort '&lt;img src=x onerror=…&gt;' führt also bei jeder Person aus, die /karte öffnet. Überall sonst geht der Ortsname durch html/template und wird escaped — dieser Pfad ist die einzige Ausnahme.

Fix: Popup per DOM aufbauen und textContent setzen, statt zu interpolieren. Dasselbe gilt für bindTooltip (dort ist es mit String(city.count) aktuell unkritisch, aber gleiche Klasse Problem).

2. echo/v4 — der Branch kompiliert gegen aktuelles main nicht

internal/handlers/app/map.go importiert github.com/labstack/echo/v4, main ist inzwischen auf echo/v5 v5.3.1. Der PR hängt an a0d0d923 vom 12. März; das package.json im Diff zeigt noch meilisearch ^0.55.0, swiper ^12.1.1 und @datadog/browser-rum — alles seitdem geändert. Bitte zuerst rebasen, sonst wird beim Merge versehentlich die halbe Dependency-Historie zurückgedreht.

🟠 Substanziell

3. Nominatim-Geocoding im Browser, bei jedem Seitenaufruf

geocodeAll läuft sequenziell mit 1,1 s Pause pro Stadt. Bei 40 Städten sind das ~45 Sekunden leere Karte — für jede Besucherin, bei jedem Aufruf, ohne Cache. Dazu kommt:

  • Nominatims Nutzungsbedingungen verbieten systematisches/bulk Geocoding ausdrücklich und verlangen, Ergebnisse zu cachen.
  • Der User-Agent-Header in geocodeCity ist ein forbidden header name — Browser verwerfen ihn stillschweigend. Die Requests gehen also unidentifiziert raus, und genau das blockt Nominatim.
  • Kein Timeout, kein AbortController.

Ihr habt die Koordinaten aber bereits: der Meilisearch-cities-Index liefert lat/lon (siehe CityHit in src/lib/city-autocomplete.ts). Wenn /api/map-data die Koordinaten serverseitig mitliefert — aus dem Index oder aus einer lat/lon-Spalte an contributions —, verschwinden Wartezeit, externe Abhängigkeit und Policy-Problem auf einen Schlag.

4. rows.Err() fehlt — internal/repository/map.go

GetCityContribCounts prüft nach der Schleife nicht rows.Err(). Alle iterierenden Funktionen in internal/repository/derive.go tun das. Ein Lesefehler mitten in der Iteration kommt aktuell als kurzes, erfolgreiches Ergebnis zurück.

5. Interne Fehler gehen an den Client — MapDataHandler

return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})

pgx-Fehler enthalten Tabellen-, Spalten- und teils Verbindungsdetails. Loggen + Sentry ja, aber nach außen eine generische Meldung.

🟡 Kleinigkeiten

  1. if (res.ok) cities = await res.json(); — bei einem 500er bleibt cities leer und die Seite zeigt „Noch keine Städte eingetragen." Ein Serverfehler sieht damit aus wie ein leerer Datenbestand. Der catch fängt nur Netzwerkfehler.

  2. Attribution-Link ist kaputt: https://leaflet.js.comhttps://leafletjs.com.

  3. Daten werden doppelt geholt. MapHandler rendert die Städteliste serverseitig, initMap holt dieselben Zeilen danach nochmal über /api/map-data. Entweder die Daten inline mitgeben (<script type="application/json">) und den Endpunkt streichen, oder umgekehrt.

  4. Die beiden Stat-Cards zählen unterschiedliche Grundmengen. „Städte" ist len .Cities (nur Beiträge mit nicht-leerem user_city), „Beiträge" ist FooterStats.TotalContributions (alle). Die Zahlen passen nicht zueinander.

  5. {{ .Count }} Beiträge ist immer Plural — „1 Beiträge". In map.ts ist das mit city.count !== 1 korrekt gelöst, im Template nicht.

  6. @import "leaflet/dist/leaflet.css" liegt zwischen swiper/css/navigation und swiper/css/pagination und zerteilt den Swiper-Block. Besser dahinter.

  7. Keine Tests, während die anderen src/lib-Module von 95 Tests abgedeckt sind. markerColor und der Fallback-Pfad von geocodeCity wären billig und lohnend.

Merge-Reihenfolge

#143, #144 und #145 hängen alle am selben Stand und hängen sich gegenseitig in die Quere: alle drei hängen unten an web/static/style.css an, #143 und #145 ändern beide header.html, #143 und #145 beide src/main.ts. Am wenigsten weh tut #145#144#143, jeweils mit Rebase dazwischen.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant