Skip to content

Latest commit

 

History

History
318 lines (249 loc) · 29.1 KB

File metadata and controls

318 lines (249 loc) · 29.1 KB

GroundShare — Technical Feature Inventory

Exhaustive technical inventory of every feature, background service, and external integration in GroundShare. Generated as the Phase 3 deliverable of the production review. Cross-checked against the live source (controllers, services, vite.config.ts, config) — nothing here is aspirational.

Companion docs: SETUP.md (run it) · DEPLOY.md (ship it) · SECURITY.md (secure it) · CLAUDE.md (AI/agent context). This file is the what exists; those are the how to operate.


1. What GroundShare Is

A Hebrew-first (RTL), mobile-first PWA + native iOS/Android app (Capacitor) for urban-planning transparency in Israel. A resident looks up an address and sees: official municipal disruptions (construction, permits, night-work, road-work), planning status, community reports/reviews, and AI-generated address comparisons — on a live map.

Layers: src/01-database (SQL Server) · src/02-server (ASP.NET Core 8 API) · src/03-client (React 19 + Vite 7 PWA / Capacitor 8). Hosted on Azure (Israel Central).


2. Core Feature Map

# Feature Frontend surface Backend controller(s) Key data sources
1 Authentication & accounts /login, /register, /forgot-password, /reset-password, /complete-profile AuthController, UsersController Azure SQL, Google OAuth, Resend, BCrypt
2 Address search & info (tabbed bottom sheet, active-items count chip) /address-search (deep-linkable) GeoMaps/GeoInfrastructure/GeoPlanning, LocationsController, PlanStatusController Google Maps, Tel Aviv GIS, iplan/MAVAT
3 Interactive map MapLayout (persistent), /main, /nearby-reports GeoMaps/GeoInfrastructure/GeoPlanning Google Maps, Tel Aviv GIS satellite tiles
4 Community event reporting /create-event, event cards EventsController, FilesController Azure SQL, Azure Blob, Content Safety, FCM
5 Address comparison (0–100 score + AI) /compare ComparisonsController Tel Aviv GIS, iplan, Gemini, Azure SQL
6 Reviews & comments review/comment sections UserReviewsController, CommentsController Azure SQL, Content Safety
7 Voting event cards EventsController (/vote), VotesController Azure SQL
8 Favorites / bookmarks /favorites FavoritesController Azure SQL
9 Notifications (push + in-app feed) /notifications, bell badge NotificationsController Azure SQL, Firebase FCM
10 Gamification (XP, levels, badges, missions, streak) /personal UsersController (/activity, /missions/claim) Azure SQL
11 Neighborhood clustering (ML) cluster overlay toggle ClusteringController Tel Aviv GIS, Azure SQL, K-Means
12 Event-relevance check-ins relevance modal EventsController, RelevanceController Azure SQL, FCM
13 AI assistant (Q&A + summaries) /dictionary (Q&A), compare/address-summary surfaces AiController, ComparisonsController Google Gemini
14 Onboarding questions /questions, /questions-thanks UsersController (PUT /users/onboardingsp_SaveOnboarding) Azure SQL
15 Reference data (cities/streets/event-types/plan-status) autocomplete, pickers CitiesController, StreetsController, EventTypesController, PlanStatusController Azure SQL, data.gov.il (CBS)
16 File uploads (images) image upload modal FilesController Azure Blob, Content Safety
17 Dictionary (+ AI Q&A) / FAQ / legal /dictionary (term lookup + Gemini Q&A box), /settings/faq, /settings/legal AiController (/ai/ask) static, Google Gemini
18 Settings & profile /settings, /settings/home-address UsersController Azure SQL
18a Contact / bug-report (in-app form, replaces dead mailto:) ContactModal (from /settings) ContactController Resend
19 PWA offline + install service worker, install banner — (Workbox) SW caches (see §6)
20 Health / observability HealthController App Insights, Serilog

3. Complete API Surface (22 controllers)

🔒 = [Authorize] (JWT required) · 🌐 = [AllowAnonymous] · all routes prefixed /api. Rate-limit policy names in (…); see §7.

