Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ Never commit real values to source control. `.env.local` is already ignored by G
| `npm run seed:venues` | Seed sample venue data (developer script) |
| `npm run bootstrap-superadmin` | Grant the initial superadmin role to `SUPER_ADMIN_EMAIL` (see docs first) |
| `npm run prune:trade-leads` | Prune trade inquiries past the 24-month retention window (dry-run; `-- --delete` executes) |
| `npm run migrate:venue-islands` | Backfill the canonical venue `island` field from legacy `locationName` values (dry-run; `-- --write` executes) |

## Deployment overview

Expand Down
7 changes: 3 additions & 4 deletions docs/TECHNICAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ client SDK writes (content management) or through Admin-SDK-backed API routes
| `lib/` | Shared logic. Client-safe: `firebase.ts`, `beers.ts`, `venues.ts`, `analytics.ts`, `types.ts`, `utils.ts`, `email.ts`, `trade-leads-common.ts`, admin `*-common`/`admin-format.ts` helpers. Server-only (`import "server-only"`): `firebase-admin.ts`, `admin-auth.ts`, `admin-users.ts`, `admin-invitations.ts`, `admin-invitation-email.ts`, `admin-invitation-resend-core.ts`, `admin-audit.ts`, `trade-leads.ts`. Policy/serialization helpers shared by both: `admin-policy.ts`, `admin-serializers.ts`, `admin-invitation-policy.ts`, `admin-invitation-resend-policy.ts`, `admin-types.ts`. |
| `tests/` | Node `node:test` unit tests (`tsx` loader) for admin/auth/invitation/audit helpers and for the *contents* of `firestore.rules` and `storage.rules`. |
| `rules-tests/` | Emulator-backed security-rules tests (`@firebase/rules-unit-testing` against the Firestore/Storage emulators). Run via `npm run test:rules`, which wraps `firebase emulators:exec`; each file uses its own `demo-*` project so parallel `node:test` files stay isolated. |
| `scripts/` | Local/manual tooling: Playwright diagnostics (`*-check.mjs`, `hero-video-network.mjs`), `check-md-links.mjs`, `check-react-versions.mjs`, `optimize-assets.mjs`, `bootstrap-superadmin.ts`, `prune-trade-leads.ts`, `migrate-venue-islands.ts`, `seed-beers.ts`, `seed-venues.ts`. `check-md-links.mjs` and `check-react-versions.mjs` run in CI; the Playwright diagnostics and data scripts do not (CI browser coverage lives in `smoke-tests/`). |
| `scripts/` | Local/manual tooling: Playwright diagnostics (`*-check.mjs`, `hero-video-network.mjs`), `check-md-links.mjs`, `check-react-versions.mjs`, `optimize-assets.mjs`, `bootstrap-superadmin.ts`, `prune-trade-leads.ts`, `seed-beers.ts`, `seed-venues.ts`. `check-md-links.mjs` and `check-react-versions.mjs` run in CI; the Playwright diagnostics and data scripts do not (CI browser coverage lives in `smoke-tests/`). |
| `docs/` | Admin handbook (`docs/admin/`), operations guides (`docs/operations/`: deployment, troubleshooting, post-deploy checklist), and this file. |
| `content/` | Legacy placeholder (`.gitkeep` only). MDX content is co-located under `app/(pages)/`; do not add files here expecting them to render. |
| `firestore.rules`, `storage.rules` | Firebase security rules — see §6/§15. |
Expand Down Expand Up @@ -209,8 +209,7 @@ in code are listed.
`tapBeerSlugs[]`/`canBeerSlugs[]`, `isPublic`, `sortOrder`, `links`
(`website`/`maps`/`instagram`/`facebook`/`untappd`), `notesPublic`.
Issue #134 split island identity out of `locationName`; reads fall back
to parsing `locationName` for legacy records until the
`migrate:venue-islands` backfill completes.
to parsing `locationName` for legacy documents predating the field.
- **Reads:** `/where-to-buy` via `getVenues()` (`isPublic` + `sortOrder`);
rules allow public reads only of `isPublic` docs; admin dashboard reads all.
- **Writes:** admin dashboard `setDoc` merge keyed by `slug` (client SDK).
Expand Down Expand Up @@ -747,7 +746,7 @@ placeholder values exist anywhere in CI.
- **Local-only scripts (`scripts/`):** Playwright-based manual diagnostics
(`screenshot-check`, `overflow-check`, `hero-video-*`),
`optimize-assets.mjs`, and Admin-SDK utilities (`bootstrap-superadmin.ts`,
`prune-trade-leads.ts`, `migrate-venue-islands.ts`, `seed-beers.ts`,
`prune-trade-leads.ts`, `seed-beers.ts`,
`seed-venues.ts`). These remain
manual/local; the CI smoke suite lives in `smoke-tests/`.
- **Verification parity:** local pre-PR checks are the same commands CI
Expand Down
2 changes: 1 addition & 1 deletion docs/admin/managing-locations.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Island is required; a venue cannot be saved without one, and free-text island na

