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.
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).
| # | 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/onboarding → sp_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 |
🔒=[Authorize](JWT required) ·🌐=[AllowAnonymous]· all routes prefixed/api. Rate-limit policy names in(…); see §7.
| 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 |
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
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)
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-disruptionscross-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 (taggedcoordSource: geometry | geocoded).planning-statusreturns MAVAT plan-lifecycle events (planEvents) when outside Tel Aviv.buildings(TLV layer 513 footprints) andaddress-points(layer 527 house numbers) are viewport/bbox queries fetched in native EPSG:2039 and reprojected viaIsraeliTM— they feed theBuildingsOverlay/AddressNumbersOverlaymap layers and theid_ktovet-based address identity (see §11 andADDRESS_IDENTITY_PLAN.md).
POST /comparisons/compare (score + AI summary) · POST /comparisons (save) · GET /comparisons/history
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
POST /locations/resolve 🌐 · GET /locations/{id} · GET /locations/{id}/stats 🌐 · GET /locations/{id}/plans 🌐
POST /userreviews · PUT /userreviews/{reviewId} · GET /userreviews/location/{locationId} 🌐
POST /ai/address-summary · POST /ai/ask (ai-generate)
POST /files/upload (file-upload) — 5-layer validation (see §5.4)
GET /clustering/neighborhoods · GET /clustering/diagnostics · POST /clustering/run-seed 🔑 · POST /clustering/run-now 🔑
GET /relevance/diagnostics · POST /relevance/run-now 🔑
🔑 = admin-only: requires the
X-Admin-Tokenheader matching theAdmin:Tokenconfig key (Key VaultAdmin--Token; same value as theCLUSTERING_ADMIN_TOKENGitHub 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.
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)
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.CreateEventAsync → Task.Run with a fresh DI scope) to home-address +
GPS-proximity subscribers (200 m radius).
Config is layered:
appsettings.json(placeholders) →appsettings.Development.json(git-ignored, local) → env vars → Azure Key Vault (prod, via Managed Identity). The trackedappsettings.jsoncontains only placeholders.
- Use: primary datastore. Access: pure ADO.NET (
Microsoft.Data.SqlClient), no EF Core; stored procs + parameterized queries viaDBServicesbase. - Config:
ConnectionStrings:DefaultConnection. Host:sql-groundshare-il.database.windows.net. - Code:
src/02-server/DAL/*(22 DAL classes; contracts inDAL/Interfaces/).
- Use: production secret store. Loads at startup when
AZURE_KEY_VAULT_URIis set; overrides appsettings. - Auth:
DefaultAzureCredential(Managed Identity in prod). Code:Program.cs(Key Vault block).
- 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.
- 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.
- Use: logs/traces/metrics via a Serilog sink. Enabled only when
APPLICATIONINSIGHTS_CONNECTION_STRINGis present. - Code:
Program.cs(Serilog config),Logging/LogRedactionEnricher.cs(PII masking).
- Use: map rendering (client), geocoding + Places autocomplete (server-proxied so the key stays hidden), satellite/label overlays.
- Config:
Google:MapsApiKey. Client fetches it viaGET /api/geo/maps-config(auth-gated, rate-limited). - Code:
GeoMapsController.cs+ sharedServices/GisFeatureService.cs; client@vis.gl/react-google-maps.
- 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).
- 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.
- 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 inpublic/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.
- Use: password-reset emails and in-app contact/bug-report submissions (
ContactController→ support inbox,Contact:SupportEmail). Config:Resend:ApiKey/FromEmail/FromName/AppBaseUrl. Fromnoreply@groundshare.app. - Code:
Services/ResendEmailService.cs,Services/PasswordResetEmailTemplate.cs,Controllers/ContactController.cs.
- Tel Aviv ArcGIS (
gisn.tel-aviv.gov.il/.../IView2/MapServer): disruption layers — construction499, permits772, night-work479/858, road-work852/853, city-plans528, neighborhoods511, buildings513, addresses527; plus satellite tiles. Reached through the sharedGisFeatureService, consumed byGeoPlanningController/GeoInfrastructureController,ComparisonsService,ClusteringJobService,NeighborhoodSeedService, andTlvAddressService. Cross-layer dedup: the same physical job is listed across overlapping layers (polygon852+ point853, road-work + night-work479/858);GisFeatureServicecollapses 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, taggedcoordSource="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 namedHttpClient("iplan") with a relaxedCipherSuitesPolicyon Linux (default OpenSSL 3 otherwise rejects the handshake). Code:Extensions/HttpClientRegistrationExtensions.cs(iplanclient),GeoPlanningController,ComparisonsService. - data.gov.il (CBS) city/street datasets feed reference data / autocomplete (client
addressAutocomplete.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 /.
- Auth: JWT (15-min access, HMAC-SHA256) + 7-day rotating refresh tokens. Web → httpOnly
gs_refreshcookie (SameSite=Lax, Secure,/api/auth); native → refresh token in body (X-Client-Platform: native). CSRF viaX-CSRFheader on the cookie path. - Rate limiting (ASP.NET 8 fixed-window per IP):
auth-login5/min ·auth-register3/h (50/h dev) ·auth-refresh20/min ·auth-forgot5/h ·auth-reset10/h ·contact10/h (50/h dev) ·maps-config10/min ·ai-generate10/min ·file-upload20/min · global 100/min. - Validation: FluentValidation (
AddValidatorsFromAssemblyContaining<Program>) + customValidationFilter. - Error handling: global
ExceptionHandlingMiddleware→ uniformApiResponse<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()(prefersCF-Connecting-IP, falls back toRemoteIpAddress) 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:
UtcDateTimeJsonConverternormalizes allDateTime/DateTime?JSON output to UTC ISO-8601 (Extensions/UtcDateTimeJsonConverter.cs), paired with the client-sideserverDate.tsparser.
- 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 incomponents/shared/appIcons.tsx; compare-specific Figma SVGs infeatures/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 inapp/components/shared/; pure per-feature helpers in each<feature>/lib/. - State: React Context (
AuthContext,GeoContext,MapContext) + race-safeuseAsynchook. No Redux/TanStack Query/Zustand. - HTTP: single
request<T>transport (services/api/client.ts) with typedApiError, single-flight 401 refresh, per-domain modules underservices/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 singlegoogle.maps.Datalayer — 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_binyan—id_binyanrepeats 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 —overrideStyleforces re-rasterization of every feature and caused the original flicker. Feature mutation only happens on the debouncedidleevent (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
enabledprop (e.g. when the overlay is toggled off).
- Viewport tile grid: viewport is snapped to a 0.01° grid (~1.1 km tiles).
- 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 asBuildingsOverlay, zoom-gated, with its own anti-flicker handling; toggled independently of the footprint layer via the consolidated building/house-number control inMapControlsToggle. Both overlays are mounted in the persistentMapLayout.
- 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.sqlwipes user data, keeps schema + seeds. - Access pattern:
DBServicesbase →ConnectAsync()→CreateSP()→ async ADO.NET. DAL methods commonly returnDictionary<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).
- 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— backenddotnet build+ xUnit tests (src/04-tests) + vuln check (blocking); frontendnpm ci+ typecheck + vitest + build +npm audit(blocking); gitleaks secret scan.cd.yml— deploy on push tomain, gated by GitHub varsDEPLOY_BACKEND_ENABLED/DEPLOY_FRONTEND_ENABLED; health checks post-deploy. Frontend is served bydeploy/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.tomlallowlist.
- No DB migration versioning —
GroundShareDB.sqlis 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 universalGooglePlaceIdfallback 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 surfacingGush/Chelkain the address panel are also deferred. SeeADDRESS_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 servewith a header-injectingserver.cjs; CSP report-only pending enforcement); the silently-broken audit log fixed (audit_logtable +sp_InsertAuditLogcreated, 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/GeoContextmemoized; GPS deferred until after login; constant-time admin-token compare; brokenSurveysControllerchain deleted; missing FK indexes added. ComparisonsControllerslimmed — compare business logic extracted intoServices/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 acrossGeoController/ComparisonsController/ClusteringJobService). GeoControllersplit — intoGeoMapsController/GeoInfrastructureController/GeoPlanningController(all under/api/geo).- DI registration refactored — moved out of
Program.csintoExtensions/*registration methods; typed exceptions inExceptions/AppExceptions.csmapped to theApiResponseenvelope. - 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; seeSECURITY.md). - Unused scaffold deps (MUI, Radix suite, extra icon/carousel libs) removed —
lucide-react+@tabler/icons-reactare now the only icon libs inpackage.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 oldCLIENT_CLEANUP_REPORT.mdplan is now done).