Auth — AuthController 🌐 (endpoint-level rate-limited)

Method Route Notes
POST /auth/register (auth-register) 3/h prod · register + issue tokens + Content-Safety name check
POST /auth/login (auth-login) 5/min · constant-time BCrypt verify
POST /auth/google (auth-login) · Google ID-token validated via official lib
POST /auth/refresh (auth-refresh) 20/min · rotation; cookie path requires X-CSRF
POST /auth/logout (auth-refresh) · revoke + clear cookie
POST /auth/forgot-password (auth-forgot) 5/h · always 200 (no enumeration)
POST /auth/reset-password (auth-reset) 10/h · consume token + revoke all sessions

Users — UsersController 🔒

GET /users/me · GET /users/{id} · PUT /users/onboarding · PUT /users/home-address · PUT /users/phone · PUT /users/onboarding/skip · DELETE /users/me · GET /users/activity (XP/badges/missions) · POST /users/missions/claim

Events — EventsController 🔒 (some 🌐)

POST /events · GET /events/location/{id} 🌐 · GET /events/nearby 🌐 · POST /events/{id}/vote · GET /events/{id}/comments · POST /events/{id}/comments · GET /events/my-pending-confirmations · POST /events/{id}/confirm-relevance (ownership-enforced)

Geo — three controllers, all under /api/geo 🔒

The original GeoController was split into three thin controllers backed by a shared GisFeatureService (no duplicated GIS logic):

Controller Routes
GeoMapsController GET /geo/maps-config (maps-config) · GET /geo/geocode · GET /geo/autocomplete · GET /geo/reverse
GeoInfrastructureController GET /geo/parcel · GET /geo/buildings · GET /geo/address-points
GeoPlanningController GET /geo/planning-status · GET /geo/nearby-disruptions

nearby-disruptions cross-layer-dedups overlapping GIS rows (same job in polygon+point+night-work layers → one entry, best geometry kept) and geocode-backfills coords for rows missing geometry (tagged coordSource: geometry | geocoded). planning-status returns MAVAT plan-lifecycle events (planEvents) when outside Tel Aviv. buildings (TLV layer 513 footprints) and address-points (layer 527 house numbers) are viewport/bbox queries fetched in native EPSG:2039 and reprojected via IsraeliTM — they feed the BuildingsOverlay / AddressNumbersOverlay map layers and the id_ktovet-based address identity (see §11 and ADDRESS_IDENTITY_PLAN.md).

Comparisons — ComparisonsController 🔒

POST /comparisons/compare (score + AI summary) · POST /comparisons (save) · GET /comparisons/history

Notifications — NotificationsController 🔒

GET /notifications · GET /notifications/feed · POST /notifications/feed/{id}/seen · POST /notifications/feed/{id}/dismiss · GET /notifications/check/{locationId} · POST /notifications/toggle/{locationId} · POST /notifications/device · DELETE /notifications/device · GET /notifications/gps · PUT /notifications/gps · DELETE /notifications/gps

Locations — LocationsController 🔒 (reads 🌐)

POST /locations/resolve 🌐 · GET /locations/{id} · GET /locations/{id}/stats 🌐 · GET /locations/{id}/plans 🌐

Reviews — UserReviewsController 🔒

POST /userreviews · PUT /userreviews/{reviewId} · GET /userreviews/location/{locationId} 🌐

AI — AiController 🔒

POST /ai/address-summary · POST /ai/ask (ai-generate)

Files — FilesController 🔒

POST /files/upload (file-upload) — 5-layer validation (see §5.4)

Clustering (ML) — ClusteringController 🌐

GET /clustering/neighborhoods · GET /clustering/diagnostics · POST /clustering/run-seed 🔑 · POST /clustering/run-now 🔑

Relevance (admin) — RelevanceController 🌐

GET /relevance/diagnostics · POST /relevance/run-now 🔑

🔑 = admin-only: requires the X-Admin-Token header matching the Admin:Token config key (Key Vault Admin--Token; same value as the CLUSTERING_ADMIN_TOKEN GitHub secret used by the daily cron workflows). The run-now endpoints return 202 and run fire-and-forget — a detached task with its own DI scope — so the App Service ~230 s request timeout can't kill a long recompute; the job's last-run timestamp is updated only on successful completion.