**Location / locality** is a separate, optional free-text field for a more specific place — `Windwardside`, `The Bottom`, `Fort Bay`, `Philipsburg`. It is shown on the venue card together with the island (for example `Windwardside, Saba` or `Philipsburg, Sint Maarten`) but never affects grouping or the Island filter. Leave it blank when the island alone is enough — the card then shows just the island label.

> **Why two fields?** Locality text used to double as the island, which let values like `Philipsburg` or `Windwardside` accidentally become their own public island group. Older records were migrated so `locationName` holds only the locality; reads still understand the old combined format until every record is migrated, so nothing breaks in between.
> **Why two fields?** Locality text used to double as the island, which let values like `Philipsburg` or `Windwardside` accidentally become their own public island group. Existing records now carry a separate canonical island and `locationName` holds only the locality; reads still understand the old combined format, so a stale record would not break the site.

## How locations appear on `/where-to-buy`

Expand Down
2 changes: 1 addition & 1 deletion lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface Venue {
locationName: string;
// Canonical island key ("saba" | "sxm" | "statia"). Optional only because
// legacy documents predate the field; reads fall back to parsing
// `locationName` until migration completes (see lib/venue-filters.ts).
// `locationName` for those records (see lib/venue-filters.ts).
island?: VenueIsland;
carriesBeerSlugs: string[];
tapBeerSlugs?: string[];
Expand Down
56 changes: 2 additions & 54 deletions lib/venue-filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,7 @@ export function islandKey(locationName: string | undefined): string {
* Canonical island key for a venue: the stored `island` field when present,
* falling back to legacy `locationName` inference for records written before
* Issue #134. New writes always set `island`, so grouping no longer depends
* on parsing locality text — the fallback exists only until migration
* completes.
* on parsing locality text — the fallback remains for legacy records.
*/
export function venueIslandKey(venue: Venue): string {
return isVenueIsland(venue.island)
Expand All @@ -134,57 +133,6 @@ export function resolveVenueIsland(venue: Venue): VenueIsland | undefined {
return isVenueIsland(legacy) ? legacy : undefined;
}

/**
* Known-locality → island map for the venue migration (Issue #134). Only
* entries that are unambiguous within the brewery's operating region belong
* here — a bare locality string can never reach this map through inference.
* "Oranjestad" is deliberately absent: it is also the capital of Aruba, so a
* bare "Oranjestad" record stays unresolved and is reported for owner review
* rather than guessed.
*/
const KNOWN_LOCALITY_ISLANDS: ReadonlyMap<string, VenueIsland> = new Map([
["windwardside", "saba"],
["the bottom", "saba"],
["fort bay", "saba"],
["philipsburg", "sxm"],
]);

export interface VenueGeography {
island: VenueIsland;
locality: string;
}

/**
* Split a legacy `locationName` into canonical `{ island, locality }` for
* the Issue #134 backfill. "<locality>, <island>" forms keep their locality
* prefix; a bare island name ("Saba", "SXM", "Sint Maarten") yields an empty
* locality; a bare locality is mapped only via KNOWN_LOCALITY_ISLANDS.
* Returns null when the value cannot be classified confidently — callers
* must report it for owner review instead of writing a guess.
*/
export function resolveVenueGeography(
locationName: string | undefined
): VenueGeography | null {
const parts = (locationName ?? "")
.split(",")
.map((part) => part.trim())
.filter((part) => part !== "");

// The legacy Saba default for an empty location, preserved verbatim.
if (parts.length === 0) return { island: "saba", locality: "" };

const lastKey = islandKey(parts[parts.length - 1]);
if (isVenueIsland(lastKey)) {
return { island: lastKey, locality: parts.slice(0, -1).join(", ") };
}

const whole = parts.join(", ");
const known = KNOWN_LOCALITY_ISLANDS.get(whole.toLowerCase());
if (known) return { island: known, locality: whole };

return null;
}

/**
* Display heading for a canonical island key. Known islands return their
* canonical label; an unknown future island key title-cases each word
Expand Down Expand Up @@ -240,7 +188,7 @@ export function distinctIslands(venues: Venue[]): string[] {
* the canonical `island` field means a locality like "Philipsburg" or
* "Windwardside" can never become its own section (Issue #134); the legacy
* `locationName` fallback inside `venueIslandKey` still merges spelling
* variants like "SXM" and "Sint Maarten" for unmigrated records.
* variants like "SXM" and "Sint Maarten" for records predating the field.
*/
export function groupVenuesByIsland(
venues: Venue[]
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@
"seed:venues": "tsx scripts/seed-venues.ts",
"optimize-assets": "node scripts/optimize-assets.mjs",
"bootstrap-superadmin": "node --env-file=.env.local --import tsx scripts/bootstrap-superadmin.ts",
"prune:trade-leads": "node --env-file=.env.local --import tsx scripts/prune-trade-leads.ts",
"migrate:venue-islands": "node --env-file=.env.local --import tsx scripts/migrate-venue-islands.ts"
"prune:trade-leads": "node --env-file=.env.local --import tsx scripts/prune-trade-leads.ts"
},
"dependencies": {
"@mdx-js/loader": "^3.1.1",
Expand Down
190 changes: 0 additions & 190 deletions scripts/migrate-venue-islands.ts

This file was deleted.

Loading
Loading