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
114 changes: 114 additions & 0 deletions .specify/specs/027-amenity-poi-types/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Implementation Plan: Playground & Restroom Amenity POI Types

> **Spec ID:** 027-amenity-poi-types
> **Status:** Planning
> **Last Updated:** 2026-05-27
> **Estimated Effort:** M

## Summary

Add two icon-table POI types (playground, restroom) that auto-classify and appear
in the legend; seed ~240 OSM-sourced amenity POIs that fall inside park
boundaries (committed snapshot + idempotent importer keyed on `osm_id`); and skip
those types in batch news/events collection via a configurable excluded-types
setting and a small backend POI classifier.

## Architecture

### Data flow — types & legend
`icons` table rows → public `GET /api/admin/icons` → `iconConfig` in App.jsx →
`Map.jsx` builds legend from enabled icons + `iconUtils.getDestinationIconTypeFromConfig`
classifies each POI by name keyword → activity fallback. Adding two rows is all
that's needed; no frontend code change.

### Data flow — OSM import
Overpass (`leisure=playground`, `amenity=toilets`, 2-county bbox) → filter to
points inside `boundary_type='park'` polygons (done during dev) → committed
`backend/data/osm/amenities.json` (240 features: osm_id, kind, lat, lon, osm_name,
park) → `import-osm-amenities.js` upserts point POIs by `osm_id`.

### Data flow — collection skip
`getAllPoisForCollection` / `getPoisForTierCollection` load
`news_collection_excluded_types` + icon config, classify each candidate via
`backend/utils/poiClassify.js`, and drop excluded types before returning ids.

## Implementation Steps

### Phase 1: Types, schema, icons
- [ ] `backend/migrations/066_add_amenity_poi_types.sql`: insert playground +
restroom icons; `ALTER TABLE pois ADD COLUMN osm_id` + unique partial index;
seed `news_collection_excluded_types` setting.
- [ ] `frontend/public/icons/playground.svg`, `restroom.svg` (32×32 badge style).

### Phase 2: OSM import
- [ ] `backend/data/osm/amenities.json` — committed snapshot (240 features). *(done)*
- [ ] `backend/migrations/import-osm-amenities.js` — read snapshot, build display
name (osm_name → `"<park> <Kind>"` with per-park collision suffix), upsert
point POIs by `osm_id` (poi_roles `{point}`, primary_activities, lat/lng,
navigation_*, brief_description, collection_tier `monthly`). Idempotent.

### Phase 3: Collection skip
- [ ] `backend/utils/poiClassify.js` — `classifyPoiType(name, primaryActivities, iconConfig)`
(port of the keyword→fallback core of `getDestinationIconTypeFromConfig`).
- [ ] Modify `getAllPoisForCollection` + `getPoisForTierCollection` in
`newsService.js` to drop excluded types (load setting + icon config once).

### Phase 4: Tests
- [ ] Unit test `poiClassify` (playground/restroom/other) and that the excluded
set removes amenity POIs from selection.

## File Changes

### New Files
| File | Purpose |
|------|---------|
| `backend/migrations/066_add_amenity_poi_types.sql` | icons + osm_id column + setting |
| `backend/migrations/import-osm-amenities.js` | idempotent OSM POI importer |
| `backend/data/osm/amenities.json` | committed 240-feature OSM snapshot |
| `backend/utils/poiClassify.js` | backend POI type classifier |
| `frontend/public/icons/playground.svg`, `restroom.svg` | type icons |
| `backend/tests/poiClassify.test.js` | unit tests |

### Modified Files
| File | Changes |
|------|---------|
| `backend/services/newsService.js` | excluded-type filtering in the two selection helpers |

## Database Migrations

`066_add_amenity_poi_types.sql` — idempotent (ON CONFLICT / IF NOT EXISTS),
re-runs safely every deploy. The OSM POI rows are loaded by the importer
(`node /app/migrations/import-osm-amenities.js`), run once at deploy after the
image is pulled — same manual pattern as `load-county-state-boundaries.js`.

## Testing Strategy

### Manual
1. Map legend shows Playground + Restroom toggles with icons; toggling filters markers.
2. Click a park (e.g. Rocky River Reservation) → playground/restroom pins inside it.
3. Amenity POI sidebar shows no News/Events tabs.
4. Trigger a batch collection dry-run / inspect selected ids → no amenity POIs.
5. Regular POIs still collect.

### Build / Gates
- [ ] `./run.sh build` passes
- [ ] `./run.sh test` (new unit test) — run by /deploy after merge
- [ ] gourmand `--full .` clean
- [ ] Gatehouse review clean

## Rollback Plan
1. Revert the PR. The `osm_id` column and seeded icons are additive and harmless;
amenity POIs can be soft-deleted by `osm_id` if needed.

## Risks and Mitigations
| Risk | Impact | Mitigation |
|------|--------|------------|
| Duplicate POIs on re-import | Med | Upsert keyed on unique `osm_id` |
| Misclassification skips a real POI from collection | Med | Classify against full icon config; excluded set is narrow + admin-configurable |
| OSM names mostly missing | Low | Fall back to `"<park> <Kind>"` with collision suffix |
| Stale OSM data | Low | Snapshot committed + dated; refetch to refresh |