Reference & misc

FavoritesController 🔒 (GET /favorites, GET /favorites/check/{id}, POST /favorites/toggle) · CommentsController 🔒 (GET /comments/event/{eventId}) · CitiesController 🔒 (GET /cities) · StreetsController 🔒 (GET /streets/city/{cityId}) · EventTypesController (GET /eventtypes 🌐) · PlanStatusController 🔒 (GET /planstatus/location/{locationId}) · ContactController 🔒 (POST /contact (contact) — in-app contact/bug-report form; HTML-encodes input, emails support via Resend) · HealthController 🌐 (GET /health)


4. Background & Hosted Services

All three are in-process hosted services (no external scheduler), each backed by a GitHub Actions cron that pokes its admin endpoint (because the student-tier App Service sleeps on idle).

Service Type Cadence What it does File
NeighborhoodSeedService IHostedService (one-shot) startup, idempotent Fetches ~71 Tel Aviv neighborhood polygons from TA GIS layer 511, reprojects EPSG:2039→WGS84, seeds DB Services/NeighborhoodSeedService.cs
ClusteringJobService BackgroundService wakes hourly, runs if last result >1 day old K-Means (auto-K via silhouette, K∈[2,6]) over polygon-bounded GIS + community signals → weighted livability score (BL/LivabilityModel.cs) + good/medium/bad label from absolute bands; skips neighborhoods with incomplete GIS and aborts the run below 60% coverage; invalidates cache Services/ClusteringJobService.cs
RelevanceJobService BackgroundService wakes 6h, runs if last prompt >20h old Finds 3+ day-old open events w/o future end-date, FCM-pushes the reporter to confirm relevance; idempotent via sp_MarkConfirmationPromptSent Services/RelevanceJobService.cs

Companion crons: .github/workflows/cluster-daily.yml, .github/workflows/relevance-daily.yml — authenticate to the run-now endpoints with the CLUSTERING_ADMIN_TOKEN secret (X-Admin-Token header). Without the token configured the pokes get 401 and, since the free tier has no Always On, the jobs never run.

Other notable async work: event-creation fires a fire-and-forget FCM fan-out (EventService.CreateEventAsyncTask.Run with a fresh DI scope) to home-address + GPS-proximity subscribers (200 m radius).


5. External & Third-Party Integrations (11)

Config is layered: appsettings.json (placeholders) → appsettings.Development.json (git-ignored, local) → env vars → Azure Key Vault (prod, via Managed Identity). The tracked appsettings.json contains only placeholders.

