GdzieWjade/README.md

193 lines
12 KiB
Markdown
Raw Normal View History

2026-10-03 19:14:41 +02:00
# GdzieWjade
2026-10-03 14:29:28 +02:00
A PWA that tells wheelchair users and parents with strollers whether a place or route in Kraków fits
**their** limits, and shows how we know: every fact carries a source, a date and a reliability level.
Missing data is never shown as "accessible".
2026-10-03 19:14:41 +02:00
Prototype for the "Kraków bez barier" hackathon challenge (the app name is one constant: `APP_NAME` in `src/config.ts`). Demo area: Wawel, Old Town and Kazimierz out east to TAURON Arena (≈19 km²).
2026-10-03 14:29:28 +02:00
## Run
```bash
pnpm install
pnpm dev # or: pnpm build && pnpm preview
pnpm test # unit tests (parsing, verdicts, routing, reach, moderation)
pnpm smoke # end-to-end flows against `pnpm preview` on :4173
pnpm a11y # axe-core WCAG 2.2 AA scan of every main view
pnpm data:build # re-fetch OSM + ZTP data into public/data/kk-snapshot.json and rebuild the timetable (kk-transit.json)
pnpm transit:build # only rebuild the timetable from the cached GTFS zips
pnpm offline # offline end-to-end check (needs `pnpm preview`): saves the app + map, cuts the network, reloads
2026-10-03 14:29:28 +02:00
```
Demo sign-in has no passwords: pick **Anna** (wheelchair), **Piotr** (stroller) or **Maria** (city
official, unlocks the City panel). Everything is stored in the browser, per account.
## What it does
| For residents and visitors | For city staff (role "Pracownik urzędu") |
| --- | --- |
| Guided profile: persona, equipment presets, limits with plain-language hints and tooltips, live preview of how many places fit | Data-quality KPIs computed from open sources, no database kept by the city |
| Fit report per place (entrance, steps, ramp/lift, width, surface, toilet, benches, baby changing) | Gaps by place type; operator-feed gap for transit stops |
| Step-free routing that avoids steps, high kerbs, steep and narrow sections, optionally cobbles; text route and barrier list | "Fix first" ranking of crossings with high or unknown kerb height, weighted by places served |
| Barrier-free reach: what you can get to in 5/10/15 min | Report moderation queue: approve → "verified by the city office" |
| Printable access card per place | Anonymous demand signals: where people look but data is missing |
| Report a correction (kept apart from OSM data, marked unverified) | CSV / GeoJSON export of the computed data |
| Saved places, text size, high contrast, reduced motion, PL/EN | |
## Routes, guidance and evidence
- **Route options:** the shortest path comes first (difficulties counted, not avoided), then variants that trade extra
distance for fewer difficulties. Each shows distance, time, overhead vs shortest, and counts of barriers / cautions /
unknown points; the best one is flagged "Recommended" and others are drawn faintly on the map.
- **Guide me:** live GPS guidance with turn prompts and barrier warnings at the top (optional voice, screen kept awake,
off-route re-routing). "Simulate GPS" walks the route for demos. Or hand the route to Google Maps / Apple Maps: waypoints
along our route keep their path close to ours, but those apps do not know the user's limits.
- **Map layers:** street map or satellite (Esri World Imagery; check its terms before production use).
- **Report evidence:** photo (EXIF stripped, downscaled), slope measured with the phone's tilt sensors, kerb height measured on
the photo against a reference object (card / A4 / ruler), or typed in. **LiDAR is not reachable from a web app** (no
LiDAR or WebXR on iOS Safari); `Measurement.method = 'lidar'` is reserved for a native iOS companion.
- **Automatic first look:** on-device CLIP zero-shot (free, private, no API key) labels the photo; rules combine it with the
claim and measurement into validity, consistency, severity and a triage lane (fast-track / review / likely reject).
It only orders the city queue. If the model cannot load the app says "No Credits left". Hosted free vision APIs
need a key (Pollinations anonymous is text-only), so none are wired in.
2026-10-03 19:14:41 +02:00
## Public transport
The route form has a **walking / transit** switch. In transit mode the app plans up to one transfer from the ZTP Kraków
timetable (static GTFS, `public/data/kk-transit.json`, built by `pnpm transit:build`):
- every walking leg (to the first stop, between stops when changing, from the last stop) is routed with the user's limits, with
barriers counted per leg; stops that cannot be reached for the profile are dropped;
- each connection shows departure and arrival times, delays, the next departures of the line and stop accessibility from OSM;
- a stop's detail page lists its next departures; other places list the stops within 400 m.
**Live delays** come from ZTP's GTFS-Realtime feeds (`TripUpdates_A/T.pb`, `ServiceAlerts_A/T.pb`, decoded in `src/lib/gtfsrt.ts`, no
dependency). ZTP sends no CORS headers, so the app requests `rt/*`, which `pnpm dev` and `pnpm preview` proxy to
`gtfs.ztp.krakow.pl`. A static production host needs the same reverse-proxy rule (nginx: `location /rt/ { proxy_pass https://gtfs.ztp.krakow.pl/; }`).
Without it the app says "timetable only" and shows no delays; it never pretends a trip is on time when no prediction exists.
The ZTP timetable has **no low-floor flag** (`wheelchair_accessible` is 0 for all 57k trips in the area) and the stops' `wheelchair_boarding`
is empty, so the app cannot promise an accessible vehicle and says so.
## Deploy to production (Docker Compose)
The app is static files plus one small proxy rule, so production is a single nginx container (`Dockerfile`, `docker/nginx.conf`).
There is no database and no server-side state: accounts, settings, reports and decisions live in each user's browser.
**1. Settings you may want to change first** (compile-time constants in `src/config.ts`):
| Constant | Meaning |
| --- | --- |
| `APP_NAME` | The app's name everywhere: header, page title, PWA name, export file names. |
| `SHOW_PROTOTYPE_BANNER` | The "prototype" strip above the header. `false` by default. |
**2. Build and start** (Docker with Compose v2):
```bash
git clone <this repo> && cd <repo>
docker compose up -d --build # serves http://localhost:8080
docker compose ps # "healthy" when /healthz answers
docker compose logs -f app
```
Pick another port or bind address with `APP_BIND` (e.g. `APP_BIND=127.0.0.1:8080` in a `.env` file next to `docker-compose.yml`).
**3. HTTPS is required.** Browsers only allow location (GPS), the service worker (offline mode) and "install app" on `https://`
(or `localhost`). Two ways to get it:
- *Your own reverse proxy* (Traefik, nginx, a load balancer): point it at the `app` container's port 80 and let it terminate TLS.
Set `APP_BIND=127.0.0.1:8080` so the container is not reachable directly. Do not strip or cache `/sw.js`.
- *Caddy, included* (certificate issued and renewed automatically): create `.env`, point the DNS A/AAAA record of the host at the
server, open ports 80 and 443, then:
```bash
printf 'DOMAIN=gdziewjade.example.org\nAPP_BIND=127.0.0.1:8080\n' > .env
docker compose --profile tls up -d --build
```
**4. What the container does**
- Serves the built app with gzip, a single-page-app fallback, and cache rules that make updates reach installed copies:
`index.html`, `sw.js`, the manifest and `data/*` are always revalidated; `assets/*` (hashed names) are cached for a year.
- Proxies `/rt/*` to `gtfs.ztp.krakow.pl` (live delays; ZTP sends no CORS headers) with a 15 s cache, so a crowd of users makes one
upstream request per feed per 15 s. If ZTP is down the app falls back to the timetable and says delays are unavailable.
- `GET /healthz` returns `ok` (used by the Compose health check). The filesystem is read-only.
**5. Updating**
```bash
git pull
docker compose up -d --build # rebuilds the image; installed apps pick up the new version on their next visit
```
To refresh the places and timetable data, run `pnpm data:build` on a machine with internet access (Overpass can rate-limit; the script
retries and rotates mirrors), commit `public/data/kk-snapshot.json` and `public/data/kk-transit.json`, then rebuild as above.
The GTFS feed changes every few days; rebuilding monthly keeps the offline copy reasonably fresh.
**6. Before going public**
- Map tiles come from `tile.openstreetmap.org`, whose usage policy discourages heavy and bulk use. The offline map pack downloads about
1,000 tiles per new device. For real traffic run your own tile server (or a commercial provider) and change the tile URL in
`src/components/MapView.tsx`, `vite.config.ts` (cache rule) and `src/lib/offline.ts`.
- Confirm the licence of the ZTP Kraków data (otwartedane.um.krakow.pl) and keep the OpenStreetMap attribution (ODbL).
- Sign-in is a demo (no passwords, no server). Put real authentication in front before trusting the city-office panel.
2026-10-03 14:29:28 +02:00
## Data and honesty rules
- **OpenStreetMap** (ODbL) via Overpass: places, steps, ramps, kerbs, surfaces, benches, toilets.
- **ZTP Kraków GTFS** (`stops.txt`, `wheelchair_boarding`): 82 stops in the area. The operator's field says
"no information" for **all** of them; OSM contributors describe 44. The app shows both and the City panel
points at the gap. Licence of the ZTP feed must be confirmed at otwartedane.um.krakow.pl before reuse.
- Reliability levels: `verified` (survey date ≤ 24 months, or approved by the city office) · `mapped`
(OSM edit ≤ 24 months) · `stale` (older) · `inferred` (from nearby objects) · `user` (unverified report) ·
`none`. The date shown is the last OSM edit of the object unless a survey date exists, so we never call
edit-date data "confirmed".
- Absence of data → "No data". User report vs OSM disagreement → "Conflicting data" with both sides shown.
Refresh failure → last snapshot date plus a warning (demo switch in *Data → Demo controls*).
## Architecture
```
scripts/build-data.mjs adapters: Overpass (OSM) + GTFS (ZTP) → public/data/kk-snapshot.json
src/lib/osm.ts tag interpretation (surface, incline, width, kerb)
src/lib/assess.ts fact model {status, source, confidence, date} + verdict per profile
src/lib/route.ts A* over the pedestrian graph, profile-aware penalties
src/lib/reach.ts time-limited Dijkstra for barrier-free reach
src/lib/civic.ts city-panel aggregates, hotspot ranking, export
src/components/* USWDS 3 UI; MapView = MapLibre GL (raster OSM tiles)
```
Acquisition and presentation are separate: a new source is a new adapter producing the same fact shape; a
new city is a bounding box in `build-data.mjs` plus its adapters. Configuration lives in code constants today;
moving it to a per-city JSON is the first scaling step.
## Business model and go-live (proposal)
- Free for residents. Revenue from B2B: venue accessibility widget/report for hotels, event organisers and
property managers; accessibility API for booking and travel apps; roll-out package for other cities.
- Owner of the product: the operating entity (not UMK). Costs: static hosting, scheduled Overpass/GTFS pulls,
report moderation.
- Month-one launch: host static build + nightly data job; real SSO for staff; server-side reports with
rate limiting and two-person confirmation before pushing to OSM; replace OSM tile server with a paid or
self-hosted tile provider; confirm every dataset licence.
## Privacy and security
No accounts on a server, no analytics, no health questions — only barrier preferences. Profile, saved places
and reports are in `localStorage`. Geolocation (optional) never leaves the browser. HTTPS only.
The demo sign-in is not security: it only selects a local profile and role.
## Accessibility status
Target WCAG 2.2 AA. Automated: axe-core finds 0 violations on login, places, place detail, route, profile,
City panel, data pages (`pnpm a11y`). Built in: full keyboard operation, focus management on view changes,
information never by colour alone, tooltips dismissible with Esc, text size and contrast settings, text
equivalents for map content. **Known limits:** choosing a place by clicking a map pin is not keyboard
accessible (use the list); no manual screen-reader pass on every platform yet; high-contrast mode is basic.
## Known limitations
Demo area only. Reports and moderation are local to one browser. Kerb height is known for only part of the
crossings and entrance width is almost never mapped. Walking times are estimates. Routing ignores traffic
lights and temporary obstacles. OSM tile servers are for demos only.