Skip to content

Latest commit

 

History

History
262 lines (212 loc) · 17.8 KB

File metadata and controls

262 lines (212 loc) · 17.8 KB

CLAUDE.md — GroundShare

What is this project?

GroundShare is a Hebrew (RTL), mobile-first PWA + native iOS/Android app (Capacitor) for urban-planning transparency in Israel. Users view a map of their neighborhood, see official planning data (construction permits, road work, city plans from Tel Aviv GIS / iplan / mavat), and crowdsource community reports (noise, blockages, etc.). Features include address comparison with an AI summary, favorites, reviews, voting, neighborhood clustering, push notifications, and a gamification system (XP/levels/streaks).

Deployment

Production is live on Azure (Israel Central):

  • Custom domain: https://groundshare.app (DNS via Cloudflare)
  • API: https://api.groundshare.app (Cloudflare → Azure App Service app-groundshare-api) — same-site with the web app so the httpOnly refresh cookie works
  • Frontend: Azure App Service (Node host serving the Vite dist/ bundle)
  • Database: Azure SQL (sql-groundshare-il.database.windows.net)
  • Secrets: Azure Key Vault via Managed Identity — no secrets in env vars

Project structure

.github/            CI/CD workflows (ci, cd, codeql, cluster-daily, relevance-daily)
docs/               Setup, deploy, security, AI context, feature inventory, plans
src/
  01-database/      GroundShareDB.sql — complete schema + stored procedures (single source of truth)
                    ResetData.sql — wipes user data while keeping schema + lookups
  02-server/        C# ASP.NET Core 8 Web API
    Controllers/    22 controllers (Auth, Users, Events, GeoMaps/GeoInfrastructure/GeoPlanning,
                    Locations, Favorites, Ai, Comments, Comparisons, Contact, Files, Health,
                    Notifications, Clustering, Relevance, UserReviews, Cities,
                    Streets, EventTypes, PlanStatus)
    DAL/            Data access — pure ADO.NET, NO Entity Framework. All DB calls use
                    SqlConnection + SqlCommand, mostly via stored procedures (sp_*).
                    DAL/Interfaces/ holds the DAL contracts used for DI.
    BL/             DTOs + domain helpers (LoginRequest, CreateEventRequest, User,
                    IsraeliTM, KMeans, ScoringModels, TlvAddressService, ...)
    Services/       Business logic (AuthService, EventService, ComparisonsService,
                    GisFeatureService, ContentSafetyService, HebrewBlocklist,
                    storage, FCM, email, audit) + 3 background/hosted services
    Domain/         Plain domain types
    Exceptions/     AppExceptions — typed exceptions mapped to HTTP status in middleware
    Extensions/     DI + pipeline registration extension methods (Program.cs is thin)
    Filters/        ValidationFilter (FluentValidation → ApiResponse envelope)
    Middleware/     Exception handling, ApiResponse envelope, security headers, request logging
    Logging/        LogRedactionEnricher (PII masking for Serilog)
    Options/        Strongly-typed config classes (Jwt, GoogleApi, Storage, Resend, ContentSafety)
    Validators/     FluentValidation rules for request DTOs
  03-client/        React 19 + TypeScript + Vite 7
    src/app/
      features/     Feature-folder screens. One folder per surface:
                    auth/ main/ address-search/ nearby/ compare/ create-event/
                    favorites/ notifications/ personal/ settings/ dictionary/
                    Each can hold sub-components + a lib/ for pure helpers (+ __tests__).
      components/shared/  Reusable UI used across features (BottomNavBar, TopSearchBar,
                    MapLayout, DrawerMenu, map overlays, appIcons.tsx, ...)
      onboarding/   First-time questions + replayable in-app tour (TourContext/Overlay)
      context/      AuthContext (JWT + user state), GeoContext (GPS watchers),
                    MapContext (map config + overlays)
      hooks/        useGeolocation, useGoogleMapsKey, useSwipeBack, useAsync,
                    useHomeNavigation, useNotificationFeed, usePendingConfirmations, ...
      services/     api/ (per-domain modules + client.ts transport), addressAutocomplete.ts,
                    pushNotifications.ts, authErrors.ts. (api.ts is a thin re-export shim.)
      types/        Centralized backend-shape types (one file per domain, Snake_Case fields)
      utils/        eventDisplay, relativeTime, serverDate, ...
      constants/    notifications.ts, onboardingOptions.ts
      routes.ts     React Router 7 — createBrowserRouter with ProtectedRoute/GuestRoute guards
    src/styles/     tailwind.css, theme.css (CSS variables), fonts.css (Heebo, Poppins)
    android/ ios/   Capacitor 8 native shells
  04-tests/         GroundShareAPI.Tests — xUnit unit tests for backend pure logic
                    (IsraeliTM, KMeans, NeighborhoodPolygon, HebrewBlocklist,
                    AdminTokenValidator, FluentValidation validators). Run in CI.