5.1 Azure SQL Database

  • Use: primary datastore. Access: pure ADO.NET (Microsoft.Data.SqlClient), no EF Core; stored procs + parameterized queries via DBServices base.
  • Config: ConnectionStrings:DefaultConnection. Host: sql-groundshare-il.database.windows.net.
  • Code: src/02-server/DAL/* (22 DAL classes; contracts in DAL/Interfaces/).

5.2 Azure Key Vault

  • Use: production secret store. Loads at startup when AZURE_KEY_VAULT_URI is set; overrides appsettings.
  • Auth: DefaultAzureCredential (Managed Identity in prod). Code: Program.cs (Key Vault block).

5.3 Azure Blob Storage

  • Use: image/file uploads. Provider switch: Storage:Provider = Local | Azurite | Azure.
  • Behavior: private container, GUID filenames, time-limited SAS URLs (Storage:SasExpiryMinutes, default 60).
  • Code: Services/AzureBlobStorageService.cs, Services/LocalBlobStorageService.cs, FilesController.cs.

5.4 Azure AI Content Safety

  • Use: text + image moderation (registration names, event descriptions, comments, uploads).
  • Layering: local Hebrew blocklist runs first (Azure scores Hebrew abuse near 0), then SHA-256-keyed 24h in-memory cache, then the API. Fail-open (allow if scanner unreachable/quota exhausted).
  • Config: ContentSafety:Endpoint/Key/RejectAtSeverity/Enabled. Code: Services/ContentSafetyService.cs, Services/HebrewBlocklist.cs.

5.5 Application Insights

  • Use: logs/traces/metrics via a Serilog sink. Enabled only when APPLICATIONINSIGHTS_CONNECTION_STRING is present.
  • Code: Program.cs (Serilog config), Logging/LogRedactionEnricher.cs (PII masking).

5.6 Google Maps Platform

  • Use: map rendering (client), geocoding + Places autocomplete (server-proxied so the key stays hidden), satellite/label overlays.
  • Config: Google:MapsApiKey. Client fetches it via GET /api/geo/maps-config (auth-gated, rate-limited).
  • Code: GeoMapsController.cs + shared Services/GisFeatureService.cs; client @vis.gl/react-google-maps.

5.7 Google Gemini API

  • Use: AI address-comparison summaries + Q&A (gemini-3.1-flash-lite). Falls back to a deterministic local summary if the key is missing or the call fails.
  • Config: Google:GeminiApiKey. Code: Services/ComparisonsService (compare summary), AiController.cs (Q&A).

5.8 Google OAuth 2.0

  • Use: "Sign in with Google". ID token validated locally via Google.Apis.Auth (signature/expiry/issuer/audience) — no outbound tokeninfo call.
  • Config: Google:ClientId (public). Code: AuthController.GoogleSignIn.

5.9 Firebase Cloud Messaging (FCM)

  • Use: push notifications (new nearby events, relevance check-ins). Optional — disabled if not configured; auto-deletes invalid tokens.
  • Config: Fcm:ServiceAccountJson / Fcm:ServiceAccountFile (FirebaseAdmin). Client config in public/firebase-messaging-sw.js, android/app/google-services.json, ios/.../GoogleService-Info.plist (Firebase client keys — public by design).
  • Code: Services/FcmSender.cs, public/firebase-messaging-sw.js.

5.10 Resend (transactional email)

  • Use: password-reset emails and in-app contact/bug-report submissions (ContactController → support inbox, Contact:SupportEmail). Config: Resend:ApiKey/FromEmail/FromName/AppBaseUrl. From noreply@groundshare.app.
  • Code: Services/ResendEmailService.cs, Services/PasswordResetEmailTemplate.cs, Controllers/ContactController.cs.

5.11 Israeli Government GIS

  • Tel Aviv ArcGIS (gisn.tel-aviv.gov.il/.../IView2/MapServer): disruption layers — construction 499, permits 772, night-work 479/858, road-work 852/853, city-plans 528, neighborhoods 511, buildings 513, addresses 527; plus satellite tiles. Reached through the shared GisFeatureService, consumed by GeoPlanningController/GeoInfrastructureController, ComparisonsService, ClusteringJobService, NeighborhoodSeedService, and TlvAddressService. Cross-layer dedup: the same physical job is listed across overlapping layers (polygon 852 + point 853, road-work + night-work 479/858); GisFeatureService collapses these by a stable address+descriptor key and keeps the best geometry, so a job isn't counted or shown 2–3× (one implementation, reused everywhere). Coord backfill: GIS rows without usable geometry are Google-geocoded from their address so they still get a marker + distance, tagged coordSource="geocoded".
  • iplan / MAVAT (ags.iplan.gov.il): national planning fallback (used when TLV layers return nothing — i.e. outside Tel Aviv). Surfaces plan-lifecycle milestones (planEvents: deposited / approved / gazette dates). Requires legacy TLS 1.2 + RSA ciphers — served through a dedicated named HttpClient ("iplan") with a relaxed CipherSuitesPolicy on Linux (default OpenSSL 3 otherwise rejects the handshake). Code: Extensions/HttpClientRegistrationExtensions.cs (iplan client), GeoPlanningController, ComparisonsService.
  • data.gov.il (CBS) city/street datasets feed reference data / autocomplete (client addressAutocomplete.ts).

6. PWA / Service-Worker Caching (vite.config.ts)

Workbox (vite-plugin-pwa, registerType: autoUpdate). App shell precached (**/*.{js,css,html,ico,png,svg,woff2}). Runtime strategies:

Pattern Strategy Why
gisn.tel-aviv.gov.il/* (satellite tiles) CacheFirst (30 d, 2000 entries) Imagery rarely changes
maps.gstatic.com/* CacheFirst (30 d) Static map assets
maps.googleapis.com/* StaleWhileRevalidate (24 h) Maps JS
/api/geo/maps-config StaleWhileRevalidate (1 h, 1 entry) Key rarely changes
/api/auth/* NetworkOnly (FIRST in the list) The boot /auth/refresh is credentialed — a NetworkFirst cache would drop the cookie and log the user out on reload
/api/geo/(planning-status|nearby-disruptions|parcel) NetworkOnly Coordinate/time-sensitive — stale = wrong city's data
/api/events/(my-pending-confirmations|{id}/confirm-relevance) NetworkOnly Must reflect live DB
/api/notifications/feed NetworkOnly Bell badge must be live
/api/(favorites|notifications) NetworkOnly Per-user live state — never serve a previous user's cached set
/api/* (everything else) NetworkFirst (5 s timeout → cache, 1 h, api-responses-v2) Best-effort offline

⚠️ Maintenance rule: any new live-data /api/* endpoint must be added to the NetworkOnly bypass list, or the SW will serve stale data across deploys.

Firebase messaging SW (public/firebase-messaging-sw.js) is separate: background-message display + notificationclick deep-linking. Payloads carry event_id + lat/lng, so a tap lands the user on the report via /address-search (same surface the in-app feed opens); falls back to /main for event_relevance_check without coords, else /.


7. Cross-Cutting Backend Capabilities

  • Auth: JWT (15-min access, HMAC-SHA256) + 7-day rotating refresh tokens. Web → httpOnly gs_refresh cookie (SameSite=Lax, Secure, /api/auth); native → refresh token in body (X-Client-Platform: native). CSRF via X-CSRF header on the cookie path.
  • Rate limiting (ASP.NET 8 fixed-window per IP): auth-login 5/min · auth-register 3/h (50/h dev) · auth-refresh 20/min · auth-forgot 5/h · auth-reset 10/h · contact 10/h (50/h dev) · maps-config 10/min · ai-generate 10/min · file-upload 20/min · global 100/min.
  • Validation: FluentValidation (AddValidatorsFromAssemblyContaining<Program>) + custom ValidationFilter.
  • Error handling: global ExceptionHandlingMiddleware → uniform ApiResponse<T> envelope; generic prod messages, dev-only exception text, no stack traces to client.
  • Logging: Serilog (two-stage bootstrap) + PII-redaction enricher; request-logging middleware (excludes auth header / body / cookies).
  • Security headers: X-Content-Type-Options, X-Frame-Options DENY, Referrer-Policy, Permissions-Policy, CSP default-src 'none', HSTS (prod only).
  • Strongly-typed options with ValidateOnStart(): Jwt, GoogleApi, Storage, Resend, ContentSafety.
  • Typed config classes: Options/*.cs.
  • Real client IP behind proxies: forwarded-headers middleware + HttpContext.GetClientIp() (prefers CF-Connecting-IP, falls back to RemoteIpAddress) so rate-limit partitioning and audit logs see the true caller behind Cloudflare → App Service (Extensions/HttpContextExtensions.cs). Gates rate limiting/audit only — never authorization.
  • UTC date serialization: UtcDateTimeJsonConverter normalizes all DateTime/DateTime? JSON output to UTC ISO-8601 (Extensions/UtcDateTimeJsonConverter.cs), paired with the client-side serverDate.ts parser.

8. Frontend Capabilities

  • Stack: React 19, Vite 7, TypeScript, React Router 7 (react-router, createBrowserRouter), Tailwind 4, Motion. Maps via @vis.gl/react-google-maps. Icons: lucide-react + @tabler/icons-react (centralized in components/shared/appIcons.tsx; compare-specific Figma SVGs in features/compare/icons.tsx).
  • Layout: screens organized into feature folders under app/features/* (auth, main, address-search, nearby, compare, create-event, favorites, notifications, personal, settings, dictionary); cross-feature UI in app/components/shared/; pure per-feature helpers in each <feature>/lib/.
  • State: React Context (AuthContext, GeoContext, MapContext) + race-safe useAsync hook. No Redux/TanStack Query/Zustand.
  • HTTP: single request<T> transport (services/api/client.ts) with typed ApiError, single-flight 401 refresh, per-domain modules under services/api/*.
  • Auth storage: access token in memory only (XSS hardening); refresh token in httpOnly cookie (web) / sandboxed localStorage (native).
  • Native (Capacitor 8): Camera, Geolocation, Push, Filesystem, Keyboard, StatusBar, SplashScreen, social login. iOS + Android shells.
  • Gamification: personal/lib/levelSystem.ts — 7 levels (תושב חדש → אגדה עירונית), badges from stats, weekly missions (ISO-week rotation), XP + streak.
  • Geo: dual coarse + high-accuracy watchers (native + web); reverse-geocoding fallback for NULL-coordinate locations.
  • Address sheet: draggable bottom sheet (peek/mid/full snaps) with a 4-tab content area (סקירה / חוות דעת / דיווחים / תכנון); the תכנון tab carries a live count badge of active (non-completed) planning + disruption items (countActivePlanningItems).
  • Nearby reports: community events + official disruptions are decorated with a distance from the user and ordered by it; per-navigation scroll/snap reset via resetSignal.
  • BuildingsOverlay (components/shared/BuildingsOverlay.tsx): draws Tel Aviv building footprint outlines (TLV GIS layer 513) on the map using a single google.maps.Data layer — not N React <Polygon> components — so hundreds of features are handled cheaply. Key design decisions:
    • Viewport tile grid: viewport is snapped to a 0.01° grid (~1.1 km tiles). tilesForBounds() computes which tiles overlap the current view; already-loaded tile keys are tracked in a ref so panning back into a visited area requires no refetch.
    • Parallel tile fetching: all new tiles for the viewport fire via Promise.all (sequential awaits caused visible stagger on multi-tile viewports).
    • Dedup by geometry string: buildings are deduplicated on their exact GeoJSON geometry string, not id_binyanid_binyan repeats across a structure's separate footprint polygons and was silently dropping real buildings; byte-identical geometry across overlapping tiles is still collapsed.
    • Anti-flicker: the Data layer is attached to the map once on mount and never detached. Zoom-threshold visibility is toggled via a single setStyle({ visible }) call (detach/reattach on a vector basemap causes a hard flash). Per-feature opacity animation is deliberately avoided — overrideStyle forces re-rasterization of every feature and caused the original flicker. Feature mutation only happens on the debounced idle event (300 ms), never mid-gesture.
    • Memory cap: if the added-feature set exceeds 3 000 entries (from city-wide panning), all features and the tile cache are cleared and only the current viewport reloads.
    • Gate: hidden below zoom 16 (footprints too small / numerous); also suppressed by the enabled prop (e.g. when the overlay is toggled off).
  • AddressNumbersOverlay (components/shared/AddressNumbersOverlay.tsx): companion overlay that renders TLV house numbers (GIS layer 527) as labels on the map. Same viewport-fetch + EPSG:2039→WGS84 reprojection approach as BuildingsOverlay, zoom-gated, with its own anti-flicker handling; toggled independently of the footprint layer via the consolidated building/house-number control in MapControlsToggle. Both overlays are mounted in the persistent MapLayout.

9. Data Layer

  • Engine: SQL Server (Azure SQL in prod). Schema source of truth: src/01-database/GroundShareDB.sql (re-runnable: IF OBJECT_ID … CREATE, CREATE OR ALTER PROCEDURE, guarded indexes). ResetData.sql wipes user data, keeps schema + seeds.
  • Access pattern: DBServices base → ConnectAsync()CreateSP() → async ADO.NET. DAL methods commonly return Dictionary<string, object> keyed by Snake_Case DB columns.
  • Core tables: user, location, event, comment, user_review, survey, city, streets, event_type, refresh/reset tokens, notifications, favorites, votes, neighborhood polygons/clusters.
  • Note: no migration versioning — schema history/rollback is by hand (see §11 Known Gaps).

10. CI/CD & Infrastructure

  • Hosting (Azure, Israel Central): App Service ×2 (API + Node-hosted Vite dist/), Azure SQL, Blob, Key Vault, App Insights, Content Safety.
  • Workflows (.github/workflows/):
    • ci.yml — backend dotnet build + xUnit tests (src/04-tests) + vuln check (blocking); frontend npm ci + typecheck + vitest + build + npm audit (blocking); gitleaks secret scan.
    • cd.yml — deploy on push to main, gated by GitHub vars DEPLOY_BACKEND_ENABLED / DEPLOY_FRONTEND_ENABLED; health checks post-deploy. Frontend is served by deploy/server.cjs (SPA fallback + security headers); the App Service Startup Command points at it explicitly.
    • cluster-daily.yml, relevance-daily.yml — pokes the hosted-service admin endpoints.
    • codeql.yml — code security analysis.
  • Secret hygiene: gitleaks at pre-commit (tools/git-hooks/pre-commit) and CI; documented prior-incident rotation; .gitleaks.toml allowlist.

11. Known Gaps

  • No DB migration versioningGroundShareDB.sql is a single re-runnable script; schema history/rollback is by hand.
  • Address identity — partial coverage. Coord-bearing resolves snap to the TLV id_ktovet (layer 527) inside Tel Aviv-Yafo; the universal GooglePlaceId fallback for non-TLV addresses is specced but not yet built (a fine DECIMAL(9,5) GeoKey covers it for now). Click-to-address via building polygon (vs. nearest 527 point) and surfacing Gush/Chelka in the address panel are also deferred. See ADDRESS_IDENTITY_PLAN.md.

Recently closed (were open in earlier reviews):

  • 2026-07 audit-fix wave — security headers now served in production (frontend host replaced pm2 serve with a header-injecting server.cjs; CSP report-only pending enforcement); the silently-broken audit log fixed (audit_log table + sp_InsertAuditLog created, repeated-failure escalation added); CI vulnerability gates made blocking; backend xUnit test project (src/04-tests, 51 tests) + frontend vitest wired into CI; route-level code splitting (~30 chunks, 176 KB gzip boot bundle); AuthContext/GeoContext memoized; GPS deferred until after login; constant-time admin-token compare; broken SurveysController chain deleted; missing FK indexes added.
  • ComparisonsController slimmed — compare business logic extracted into Services/ComparisonsService.cs; the controller is now a thin HTTP handler.
  • Shared GIS service extracted — GIS fetch + ArcGIS parsing + cross-layer disruption dedup + geometry math now live once in Services/GisFeatureService.cs, consumed by the three split Geo* controllers and the clustering job (no more copy-paste across GeoController/ComparisonsController/ClusteringJobService).
  • GeoController split — into GeoMapsController / GeoInfrastructureController / GeoPlanningController (all under /api/geo).
  • DI registration refactored — moved out of Program.cs into Extensions/* registration methods; typed exceptions in Exceptions/AppExceptions.cs mapped to the ApiResponse envelope.
  • Refresh-token rotation hardened — single-winner atomic rotation (sp_RotateRefreshToken) + grace-window successor hand-back + reuse detection / token-family revocation (was previously a deferred backlog item; see SECURITY.md).
  • Unused scaffold deps (MUI, Radix suite, extra icon/carousel libs) removedlucide-react + @tabler/icons-react are now the only icon libs in package.json.
  • Length validation added — comments/reviews/event-description capped (FluentValidation) with client-side counters.
  • Overlapping-GIS-layer double-counting fixed — cross-layer disruption dedup collapses the same job listed across polygon/point/night-work layers.
  • Client refactor complete — screens reorganized into app/features/* folders; shared extractions (PinPromptPopup, HamburgerMenuButton, ScrollHint, LoadingOverlay, WinnerBanner, StatBox, levelSystem.ts) landed (the old CLIENT_CLEANUP_REPORT.md plan is now done).