尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗A mobile-first, installable (PWA) navigation app for people who walk, cycle or scoot. Plan routes, see community-reported hazards, suggest missing paths, and earn points when moderators approve your contributions.
Stack: plain HTML/CSS/ES modules + Leaflet 1.9.4 from cdnjs. No build step, no framework, no npm install.
Any static file server works. The app must be served over http(s)://, not opened from file:// (ES modules and the service worker need an origin).
cd ghetto-wayz
python3 -m http.server 8787
# open http://localhost:8787
Install it as an app from the browser's "Add to Home Screen" / "Install" prompt once served over HTTPS (or localhost).
| Feature | Default (no config) | With config |
|---|---|---|
| Map tiles | Live — OpenStreetMap standard tiles, no key (dark colourways apply a CSS filter). OSM's tile policy is for light use; for a launched app set TILE_URL to a provider such as MapTiler, Stadia or Thunderforest (free tiers with a key) | Custom tile URL + attribution via TILE_URL / TILE_ATTRIBUTION |
| Place search | Live — Nominatim (OpenStreetMap) public API, fair-use; sample places merged in | Add NOMINATIM_EMAIL (asked by their usage policy) |
| Routing | Sample — hand-made example routes between sample places in Coventry; anything else gets a clearly labelled straight-line estimate | Live via OpenRouteService (ORS_API_KEY) or a self-hosted OSRM (OSRM_BASE_URL) |
| Geolocation | Live — browser Geolocation API, asked for only when needed, never stored | — |
| Accounts, alerts, path suggestions, points, leaderboard, moderation | Local demo — localStorage on this device only. Not shared with anyone. Seeded with sample users/alerts/paths flagged sample: true | Implement the Backend interface (below) against your real service |
| Friends, live-location sharing, trip sharing | Local demo (or live with Supabase, below) — works only between accounts signed in on the same browser (e.g. two tabs; tabs sync via storage events). A sample friend (GW-KERB-H0P1) auto-accepts and can run a scripted trip so you can see the viewer UI. Nothing leaves the device. | Implement the social methods + a RealtimeProvider (see below) |
Every route card carries a Live / Sample / Estimate chip and a plain-language "basis" line, and alerts/paths from the seed are labelled Sample. Nothing in demo mode is presented as live or verified.
OpenRouteService and OSRM only know OpenStreetMap, so they can't route along a footpath or alley that the community added. The app joins them in itself (js/providers/communityRouting.js):
The engine legs are live routes. The community section is timed at the mode's average speed and is shown as a 🟢 step. It costs up to 4 extra routing calls per search, and only when an approved path is nearby. Tuning is in COMMUNITY_ROUTING in js/backend/rules.js.
The long-term fix is to add good, clearly public paths to OpenStreetMap itself, following OSM's rules: only map what you've surveyed. Every routing engine then picks them up.
Copy config.example.js to config.local.js (git-ignored) and fill in what you have:
window.GW_CONFIG = {
ORS_API_KEY: "", // openrouteservice.org — enables live routing (foot-walking / cycling-regular)
OSRM_BASE_URL: "", // alternative: self-hosted OSRM with foot + bike profiles
TILE_URL: "", // optional custom tile template; TILE_ATTRIBUTION must match your provider's terms
TILE_ATTRIBUTION: "",
NOMINATIM_EMAIL: "", // your contact email for Nominatim's usage policy
TOMTOM_API_KEY: "", // developer.tomtom.com — enables the Live traffic layer (flow tiles + incidents)
BACKEND: "local" // swap to your adapter name once added in js/backend/index.js
};
Keys live only in config.local.js on the client. For production, proxy OpenRouteService through your backend so the key is not shipped to browsers.
Drive uses the provider's car profile (driving-car on OpenRouteService, driving on OSRM), which follows OpenStreetMap access rules — cars are never routed onto footways, cycleways, pedestrian zones or private roads. Neither engine supplies live traffic or real-time closures, so travel times are free-flow estimates and every car route carries a No live traffic chip. Without a routing provider, Drive shows a clear "needs a routing provider" message rather than a fabricated route. While navigating in Drive the bar is larger, nothing needs tapping except End, and voice guidance (browser SpeechSynthesis, en-GB) is on by default with a toggle; walking/cycling can turn voice on too. There are no adverts anywhere in the app.
deriveLampStatus): 🟡 Recently confirmed working — a light_ok report within 14 days; 🔴 Reported out — latest report is light_out (30-day expiry); ⚪ Unknown / not verified — no current report. Tapping a lamp shows the status, when it was last reported, moderator verification, and the report history, plus Seen it working / Report out.🔦 N% lit·confirmed and the sheet shows a bar with all four shares and a plain-language sentence (e.g. "66% has no lamp data at all"). On the map the selected route gets a thin warm overlay on confirmed stretches and red on reported-out ones. Prefer routes with confirmed lighting appears only at night and only when at least one route has ≥ 20% confirmed coverage; it ranks routes by that share and labels the best "Most confirmed lighting". Nothing is ever described as safe.data/coventry-lamps.json, a snapshot of OpenStreetMap highway=street_lamp nodes refreshed every Monday by .github/workflows/lamps.yml (scripts/fetch-lamps.mjs). Outside the Coventry box the app falls back to live Overpass. OSM has only ~500 lamps mapped in Coventry (the council runs ~31,400), so a council FOI dataset can later replace this file as-is.data/coventry-lit.json (same weekly workflow, scripts/fetch-lit.mjs) holds every road/path tagged lit=yes/lit=no — about a third of the city's 4,100 km walkable network (Oct 2026: 1,071 km lit, 195 km unlit). Drawn at zoom ≥ 15 as an amber glow (lit) or red dashes (dark). Tap a street → It's lit / files a / report (180-day expiry, normal 5/day limit). A report changes the street's colour only once it's : verified by a moderator or confirmed by 2+ other people. Route scoring counts lit/dark streets after confirmed or reported-out lamps.Two sources, both free at test-app scale:
config.local.js as TOMTOM_API_KEY. The free tier covers roughly 2,500 incident requests and 50,000 tiles a day. The app keeps well inside that: incidents are fetched once per map view, cached 60 s, and only at zoom ≥ 11.
🚦 N traffic when live incidents sit within 30 m of it, and the route sheet lists them under Live traffic on this route. The No live traffic chip on Drive routes disappears while the layer is on.supabase/migration-006-realtime-alerts.sql once; after that every phone with the app open sees a new report, confirmation or moderator review within a second or two, with a toast when a new alert lands within 1.5 km of the user. No extra cost: Realtime is included in the free project.Before a public launch, move the TomTom incidents call behind a Supabase Edge Function so the key is not shipped in config.local.js.
supabase/migration-008-place-reviews.sql, js/ui/explore.js): signed-in members can leave 1–5 stars and up to 500 characters. One review per person per place (writing again replaces it), 5 new reviews a day. Other members can report a review once; three reports hide it automatically until a moderator restores it or keeps it hidden (Moderation queue › Hidden reviews, logged in admin_actions). A moderator hide survives edits; a report-driven hide is cleared when the author edits (reports reset, so it gets a fresh look). Profile › My reviews lists yours.The 🛡️ button (map controls, home sheet, route sheet and the navigation bar) opens Safety:
{ sos: true } or { safety: { dueAt } } on the share row, so the same privacy rules apply (friends only, positions deleted when it ends, only while the app is open). migration-010 adds the safe end reason. Alerts reach friends while their app is open; if they allowed notifications, the phone also buzzes and shows a system notification. GettoWayz is not an emergency service — the copy says so.⚠️ N alerts (good-news reports like "lamp working" no longer count), ⛰️ N m up (climb from OpenRouteService elevation, walk/bike/scooter) and 🚧 Path reported blocked when the latest community check on a path it uses says so.✅ N used it, last checked …, or a Not confirmed in 90 days / Latest report: blocked chip.amenity=bicycle_parking). Its place sheet lists recent 🔓 Bike theft spot reports nearby and has Report a theft here. Bike theft is also a normal alert category (14-day expiry).my_badges() works them out in SQL from verified trips, awarded points and path checks, so they can't be faked; nothing is stored and badges never earn points.Profile › Offline maps (or map layers ⧉ › 📥 Offline maps) saves an area to use with no signal: this map view, about 3 km around me or all of Coventry (~13 MB). It shows the size before downloading, a progress bar, and a list of saved areas with Show / Delete.
js/offline.js, Cache Storage gw-offline-map-v1): OpenFreeMap vector tiles for the area at zoom 10–14 (the map over-zooms for street level), plus the style, tile index, icons, Latin fonts and UK shaded relief. Max 2,500 tiles per area; 6 downloads at a time to stay polite to the free tile server.sw.js): map data is network-first. If the network fails or takes over 4 s and the tile is saved, the saved copy is shown. Saved tiles refresh themselves while you're online. Cache keys drop OpenFreeMap's weekly data version, so packs keep working after their data updates.No public routing engine has a scooter profile. Scooter mode uses the cycling network with scooter speed estimates and says so in the route basis. Check local rules on where e-scooters may be ridden.
Everything — accounts, alerts, paths, points, friends, live sharing, trip rewards — runs on one free Supabase project.
supabase/schema.sql, Run. (Projects created before these features were added: also run supabase/migration-002-streetlights.sql then supabase/migration-003-route-sharing.sql, then supabase/migration-004-points.sql, then supabase/migration-005-manual-points.sql, then supabase/migration-006-realtime-alerts.sql, then supabase/migration-007-mods-on-leaderboard.sql, then supabase/migration-008-place-reviews.sql. New projects run schema.sql, then 002 to 010 in order — migration-009-street-lighting.sql and migration-010-community-badges.sql come last.) If it reports pg_cron not available, enable it under Database → Extensions and re-run the last do $$ … $$ block (it expires sessions every minute even when nobody is online; without it they still expire on the next read).service_role key).config.example.js → config.local.js:
window.GW_CONFIG = { SUPABASE_URL: "https://<id>.supabase.co", SUPABASE_ANON_KEY: "<anon key>", BACKEND: "supabase" };
update profiles set role = 'admin' where alias = 'your-alias'; in the SQL editor.What the SQL enforces server-side: all rate limits, points only via gw_grant() (clients cannot write point_entries or profiles.points), share positions readable only by the two participants (RLS), progress along the route computed in update_share_position() so clients can't inflate distance, trip_check_in() replay-proof via a unique trips.share_id, block → sessions ended and positions nulled in one statement. Tested locally against Postgres 16 under the authenticated role.
Realtime: SupabaseRealtime (in js/backend/supabase.js) subscribes to shares and friends changes; Supabase only streams rows the signed-in user's RLS policies allow, so a user never receives someone else's session.
js/backend/index.js documents the interface. Implement it (Supabase, Firebase, your own API…) and register it in createBackend():
currentUser, register, signIn, signOut, updateUser, onChange
listAlerts, createAlert, confirmAlert, reviewAlert
listPaths, suggestPath, reviewPath
pointHistory, leaderboard
Server-side you must enforce what localDemo.js enforces client-side: rate limits (rules.js), "no confirming your own report", points granted only on moderator approval/verification, and the admin role check on review calls. Approved paths should be served to all clients from listPaths() — the map and the "More verified paths" labelling read from there.
Flows
GW-XXXX-XXXX, copyable). Enter someone's ID → Find → Send request. Lookup returns alias + ID only. They must accept; until then nothing is visible either way. Reject, cancel, remove, block, report, and "who can send me requests" (anyone / nobody) are all in the Friends screen.Privacy model (enforced in the backend adapter, must be enforced server-side too)
share_routes table that only the owner, and the recipient after accepting, can read. Positions are only written for an accepted share with live on. Only the owner can push positions or change live; either participant can end the share.Realtime: js/providers/realtime.js defines RealtimeProvider { onUpdate(cb), isCrossDevice, note }. The demo implementation listens to backend changes in this browser. For real users, implement it over WebSockets / Supabase Realtime / Firebase, scoping each subscription to the session ids the signed-in user may see, and keep publishing through backend.updateSharePosition() so access checks live in one place.
Account → Appearance (or "Change colourway" on the home sheet) switches between three palettes built from the logo (navy #202b47, lime #aaf129, teal #2bb190): Night Route (default), Deep Navy, Daylight. The choice is stored in localStorage; Daylight also swaps the default basemap to CARTO light. Tokens live at the top of css/app.css.
All values live in one place: js/backend/pointsPolicy.js, which is used by demo mode and the in-app guide. In live mode the server is authoritative. supabase/migration-004-points.sql seeds the same defaults into point_rules (per-rule points and daily caps) and points_settings (trip rules and the level curve), and every award is calculated in SQL. To change live values, edit those two tables. Clients never send a points amount.
Earning (defaults)
| Action | Points | Daily cap |
|---|---|---|
| Missing path approved by a moderator | +25 | 75 |
| Hazard report verified by a moderator | +8 | 40 |
| Streetlight report verified by a moderator | +3 | 15 |
| Street lighting report (lit/dark) verified or confirmed by 2+ people | +3 | 15 |
| You confirmed a report that was then verified | +2 | 10 |
| Verified trip completed: walk, bike or scooter | +4 | 16 completion pts/day (4 trips) |
| Verified drive completed (no distance bonus) | +2 | shares the cap above |
| Trip distance bonus: 400 m +1 · 1 km +3 · 2.5 km +5 · 5 km +8 · 8 km+ +10 | max 14/trip incl. base | 30 |
Verified trips. The old self-reported "Check in" award is gone. A trip earns points only if all of this holds:
start_trip on a planned route.trip_checkpoint).finish_trip).How the distance is worked out and protected:
Ledger & integrity. point_entries is append-only from the app's point of view:
awarded, capped, reversed, reversal), requested amount, reversal link, reason, actor and policy version.reversal entry and marks the original reversed. Nothing is deleted.profiles.points is updated in the same transaction and always equals the sum of non-reversed awards.gw_award, gw_grant, gw_reverse_entry, gw_end_shares_between, gw_are_friends, gw_blocked_either, gw_trip_end) can no longer be called by app users. Before migration 004, gw_grant could be called directly over the API.Levels. There are ten levels on a steady curve. The threshold for level n is round to 5 of 25 × (n−1)^1.75: 0, 25, 85, 170, 285, 420, 575, 755, 950 and 1,170 points.
profiles.level_floor). Reversed points come off their balance and rankings, but their level doesn't drop.Leaderboards. leaderboard_page(scope, period, offset, limit) and leaderboard_me(...):
awarded points count; reversed, capped and pending don't.Moderators. The moderation screen now also has:
admin_actions.Setup: run supabase/migration-004-points.sql once after 002 and 003, then 005, 006 and 007. No new environment variables or services.
Moderator points adjustments (testing): Profile › App › Add test points (moderators only) opens the moderation screen at Add or remove points. Enter an alias, an amount (up to ±1000) and a reason. Each adjustment is a normal ledger entry (Points adjusted by a moderator) that the user sees in Activity with the reason, is logged in admin_actions, and can be reversed from Points lookup & reversals. Removals never take a balance below 0; levels already reached are kept. Moderators appear on leaderboards like anyone else once they opt in (supabase/migration-007-mods-on-leaderboard.sql).
Five tabs, the same everywhere: Leaderboard · Friends · Map · Activity · Profile. Map is the start screen and sits raised in the centre.
| Tab | What's on it |
|---|---|
| Leaderboard | Your rank and points for the chosen board (Everyone / Friends × This week / All-time), top three, full list, who-can-see-me switches, link to the points rules |
| Friends | “Sharing now” card with Stop, your GettoWayz ID, add by ID, friends, requests, sharing history; Share a trip uses the route planned on the map |
| Map | Search, travel mode, routes, navigation, report / add path, layers and a full map legend |
| Activity | “Needs you” (friend requests, trip invitations, requested changes) and a feed of trips, points, contributions and sharing, filterable |
| Profile | Avatar, ID, level and progress, level milestones, contributions, location sharing, privacy, notifications, accessibility, app settings |
#leaderboard, #friends, #activity, #profile). Back moves between tabs, and closes an open pop-up first.gw:prefs in localStorage. They are never sent to the server.index.html app shell
css/app.css design tokens + styles
js/app.js wiring: search, routing, navigation, controls
js/map.js Leaflet wrapper: tiles, routes, pins, draw tool
js/state.js tiny store
js/providers/ routing.js (ORS / OSRM / sample), geocoding.js (Nominatim), streetlights.js (Overpass / sample + status + scoring), realtime.js, sampleData.js
js/backend/ index.js (interface), localDemo.js, social.js (friends/sharing/rewards), seed.js, rules.js
js/ui/ tabs.js (tab shell + router), points.js (Leaderboard), friends.js (Friends), activity.js (Activity), account.js (Profile + sign-in), avatar.js; sheet, modal, alerts, paths, admin, streetlights
js/prefs.js device-only settings: larger text, reduce motion, in-app alerts
sw.js caches the app shell for offline launch (tiles/APIs are never cached)
manifest.webmanifest PWA manifest + icons/
config.example.js copy to config.local.js
There is one copy of the app code — the files above, in the repo root. The website is served straight from it, and the Android APK and iOS app are both built from it via apps/ (Capacitor). Change the code once and all three get it.
apps/capacitor.config.json app id + name for both phone apps
apps/scripts/sync-web.mjs copies the web code into apps/www before each phone build
apps/scripts/patch-native.mjs adds location permissions (Android manifest, iOS Info.plist)
apps/android/ Android Studio project (APK)
apps/ios/ Xcode project (needs a Mac or cloud Mac)
Step-by-step commands are in apps/README.md.
Map data and streetlight positions © OpenStreetMap contributors, via the Overpass API. Default tiles © CARTO. Search by Nominatim. Routing (when configured) by OpenRouteService. Leaflet is BSD-2-Clause.
street_litstreet_darkstreet_lighting_confirmed +3 (cap 15/day) to the reporter and confirm_helpful +2 to each confirmer, paid once when the 2nd confirmation lands or a moderator verifies — whichever comes first. Needs supabase/migration-009-street-lighting.sql on the live backend.