tools/              Pre-commit hook for gitleaks + installer

Running locally

# Database: Run src/01-database/GroundShareDB.sql in SSMS against local SQL Express.
# DB starts empty — register your first user through the app's Register screen.
# To reset between tests: run src/01-database/ResetData.sql

# Backend (port 5227)
cd src/02-server
dotnet run

# Frontend (port 5173)
cd src/03-client
npm install
npm run dev

Backend requires appsettings.Development.json (gitignored) with local SQL connection string, JWT key, and Google Maps API key. See SETUP.md for details.

Critical patterns to follow

Backend — DAL pattern (no ORM)

The backend does NOT use Entity Framework. All database access is manual ADO.NET through a base class DBServices:

// Every DAL class inherits DBServices and uses this pattern:
using SqlConnection con = Connect();
var p = new Dictionary<string, object> { { "@Param", value } };
SqlCommand cmd = CreateSP("sp_StoredProcName", con, p);
return ReadAll(cmd);  // Returns List<Dictionary<string, object>>
  • Stored procedures use sp_ prefix (e.g., sp_CreateEvent, sp_RegisterUser)
  • Database columns use Snake_Case: User_ID, Event_ID, City_Name, Start_Date
  • Most DAL methods return Dictionary<string, object> — not typed models
  • Some DAL methods use raw SQL instead of SPs (inconsistent but functional)
  • New endpoints: Controller → Service (if logic is non-trivial) → DAL class → stored procedure

Backend — controllers stay thin

Non-trivial logic lives in Services/, not controllers. Examples already extracted:

  • ComparisonsService — resolve → score → classify → persist → AI summary (was in ComparisonsController)
  • GisFeatureService — shared GIS fetch + ArcGIS parsing + cross-layer disruption dedup + geometry math + geocode backfill, used by the three Geo* controllers and the clustering job (so the dedup logic is no longer copy-pasted)
  • EventService — event creation + the fire-and-forget FCM fan-out
  • AuthService — token issuance/rotation helpers

DI is registered via extension methods in Extensions/ (AddApplicationServices, AddDataAccessLayer, AddBackgroundJobs, AddAppHttpClients, AddConfiguredCors, AddAppRateLimiting, etc.) — keep Program.cs thin and add new registrations to the matching extension.

Backend — error handling

Throw typed exceptions from Exceptions/AppExceptions.cs (e.g. NotFoundException, ConflictException); ExceptionHandlingMiddleware maps them to the uniform ApiResponse<T> envelope with the right status code. Don't return raw error strings from controllers. SQL unique-constraint collisions surface as specific patterns (EMAIL_EXISTS, error numbers 2601/2627) — catch those where relevant.