## Changelog
| Date | Changes |
|------|---------|
| 2026-05-27 | Initial plan |
121 changes: 121 additions & 0 deletions .specify/specs/027-amenity-poi-types/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Specification: Playground & Restroom Amenity POI Types

> **Spec ID:** 027-amenity-poi-types
> **Status:** Draft
> **Version:** 0.1.0
> **Author:** Scott McCarty
> **Date:** 2026-05-27

## Overview

Parents want to find parks with good playgrounds and restrooms. This adds
**Playground** and **Restroom** as first-class map POI types (filterable legend
entries), seeds real locations inside our park boundaries from OpenStreetMap, and
excludes these amenity types from news/events collection (they have no news and
collecting for them wastes API budget).

Closes #418.

## User Stories

### Find amenities

**US-027-1: Filter the map for playgrounds & restrooms**
> As a parent, I want Playground and Restroom to appear as their own toggleable
> types in the map legend so I can see which parks have them.

Acceptance Criteria:
- [ ] `playground` and `restroom` appear as POI types in the legend with distinct icons.
- [ ] Toggling them shows/hides those markers like any other type.
- [ ] A POI named "… Playground"/"… Restroom" (or with `primary_activities`
Playground/Restroom) renders with the correct icon.
- [ ] Works for anonymous visitors (legend reads public `/api/admin/icons`).

**US-027-2: Real amenity locations inside parks**
> As a parent, I want actual playground and restroom locations shown inside the
> parks, sourced from OpenStreetMap.

Acceptance Criteria:
- [ ] Playgrounds (`leisure=playground`) and restrooms (`amenity=toilets`) that
fall inside an existing **park** boundary are imported as point POIs
(~240 features across 42 parks).
- [ ] Each is named from its OSM `name` tag, else `"<Park> Playground/Restroom"`
(numeric suffix on collision within a park).
- [ ] Import is idempotent — re-running creates no duplicates (keyed on OSM id).

### Don't waste collection budget

**US-027-3: Skip news/events collection for amenity types**
> As an operator, I want news/events collection to skip playground and restroom
> POIs so we don't spend API budget on POIs that never have news.

Acceptance Criteria:
- [ ] Batch collection (all-POIs and per-tier) excludes POIs classified as an
excluded type.
- [ ] The excluded type set is admin-configurable (`news_collection_excluded_types`),
defaulting to `["playground","restroom"]`.
- [ ] News/Events tabs naturally stay hidden for these POIs (0 counts).

## Data Model

### Schema Changes

```sql
-- Provenance + idempotency key for OSM-sourced POIs
ALTER TABLE pois ADD COLUMN IF NOT EXISTS osm_id TEXT;
CREATE UNIQUE INDEX IF NOT EXISTS idx_pois_osm_id ON pois (osm_id) WHERE osm_id IS NOT NULL;

-- New map types (filterable legend entries, keyword/activity classified)
INSERT INTO icons (name, label, svg_filename, title_keywords, activity_fallbacks, sort_order) VALUES
('playground', 'Playground', 'playground.svg', 'playground,play area', 'Playground', 19),
('restroom', 'Restroom', 'restroom.svg', 'restroom,restrooms,bathroom,toilet,toilets', 'Restroom', 20)
ON CONFLICT (name) DO NOTHING;

-- Default excluded-from-collection types
INSERT INTO admin_settings (key, value) VALUES
('news_collection_excluded_types', '["playground","restroom"]')
ON CONFLICT (key) DO NOTHING;
```

`pois.geom` and the icons/legend system are otherwise unchanged.

## API Endpoints

No new endpoints. Existing `/api/admin/icons` (public GET) surfaces the new types
to the map legend automatically.

## UI/UX Requirements

No new components. The legend is generated from `iconConfig`, so the two new icon
rows appear and filter automatically. Two new 32×32 SVGs in
`frontend/public/icons/` (`playground.svg`, `restroom.svg`) matching the existing
circle-badge style.

## Non-Functional Requirements

**NFR-027-1: Idempotent import** — re-running the OSM importer (or re-deploying)
must not duplicate POIs; keyed on `osm_id`.

**NFR-027-2: Reproducible data** — the OSM snapshot is committed
(`backend/data/osm/amenities.json`); the importer is deterministic and needs no
network at deploy time.

**NFR-027-3: No regression** — non-amenity POIs collect news/events exactly as
before; only excluded types are dropped from batch selection.

## Dependencies

- Builds on the icons/legend system (migration 065) and park boundaries (#198, 044).
- OSM data: Overpass API `leisure=playground` + `amenity=toilets`, filtered to
features inside `boundary_type='park'` polygons.

## Open Questions

None — model (icons-table types), collection skip (by type), and data scope
(OSM import, park-contained) resolved.

## Changelog

| Version | Date | Changes |
|---------|------|---------|
| 0.1.0 | 2026-05-27 | Initial draft |
Loading
Loading