No description
Find a file
2026-10-03 20:49:44 +02:00
docker Try to fix nginx config on prod. 2026-10-03 20:44:58 +02:00
public Fix bus stops and rebrand (TBD). 2026-10-03 19:14:41 +02:00
scripts Add Docker files and remove DEMO content. 2026-10-03 19:56:49 +02:00
src Removed weird outlines 2026-10-03 20:24:05 +02:00
.dockerignore Add Docker files and remove DEMO content. 2026-10-03 19:56:49 +02:00
.DS_Store Works :) 2026-10-03 14:29:28 +02:00
.gitignore Scaffold and MVP 2026-10-03 13:33:50 +02:00
17b7999e-e2c0-42fd-8402-b008fb4b392c.pdf Scaffold and MVP 2026-10-03 13:33:50 +02:00
docker-compose.yml Add Docker files and remove DEMO content. 2026-10-03 19:56:49 +02:00
Dockerfile Add Docker files and remove DEMO content. 2026-10-03 19:56:49 +02:00
index.html Add Docker files and remove DEMO content. 2026-10-03 19:56:49 +02:00
package.json Add Docker files and remove DEMO content. 2026-10-03 19:56:49 +02:00
pnpm-lock.yaml Works :) 2026-10-03 14:29:28 +02:00
pnpm-workspace.yaml Works :) 2026-10-03 14:29:28 +02:00
README.md Add Docker files and remove DEMO content. 2026-10-03 19:56:49 +02:00
tsconfig.json Scaffold and MVP 2026-10-03 13:33:50 +02:00
vite.config.ts Fix no credentials for service worker. 2026-10-03 20:49:44 +02:00

GdzieWjade

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".

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²).

Run

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

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.

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):

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:

    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

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.

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.