Frontend — component patterns

  • All components are functional with hooks (useState, useEffect, useCallback, useRef)
  • Named exports for components: export function MainScreen() { ... }
  • Feature folders: screens live under app/features/<feature>/; pure helpers go in <feature>/lib/. Cross-feature UI lives in app/components/shared/.
  • Routing: React Router 7 with createBrowserRouter (object form, Component:) in routes.ts; RouterProvider mounted in App.tsx. Import from react-router (not react-router-dom).
  • Auth state: useAuth() from AuthContext — user, login, logout, isAuthenticated
  • Geo state: useGeolocation() (thin wrapper over GeoContext) — dual coarse + high-accuracy watchers
  • Map state: useMap() from MapContext — screens set config and overlays, MapLayout renders them
  • API calls: per-domain modules under services/api/* over a single request<T> transport in services/api/client.ts, with typed ApiError and single-flight 401 refresh
  • Navigation: useNavigate() with state passing via location.state

Frontend — auth & token storage (XSS hardening)

  • Access token lives in JS memory only, on both web and native — never in localStorage.
  • Web: refresh token is an httpOnly gs_refresh cookie (SameSite=Lax, Secure, path /api/auth); the cookie path requires an X-CSRF header.
  • Native (X-Client-Platform: native): refresh token returned in the body and kept in sandboxed localStorage.
  • Refresh is single-flight in services/api/client.ts; the server does single-winner rotation with reuse detection (sp_RotateRefreshToken).

Frontend — styling

  • Primary: Tailwind CSS 4 classes + inline style={{}} for custom colors/gradients/shadows
  • Color palette: #063c55 (dark teal primary), #7ECFE5 (light cyan), #f68d1e (orange accent), #f3fbfd (light bg)
  • Fonts: Heebo (Hebrew text), Poppins (Latin/numbers)
  • RTL: dir="rtl" on root divs. Use dir="ltr" wrapper when flex direction needs reversing
  • Icons: lucide-react + @tabler/icons-react, centralized in components/shared/appIcons.tsx (single source of truth — swap a glyph there and every call site updates). Compare-specific Figma SVGs in features/compare/icons.tsx. See src/03-client/docs/icon-map.md. No MUI / Radix (removed).
  • Responsive: Mobile-first; desktop shows a phone-frame preview (412×915px)

Frontend — map integration

  • Google Maps via @vis.gl/react-google-maps with AdvancedMarker
  • API key fetched from server at runtime via GET /api/geo/maps-config (never hardcoded in client)
  • MapContext overlay system: screens push markers/circles, MapLayout renders them
  • Persistent map: MapLayout stays mounted across the map routes (/main, /nearby-reports, /address-search, /questions)
  • Building footprints (BuildingsOverlay, TLV layer 513) + house-number labels (AddressNumbersOverlay, layer 527) + neighborhood polygons + TLV satellite tiles are all mounted in MapLayout
  • Smooth panning via sequence counter in panTo()
  • Default center: Tel Aviv (32.0853, 34.7818)

API base URL and auth

  • Base URL (dev): http://localhost:5227/api. Production: https://api.groundshare.app/api.
  • Auth: JWT Bearer (access token, 15-min) in the Authorization header; access token held in memory (see above)
  • Token refresh: automatic single-flight on 401 in services/api/client.ts
  • CORS: localhost:5173 / :4173 in dev; native Capacitor origins + the web origin in prod

Key API routes

POST /api/auth/register, /login, /google, /refresh, /logout, /forgot-password, /reset-password
GET  /api/users/me, /{id}            PUT /api/users/onboarding, /home-address, /phone
GET  /api/users/activity             POST /api/users/missions/claim      DELETE /api/users/me

POST /api/events                          Create event (fires FCM fan-out)
GET  /api/events/nearby?lat=&lng=&radius= Nearby events (anonymous)
GET  /api/events/location/{id}            Events for a location (anonymous)
POST /api/events/{id}/vote                Upvote/downvote
GET/POST /api/events/{id}/comments        Comment thread
GET  /api/events/my-pending-confirmations · POST /api/events/{id}/confirm-relevance

GET  /api/geo/maps-config                 Get Maps API key  (GeoMapsController)
GET  /api/geo/geocode | /reverse | /autocomplete                  (GeoMapsController)
GET  /api/geo/parcel | /buildings | /address-points               (GeoInfrastructureController)
GET  /api/geo/planning-status | /nearby-disruptions               (GeoPlanningController)

POST /api/locations/resolve               Resolve address → location_id (anonymous)
GET  /api/locations/{id} | /{id}/stats | /{id}/plans

POST /api/comparisons/compare             Score + AI summary
POST /api/comparisons                     Save     GET /api/comparisons/history
POST /api/userreviews · PUT /api/userreviews/{id} · GET /api/userreviews/location/{id}
POST /api/favorites/toggle · GET /api/favorites · GET /api/favorites/check/{id}
GET  /api/notifications, /feed · POST /notifications/toggle/{id}, /device · GPS sub endpoints
POST /api/ai/address-summary · POST /api/ai/ask
GET  /api/clustering/neighborhoods · /diagnostics   POST /clustering/run-now (admin)

The single GeoController was split into GeoMapsController (maps key, geocode, autocomplete, reverse), GeoInfrastructureController (parcel, buildings, address-points), and GeoPlanningController (planning-status, nearby-disruptions) — all still under the /api/geo route prefix.

Naming conventions

Layer Convention Examples
DB columns Snake_Case User_ID, City_Name, Start_Date
Stored procs sp_ prefix sp_CreateEvent, sp_GetUserById
C# classes PascalCase EventsController, EventsDAL, GisFeatureService
C# DTOs PascalCase CreateEventRequest, LoginRequest
TS components PascalCase files MainScreen.tsx, BottomNavBar.tsx
TS services/hooks camelCase files client.ts, useGeolocation.ts
API routes lowercase /api/events, /api/userreviews

Hebrew / RTL considerations

  • All user-facing text is in Hebrew
  • Event statuses: "קורה" (happening), "קרה" (happened), "עתיד לקרות" (upcoming)
  • User roles: "שוכר", "בעל דירה", "משקיע", etc.
  • City/street autocomplete uses data.gov.il CBS dataset (Hebrew names with trailing spaces — handled in code via fn_NormalizeAddressPart)
  • Always test RTL layout when adding UI components

Database rules (MANDATORY)

src/01-database/GroundShareDB.sql is the single source of truth for the entire schema.

  • Never create migration files. All schema changes (new tables, new SPs, modified SPs, new indexes) go directly into GroundShareDB.sql.
  • Every table uses IF OBJECT_ID(...) IS NULL CREATE TABLE ... so the file is safe to re-run on existing databases.
  • Every SP uses CREATE OR ALTER PROCEDURE — idempotent by design.
  • Every index uses IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = '...') CREATE INDEX ....
  • ResetData.sql wipes all user data but keeps schema and event_type seed rows. Update it whenever a new table with an IDENTITY column is added.
  • All city/street write-path SPs must call dbo.fn_NormalizeAddressPart() before insert/lookup.
  • All multi-statement SPs that touch more than one table must use BEGIN TRY / BEGIN TRANSACTION / COMMIT / CATCH / ROLLBACK / THROW.
  • Refresh tokens are stored hashed (SHA-256 hex) — the hash is computed in C# before calling the SP; the SP receives only the hash. Rotation/reuse-detection runs through sp_RotateRefreshToken (single-winner; Replaced_By holds the successor's hash).
  • Address identity: coord-bearing resolves snap to the TLV id_ktovet (layer 527) via TlvAddressService; precedence is TlvAddressId → fine GeoKey → text (see docs/ADDRESS_IDENTITY_PLAN.md).

The user runs all SQL himself in SSMS against PROD. Edit GroundShareDB.sql/ResetData.sql and tell him exactly what to run — never execute SQL or connect to a database.

Adding a full-stack feature (checklist)

  1. Database: Add table/SP directly in src/01-database/GroundShareDB.sql (user runs it in SSMS)
  2. DAL: Create XxxDAL.cs inheriting DBServices, call SP with CreateSP(); add an interface in DAL/Interfaces/ if it'll be injected
  3. Service (if logic is non-trivial): add a XxxService.cs and register it in the matching Extensions/* method
  4. DTO: Add request/response models in BL/; add a FluentValidation validator in Validators/
  5. Controller: Create XxxController.cs with [Route("api/[controller]")] (thin — delegates to the service/DAL)
  6. Frontend API: Add a function in the right services/api/<domain>.ts module and re-export from services/api/index.ts
  7. Types: Add the backend shape to app/types/ (Snake_Case fields)
  8. Screen: Create the screen under app/features/<feature>/, wire up the API call
  9. Route: Add to routes.ts (with ProtectedRoute/GuestRoute wrapper as needed)
  10. PWA: If it's a live-data /api/* endpoint, add it to the NetworkOnly bypass list in vite.config.ts (or the SW will serve stale data across deploys)

Things that are easy to get wrong

  • The backend uses ADO.NET, not EF Core — don't generate EF migrations or DbContext code
  • API responses are often Dictionary<string, object> — keys match DB column names (Snake_Case)
  • The appsettings.json in the repo has placeholder values — real secrets are in appsettings.Development.json (gitignored) / Key Vault (prod)
  • Google Maps API key must come from server /api/geo/maps-config — never embed in client code
  • The Geo* controllers fan out parallel async calls to multiple Tel Aviv GIS layers with 8–10s timeouts via GisFeatureService — be careful changing the dedup/geometry logic there
  • ags.iplan.gov.il (MAVAT) needs legacy TLS — use the named "iplan" HttpClient (relaxed CipherSuitesPolicy), not the default client; TA GIS uses the default client
  • Don't pass outSR=4326 on TA GIS geometry queries — fetch native EPSG:2039 and reproject with IsraeliTM.ToWgs84 (which adds the Israel-1993→WGS84 datum shift)
  • Don't SW-cache maps.googleapis.com/gstatic map tiles (only the TLV satellite CacheFirst rule is intended); the /api/auth/* NetworkOnly rule must stay FIRST in runtimeCaching