# 02 — Feature Specification ## 1. Scope and conventions This spec defines the functional scope of Open Kite Map v1.0 and its release phasing. Each feature has an ID (F1…), a priority (**P0** = MVP-blocking, **P1** = v1.0, **P2** = post-1.0), user stories, behavior spec, and acceptance criteria. Non-functional requirements (NFRs) follow in §4. Release phases: - **MVP (0.x):** All P0 features. Read-mostly: browse spots, see conditions and scores, view forecasts. Spot data seeded by maintainers + OSM import. - **v1.0:** All P1 features. Community editing, accounts, condition reports, comparison view. - **Post-1.0 (P2):** Alerts, offline mode, tide integration, i18n beyond en/es/de. --- ## 2. Feature catalog ### F1 — Interactive spot map (P0) The home screen: a full-viewport MapLibre map showing kite spots as score-badged markers. **User stories** - As any flyer, I open the app and see spots near my location with a color-coded flyability badge, so I can pick a destination in seconds. - As a traveler, I can pan/search anywhere in the world and see spots there. **Behavior** 1. On load: request browser geolocation. If granted, center map at user location, zoom 11. If denied/unavailable, center on last-viewed location (localStorage) or fall back to IP-region centroid at zoom 6; show a dismissible "enable location" hint. 2. Spot markers render as a circular badge: score number (0–100) on a color from the score scale (§F5), with a small kite-type-relevant icon. Marker size scales with zoom. 3. Clustering: at zoom < 10, spots cluster (MapLibre cluster source); cluster badge shows count and the *best* score in the cluster, colored accordingly — "there's something good in here." 4. Viewport-driven data loading: map move triggers a debounced (300 ms) `GET /spots?bbox=…` call. Responses cached client-side per tile-aligned bbox for 60 s. 5. Scores on markers reflect **current conditions** for the active kite-type profile (§F5). Switching profile re-colors all visible markers without refetching weather (score computed client-side from cached condition data; see architecture doc §5). 6. Tapping a marker opens the spot detail panel (§F2 view). Tapping a cluster zooms into it. 7. Map controls: zoom, compass/reset-north, geolocate, layer toggle (street/satellite/terrain — all from open tile sources), and a wind-overlay toggle (P1, see F11). 8. Search box: geocoding via Nominatim (place names) and spot-name search via our API, merged in one result list, debounced 400 ms, max 1 req/s to Nominatim per its usage policy. **Acceptance criteria** - [ ] Cold load to interactive map with markers in < 3 s on a mid-range phone over 4G (see NFR-P1). - [ ] Panning across a region never fires more than 1 spots request per 300 ms. - [ ] With geolocation denied, the app remains fully usable. - [ ] Profile switch updates all marker colors in < 200 ms with no network round-trip when conditions are cached. - [ ] OSM attribution and weather-data attribution are always visible per license requirements. --- ### F2 — Spot detail (P0) Everything about one spot: conditions now, score breakdown, metadata, hazards, forecast entry point. **User stories** - As Maya, I tap a park and immediately see "Good for light kites now" plus warnings about trees. - As Inés, I check a coastal spot's shore orientation and see an explicit offshore-wind danger banner when applicable. **Behavior** 1. Layout: bottom sheet on mobile (peek → half → full), side panel on desktop. Described in wireframes doc §3. 2. **Header:** spot name, type chips (beach / park / field / water / hill), distance from user, favorite star (P1, requires account). 3. **Now block:** Flyability Score badge with one-line plain-language summary ("Great — steady 11 mph for stunt kites"); wind speed, gust, direction (arrow + cardinal + degrees); temperature; precipitation state. Data age shown ("updated 6 min ago"). 4. **Score breakdown (expandable):** each rubric factor with its contribution — e.g., "Wind speed 11 mph: in ideal band ✓ / Gust ratio 1.6: −15 / Rain: none ✓ / Obstacle penalty: −5 (tree line W)". Implements principle P1 (explainability). 5. **Safety banner:** rendered above the fold whenever any hard-block condition (§ scoring rubric §6) is active: offshore wind at water spots, thunderstorm risk, wind above hard ceiling. Red, iconed, not dismissible while condition persists. 6. **Spot info:** description, surface, size class, shore orientation (water spots), launch notes, hazards list (typed: power lines, trees, airport proximity, crowds, water hazards, restricted airspace), legality note + source link, photos (P1), parking/access notes. 7. **Forecast strip:** next 12 h as hourly mini-bars colored by score; tap → forecast panel (F6). 8. **Community block (P1):** latest condition reports (F9), spot reviews, "edit this spot" link (F8). 9. Deep-linkable: `/spot/{slug}` URL, server-rendered meta tags for link previews. **Acceptance criteria** - [ ] All P0 blocks render from a single `GET /spots/{id}` + `GET /spots/{id}/conditions` pair. - [ ] Safety banner appears in 100% of hard-block conditions (unit-tested against rubric fixtures). - [ ] Score breakdown factors sum exactly to the displayed score. - [ ] Panel is fully navigable with keyboard and screen reader (NFR-A11Y). --- ### F3 — Live conditions engine (P0) Backend capability: fetch, cache, and serve current wind/weather per spot. **Behavior** 1. Conditions come from the weather provider abstraction (data-sources doc §5): Open-Meteo primary; NOAA NWS for US spots when configured; provider recorded on every reading. 2. **Cache policy:** conditions per spot cached 10 min (current) / 60 min (hourly forecast) / 6 h (daily forecast). Spots within 0.05° (~5 km) share a weather cell to minimize upstream calls (Open-Meteo model resolution is ≥ ~1–11 km depending on model; sub-cell differences are below model resolution). 3. Served fields: wind speed (10 m), gusts, direction, temperature, precipitation (current + probability), cloud cover, weather code, and computed gust ratio. 4. Staleness contract: API responses carry `observed_at` and `fetched_at`; the client must show data age and degrade the score's confidence indicator when current data is > 30 min old. 5. Upstream failure handling: serve stale cache up to 2 h with `stale: true` flag; beyond that return `conditions_unavailable` and the UI shows spots gray (unknown), never a fake score. **Acceptance criteria** - [ ] No more than 1 upstream call per weather cell per 10 min regardless of traffic. - [ ] Provider outage degrades gracefully per the staleness contract (integration-tested with a mock provider). - [ ] Every response names its provider and model for attribution and debugging. --- ### F4 — Flyability Score display & plain-language summaries (P0) The score itself is specified in the rubric doc; this feature covers its presentation. **Behavior** 1. Score 0–100 mapped to five labels/colors: 80–100 **Excellent** (green), 60–79 **Good** (light green), 40–59 **Marginal** (yellow), 15–39 **Poor** (orange), 0–14 **No-fly** (red). Gray = unknown. 2. Plain-language generator: template-based one-liner combining dominant factor + band, e.g. "Marginal — wind is light (5 mph) for power kites", "No-fly — offshore wind: dangerous at this spot". Templates enumerated per rubric factor; localized strings. 3. Confidence indicator (dot: solid/hollow) reflecting data age and forecast horizon. 4. The same score pipeline runs identically server-side (for API consumers, alerts) and client-side (for instant profile switching); the rubric is published as versioned constants so both stay in sync, and the API exposes `rubric_version`. **Acceptance criteria** - [ ] Identical inputs produce identical scores client- and server-side (shared test-vector file in repo). - [ ] Color scale meets WCAG AA contrast for badge text. - [ ] Every label string exists for en, es, de. --- ### F5 — Kite-type profiles (P0) **Behavior** 1. Four built-in profiles: **Single-line / casual**, **Stunt / sport (dual & quad line)**, **Power / traction (land)**, **Kiteboarding (water)**. Bands per the rubric doc §3. 2. Profile selector lives in the map header; persists in localStorage (and account settings at P1). Default: single-line. 3. Profile affects: marker scores/colors, summaries, forecast coloring, "best window" computation, and which safety logic applies (offshore rules engage for water profiles at water spots; over-wind ceilings differ). 4. Custom profiles (P2): user-defined wind band and gust tolerance, e.g. "ultralight stunt 4–10 mph". **Acceptance criteria** - [ ] Profile persists across sessions without an account. - [ ] Switching profiles is instant (< 200 ms) on cached data. --- ### F6 — Forecast panel (P0) **User stories** - As Maya, I see "Best time: Saturday 2–5 pm" for the weekend. - As Dev, I read an hourly graph with mean and gust bands and my kite's window overlaid. - As Inés, I scan 7 days for side-onshore days in range and see which model said so. **Behavior** 1. Opens from spot detail or via `/spot/{slug}/forecast`. Layout per wireframes doc §4. 2. **Day strip:** 7 day cards (date, icon, min–max wind, dominant direction arrow, day-level score = best 3-h window score that day). Selecting a day drives the hourly view. 3. **Hourly graph (48 h visible, scrollable to 168 h):** wind mean line + gust line with shaded band between; background columns colored by hourly flyability score; the active profile's ideal band drawn as a horizontal reference zone; direction arrows along the axis every 3 h; precipitation bars on a secondary axis below. 4. **Best-window callouts:** top 3 contiguous windows ≥ 2 h with score ≥ 60 in the next 72 h, phrased plainly ("Sat 14:00–17:00 — Excellent, 12–14 mph NW"). 5. Model attribution footer ("Open-Meteo: best-match (ICON/GFS)"), forecast issued time, and a confidence note that decays with horizon (days 1–3 solid, 4–5 hollow, 6–7 "low confidence"). 6. All times in the spot's local timezone, labeled. **Acceptance criteria** - [ ] Hourly graph renders 168 h of data with 60 fps pan on a mid-range phone (canvas-based rendering). - [ ] Best-window algorithm is pure, deterministic, and covered by test vectors. - [ ] Day-level score equals max rolling-3 h-window mean of hourly scores (documented formula). --- ### F7 — Spot comparison (P1) Compare up to 4 spots side-by-side: current score, next-24 h sparkline, distance, drive radius hint. Entry: long-press/checkbox on markers or saved-spots list. Primarily serves Dev's "which of my five spots" job. **Acceptance criteria:** comparison view reachable in ≤ 3 taps from map; shares a URL (`/compare?spots=a,b,c`). --- ### F8 — Community spot editing (P1) 1. Account holders can create spots and edit any spot's metadata via structured forms (no free-form geometry editing in v1: location is a point + optional flying-area polygon drawn on map). 2. Every edit creates an immutable revision (who/when/what diff); spot pages show history; any revision can be reverted by moderators or the original author within 24 h. 3. New spots and edits by users with < 3 accepted edits enter a moderation queue; trusted users' edits go live immediately (flagged for retro-review). Anyone can "report" a spot. 4. Imported OSM-derived seeds (data-sources doc §6) are tagged with provenance and ODbL attribution. 5. Hard rule: hazard fields can be *added* by anyone instantly but *removed* only via moderation — safety information is sticky. **Acceptance criteria:** full revision history retained; ODbL attribution rendered on all spot pages containing OSM-derived data; hazard-removal always queued. --- ### F9 — Condition reports (P1) Lightweight ground truth: a logged-in user at/near a spot posts a structured report (felt wind: Beaufort-style picker; quality: smooth/gusty/turbulent; crowd level; free text ≤ 280 chars; optional photo). Reports show on spot detail for 24 h, then archive. Reports within 2 h and 1 km feed a "field-verified" check next to the live data (no score modification in v1 — kept simple and gameable-resistant; revisit post-1.0). --- ### F10 — Accounts, preferences & units (P1) - Email magic-link auth only (no passwords to breach, no OAuth lock-in). Session JWT, 30-day refresh. - Preferences: units (mph/kph/kt/m/s + Beaufort toggle), default kite profile, home location, saved spots. - All read features work logged-out (principle, and Maya's anti-requirement). Accounts gate only: editing, reports, favorites, alerts. - GDPR: export-my-data and delete-my-account endpoints from day one. ### F11 — Wind field overlay (P1) Optional map layer: wind direction/speed arrows on a grid over the visible viewport, sourced from the same provider via bulk grid query, cached per tile per 30 min. Off by default (bandwidth). ### F12 — Alerts (P2) "Notify me when spot X scores ≥ N within the next 48 h" — evaluated against forecast refreshes; web-push delivery. ### F13 — Offline/PWA mode (P2) Installable PWA; last-fetched conditions, saved spots, and map tiles for saved areas cached for field use. --- ## 3. Out of scope for v1 (explicit) Tide live data (field exists, manual notes only), webcams, lessons/school directories, marketplace, route planning, native apps, kite-size calculators beyond the simple suggestion in F6. ## 4. Non-functional requirements - **NFR-P1 Performance:** map interactive < 3 s on Moto G-class device over 4G; API p95 < 300 ms for cached condition reads; bundle ≤ 350 kB gzipped initial. - **NFR-A11Y:** WCAG 2.1 AA. All map information available in a non-map list view ("List" toggle). Color scale never the sole signal (labels + icons). - **NFR-SEC:** OWASP ASVS L1; rate limiting on all write endpoints; magic-link tokens single-use, 15-min expiry. - **NFR-PRIV:** No third-party analytics; self-hosted privacy-preserving metrics (e.g., aggregate counts only). Geolocation never leaves the device except as map-center coordinates in API queries. - **NFR-OPS:** Entire stack self-hostable via one `docker compose up`; no mandatory external service beyond the open weather APIs and tile source. - **NFR-COST:** Steady-state hosting for 10k MAU must fit a single ~$20/mo VPS + free-tier tiles/weather (drives the caching design and the Rust backend choice). - **NFR-LIC:** All runtime dependencies OSI-approved licenses compatible with AGPL-3.0. - **NFR-I18N:** All UI strings externalized; en/es/de at v1.0; RTL-safe layouts.