UI:
- Center-align all numeric table columns (deals + appraisals) for better
visual balance — text-start was crowding numbers against the right edge.
Backend (search_appraisals):
- Add include_nearby + nearby_block_radius flags. When set, fetches three
buckets in one request:
1. exact match (block + plot, original behavior)
2. same block, other plots
3. nearby blocks (block ± radius, default ±2)
- Composite-key dedupe across buckets (gov.il has no stable decision id).
Frontend:
- AppraisalsTable now renders the three buckets as stacked sections with
Hebrew headers ("החלטות תואמות" / "באותו גוש" / "בגושים סמוכים").
- Tab counter sums all three buckets.
Auto-enables include_nearby whenever a block is in scope (address mode
via gush_helka lookup, or block_plot mode directly).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
text-end in dir="rtl" aligns content to the LEFT (end of the line in RTL),
which made numeric columns visually detach from their headers and feel
mis-aligned. Switch all numeric columns to text-start so numbers sit on
the right edge — matching the RTL reading direction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Govmap autocomplete echoes back "גוש N חלקה M" without resolving to a street,
and there's no public reverse-geocoding endpoint. But cadastral parcel
numbers are assigned along street frontage, so the deal with parcelNum
closest to the searched plot (within the same gush) is almost always on
the same street. Use that as the primary signal; fall back to autocomplete
text parsing for address-mode searches.
Example: search gush 6212 plot 894. Plot 896 (הילדסהיימר 12) is the closest
match → "הילדסהיימר" is the searched street → labels work correctly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous approach: pick most common streetNameHeb among street-source deals.
Problem: a busier nearby street wins even when the searched parcel is on a
quieter street (e.g. רב צעיר beats הילדסהיימר despite being a different street).
Fix: backend now returns resolved_address from Govmap autocomplete (e.g.
"הילדסהיימר 14, ירושלים"). Frontend parses the leading non-numeric tokens
as the street name and uses that as ground truth for proximity labeling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Instead of showing "סביבה קרובה" for all street-polygon deals, we now
infer the searched street from the most common streetNameHeb among
deal_source="street" deals. Deals on that street get "אותו רחוב" (blue);
deals on other streets get "סביבה קרובה" (lighter blue). Works correctly
for both address searches and gush+plot searches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add גוש, חלקה, תת-חלקה columns (gushNum/parcelNum/subParcelNum from Govmap)
- Fix house number access: was reading `house_num` (non-existent field),
now reads `houseNum` (actual extra field from Govmap API)
- Rename proximity labels: "באותו רחוב" → "סביבה קרובה", "בשכונה" → "שכונה"
because "street" polygon searches return deals from multiple streets,
making "same street" label factually incorrect
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The deals tab was previously disabled when searching by block+plot
because find_recent_deals_for_address needs an address string. Govmap's
autocomplete in fact accepts the literal "גוש N חלקה M" and returns
the parcel coordinates, so we just compose that string client-side
and let the rest of the pipeline (coords → polygon → deals) run as-is.
Verified live: search "Block=6212 Plot=894" now returns the same two
deals on יהודה המכבי that an address search would.
Appraiser-name search still disables the deals tab — there's no
location to anchor a deals query on.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five-layer defense so that no single library/browser quirk can flip the
layout back to LTR:
1. <body dir="rtl"> in addition to <html dir="rtl"> — some libs read
document.body.dir, not document.documentElement.dir.
2. Explicit `direction: rtl` on html, body in the @layer base CSS.
3. <DirectionProvider dir="rtl"> from @radix-ui/react-direction wrapping
the React tree, so every Radix primitive (Tabs, Dialog, Select,
Popover, Toast) gets RTL context — Radix's useDirection defaults to
"ltr" without it.
4. shadcn Tabs wrapper now defaults its dir prop to "rtl" — redundant
with the provider above, but defends against a future provider
removal silently breaking Tabs.
5. dir="rtl" set explicitly on each <table> element.
Fixes the symptom seen on the live site: deals/appraisals tables had
תאריך (the first <th> in the JSX) rendering on the LEFT instead of the
RIGHT, and Tabs flowed עסקאות→שמאי left-to-right instead of the
expected right-to-left.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The catch-all `lib/` pattern in both .gitignore and .dockerignore (intended
to exclude Python venv lib dirs) silently swallowed `web/src/lib/utils.ts`.
That's why every Docker build got "ENOENT /web/src/lib/utils" — the file
was never committed and never copied into the container, but local builds
worked because the file existed on disk.
Anchor `lib/` and `lib64/` to the repo root with `/lib/` and `/lib64/`,
then add the previously-ignored utils.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vite's path alias resolution worked locally but consistently failed in
the Docker stage with "Could not load /web/src/lib/utils ... ENOENT"
even after fixing __dirname for ESM. Some interaction between act/buildx
and Vite's resolver around tsconfig paths. Relative imports sidestep the
issue entirely and are explicit about the dependency direction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`__dirname` is undefined in ESM modules, so `path.resolve(__dirname, "./src")`
silently produced a bad path in Docker. Locally it happened to work (some
Node interop), but the Docker stage hit a clear error: "Could not load
/web/src/lib/utils ... ENOENT". Use fileURLToPath(import.meta.url).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Docker build was repeatedly failing on `@/*` path-alias resolution in
tsc, even though local builds (and the same script in the same Node 22
container) succeeded. Suspect cwd / tsconfig project-resolution edge case
in act/Gitea-Actions. Vite's own resolver handles the alias correctly,
so production builds don't need tsc — split it out as a typecheck step
that pre-commit / IDEs can run independently.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The composite-mode + project-references setup broke `@/*` path alias
resolution inside the Docker stage 1 build (worked locally because tsc's
incremental cache was warm). Switch to a single tsconfig with
`tsc --noEmit && vite build` — Vite handles the alias via its own
resolver, so we only use tsc as a type-checker.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a self-contained web interface that wraps the MCP tools so end users
don't need to issue MCP commands. Single Docker container serves:
/ → React SPA (RTL Hebrew, Heebo font, shadcn/ui components)
/api/* → FastAPI REST endpoints used by the SPA
/api/docs → OpenAPI / Swagger
/mcp → existing FastMCP streamable-http endpoint
/health → liveness probe
REST endpoints (in nadlan_mcp/web_app.py):
GET /api/search/address?q=... — autocomplete + best-effort gush/חלקה
via Govmap PARCEL_ALL layer
POST /api/search/deals — Govmap deals around an address
POST /api/search/appraisals — Decisive-appraiser search (gov.il)
GET /api/appraisals/pdf?url=.. — Stream PDFs through this server,
carrying the upstream x-client-id
header (a normal browser fetch fails)
UI flow:
- Address search → resolves gush/חלקה → shows BOTH deals (Govmap) AND
appraiser decisions (gov.il) for the same parcel side-by-side in tabs.
- Block+plot search → goes straight to appraisals.
- Appraiser-name search → all decisions by that appraiser.
Stack chosen for RTL-first usability and longevity: React 19, Vite 6,
TypeScript, Tailwind, shadcn/ui (Radix primitives), TanStack Query,
Lucide icons. ~295KB gzipped JS, no SSR overhead.
Multi-stage Dockerfile: Node 22 builds web/ in stage 1, Python 3.13
runtime serves the dist + the FastAPI app in stage 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>