Add: Web UI (React 19 + Vite + shadcn/ui) + FastAPI REST layer

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>
This commit is contained in:
2026-04-25 12:35:08 +00:00
parent 4bc054f315
commit 5e9963f51b
29 changed files with 5152 additions and 59 deletions
+96
View File
@@ -0,0 +1,96 @@
import { Download, FileText } from "lucide-react";
import { AppraisalDecision } from "@/api/types";
import { Button } from "@/components/ui/button";
import { api } from "@/api/client";
import { formatDate } from "@/lib/utils";
interface Props {
decisions: AppraisalDecision[];
totalInDb: number;
}
export function AppraisalsTable({ decisions, totalInDb }: Props) {
if (!decisions.length) {
return (
<div className="text-center py-12 text-muted-foreground">
לא נמצאו החלטות שמאי מכריע באזור הזה
</div>
);
}
return (
<div className="space-y-3">
<div className="text-sm text-muted-foreground">
נמצאו {decisions.length} מתוך {totalInDb.toLocaleString("he-IL")} החלטות במאגר
</div>
<div className="overflow-x-auto rounded-md border">
<table className="w-full text-sm">
<thead className="bg-muted text-muted-foreground">
<tr>
<th className="p-3 text-start font-medium">תאריך הכרעה</th>
<th className="p-3 text-start font-medium">שמאי מכריע</th>
<th className="p-3 text-start font-medium">ועדה</th>
<th className="p-3 text-start font-medium">גוש/חלקה</th>
<th className="p-3 text-start font-medium">סוג שומה</th>
<th className="p-3 text-start font-medium">כותרת ההחלטה</th>
<th className="p-3 text-end font-medium">PDF</th>
</tr>
</thead>
<tbody>
{decisions.map((d) => (
<tr key={d.id} className="border-t hover:bg-accent/40 align-top">
<td className="p-3 whitespace-nowrap">{formatDate(d.decision_date)}</td>
<td className="p-3 whitespace-nowrap">{d.decisive_appraiser || "—"}</td>
<td className="p-3 whitespace-nowrap">{d.committee || "—"}</td>
<td className="p-3 whitespace-nowrap tabular-nums">
{d.block ?? "—"}
{d.plot ? ` / ${d.plot}` : ""}
</td>
<td className="p-3">{d.appraisal_type || "—"}</td>
<td className="p-3 max-w-md">
<span className="line-clamp-2">{d.appraisal_header || "—"}</span>
</td>
<td className="p-3 text-end whitespace-nowrap">
{d.pdf_url ? (
<div className="flex justify-end gap-1">
<Button
size="sm"
variant="outline"
asChild
title="פתח PDF בלשונית חדשה"
>
<a
href={api.pdfProxyUrl(d.pdf_url)}
target="_blank"
rel="noopener noreferrer"
>
<FileText />
פתח
</a>
</Button>
<Button
size="sm"
variant="ghost"
asChild
title="הורד PDF"
>
<a
href={api.pdfProxyUrl(d.pdf_url) + "&download=1"}
download={`${d.block || "appraisal"}-${d.plot || "0"}-${d.id}.pdf`}
>
<Download />
</a>
</Button>
</div>
) : (
"—"
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}