From 0ee4daaed18249b4412cd5a84bbb9fc453106f1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Fri, 12 Jun 2026 22:53:14 +0200 Subject: [PATCH] fix: restore list map previews (batch geojson query) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Map previews were missing on every list view (activities, routes, home, profile, feed): the batch geojson helpers used `WHERE id = ANY(${ids}::text[])`, but drizzle expands a JS array in a sql template to `($1,$2,...)`, producing the invalid `ANY((...)::text[])`. That throws, and the helpers' `try/catch` swallowed it — so every card fell back to "No map preview". Rewrite getSimplifiedActivityGeojsonBatch (activities) and getSimplifiedGeojsonBatch (routes) to use the query builder: `inArray(table.id, ids)` for the id list + a raw `ST_AsGeoJSON(ST_Simplify(geom, 0.001))` select column. Same result, valid SQL. E2E: list-map-previews.test.ts creates an activity with geometry and asserts a Leaflet preview renders on /activities (no "No map preview"). Registered in playwright.config. typecheck + lint + unit (journal 315) green; verified in the browser. Co-Authored-By: Claude Opus 4.8 --- apps/journal/app/lib/activities.server.ts | 19 ++++++++------ apps/journal/app/lib/routes.server.ts | 19 ++++++++------ e2e/list-map-previews.test.ts | 30 +++++++++++++++++++++++ playwright.config.ts | 8 ++++++ 4 files changed, 62 insertions(+), 14 deletions(-) create mode 100644 e2e/list-map-previews.test.ts diff --git a/apps/journal/app/lib/activities.server.ts b/apps/journal/app/lib/activities.server.ts index 4058326..2848c61 100644 --- a/apps/journal/app/lib/activities.server.ts +++ b/apps/journal/app/lib/activities.server.ts @@ -1,5 +1,5 @@ import { randomUUID } from "node:crypto"; -import { eq, desc, and, isNotNull, sql } from "drizzle-orm"; +import { eq, desc, and, isNotNull, inArray, sql } from "drizzle-orm"; import { unionAll } from "drizzle-orm/pg-core"; import { getDb } from "./db.ts"; import { activities, routes, syncImports, users, follows, remoteActors } from "@trails-cool/db/schema/journal"; @@ -376,12 +376,17 @@ async function getSimplifiedActivityGeojsonBatch(ids: string[]): Promise) { + // Use the query builder for the id list: a raw `ANY(${ids}::text[])` + // makes drizzle expand the array to `($1,$2,...)`, yielding the invalid + // `ANY((...)::text[])` — which throws and silently drops every preview. + const rows = await db + .select({ + id: activities.id, + geojson: sql`ST_AsGeoJSON(ST_Simplify(${activities.geom}, 0.001))`, + }) + .from(activities) + .where(and(inArray(activities.id, ids), isNotNull(activities.geom))); + for (const row of rows) { if (row.geojson) map.set(row.id, row.geojson); } } catch { diff --git a/apps/journal/app/lib/routes.server.ts b/apps/journal/app/lib/routes.server.ts index 91ed3f3..406113e 100644 --- a/apps/journal/app/lib/routes.server.ts +++ b/apps/journal/app/lib/routes.server.ts @@ -1,5 +1,5 @@ import { randomUUID } from "node:crypto"; -import { eq, desc, and } from "drizzle-orm"; +import { eq, desc, and, inArray, isNotNull } from "drizzle-orm"; import { getDb } from "./db.ts"; import { routes, routeVersions } from "@trails-cool/db/schema/journal"; import type { Visibility } from "@trails-cool/db/schema/journal"; @@ -209,12 +209,17 @@ async function getSimplifiedGeojsonBatch(ids: string[]): Promise) { + // Query-builder id list: a raw `ANY(${ids}::text[])` makes drizzle expand + // the array to `($1,$2,...)`, yielding the invalid `ANY((...)::text[])` + // — it throws and silently drops every route preview. + const rows = await db + .select({ + id: routes.id, + geojson: sql`ST_AsGeoJSON(ST_Simplify(${routes.geom}, 0.001))`, + }) + .from(routes) + .where(and(inArray(routes.id, ids), isNotNull(routes.geom))); + for (const row of rows) { if (row.geojson) map.set(row.id, row.geojson); } } catch { diff --git a/e2e/list-map-previews.test.ts b/e2e/list-map-previews.test.ts new file mode 100644 index 0000000..254cd4e --- /dev/null +++ b/e2e/list-map-previews.test.ts @@ -0,0 +1,30 @@ +import { test, expect, gotoHydrated } from "./fixtures/test"; +import { setupVirtualAuthenticator, registerUser } from "./helpers/auth"; + +// A GPX fixture with geometry → the activity gets a PostGIS geom, so its list +// card should render a Leaflet map preview. Guards the batch-geojson query +// (a malformed `ANY(${ids}::text[])` previously threw and was swallowed, +// silently dropping every list preview). +const GPX = "packages/fit/__fixtures__/alpine.gpx"; + +test.describe("List map previews", () => { + test("activities list renders a map preview for an activity with geometry", async ({ page }) => { + const cdp = await page.context().newCDPSession(page); + await setupVirtualAuthenticator(cdp); + + const stamp = Date.now(); + await registerUser(page, `lmp-${stamp}@example.com`, `lmp${stamp}`); + + await gotoHydrated(page, "/activities/new"); + await page.getByLabel("Name").fill("Preview check"); + await page.locator('input[name="gpx"]').setInputFiles(GPX); + await page.getByRole("button", { name: "Create Activity" }).click(); + await expect(page).toHaveURL(/\/activities\/[0-9a-f-]+$/, { timeout: 10000 }); + + await gotoHydrated(page, "/activities"); + // The Leaflet container mounts after the client-only dynamic import; no + // "No map preview" fallback should remain. + await expect(page.locator(".leaflet-container").first()).toBeVisible({ timeout: 15000 }); + await expect(page.getByText("No map preview")).toHaveCount(0); + }); +}); diff --git a/playwright.config.ts b/playwright.config.ts index ba4a5ad..14dd779 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -130,6 +130,14 @@ export default defineConfig({ baseURL: "http://localhost:3000", }, }, + { + name: "list-map-previews", + testMatch: "list-map-previews.test.ts", + use: { + ...devices["Desktop Chrome"], + baseURL: "http://localhost:3000", + }, + }, ], webServer: process.env.CI ? [