אחרי העלאת החלטה סופית והרצת שני הפייפליינים האוטומטיים (למידת-קול,
חילוץ/אימות-הלכות), התיק לא הציג אם כל תהליך בוצע/הצליח/למה-נכשל. במיוחד
תקלת chair_name ריק (2026-06-12) שמפילה בשקט את העתק-ה-case_law → חילוץ-הלכות
לא מתחיל בכלל, בלי שזה גלוי. כעת מוצגות שתי אינדיקציות ליד כפתורי-ההרצה.
Backend (גזירה ממקור-יחיד, ללא מסלול-מעקב מקביל):
- SCHEMA_V36: draft_final_pairs.learning_run (JSONB) — שדה-תיעוד על פנקס-ההתאמה
(INV-LRN4), חותם את תוצאת-הריצה של פייפליין-הלמידה (succeeded/failed+סיבה+at).
- set_learning_run_outcome() — חיתום הצלחה/כישלון על ה-pair האחרון.
- case_learning_status() — גזירה read-only מ-draft_final_pairs/style_corpus/
decision_lessons/case_law/halachot: בוצע? הצליח? למה-לא? כמה הלכות חולצו.
- final_learning_pipeline.py — חותם outcome בהצלחה וב-except (surfaced, לא בלוע).
- חשיפה: case_get מוסיף learning_status (→MCP + /api/cases/{case}/details) +
endpoint ייעודי GET /api/cases/{case}/learning-status (אותה פונקציה — בלי כפילות).
UI (אושר דרך שער-העיצוב Claude Design — כרטיס 21-final-learning-status):
- useCaseLearningStatus (api/learning.ts) — hook + polling עדין בזמן in-flight.
- LearningStatusBadges — 2 שורות (למידת-קול / חילוץ-הלכות) עם badge + תת-שורה
(מס' לקחים · רישום-קורפוס / מס' הלכות + פירוק אושרו/ממתינות/נדחו / סיבת-כישלון).
- שילוב ב-drafts-panel תחת "החלטה סופית של היו״ר" + אינוולידציה בכפתורי-ההרצה.
אומת מול ה-DB החי: הצליח+5 הלכות (8174-12-24) · נכנס-אך-pending (1200-12-25) ·
לא-נכנס-לקורפוס (8125-09-24) · round-trip חיתום-כישלון. tsc/eslint נקיים.
Invariants: G1 (נרמול-במקור — גזירה, לא טלאי), G2 (אין מסלול מקביל — שדה על
הפנקס הקיים + exposer יחיד), INV-LRN4 (פנקס-ההתאמה), INV-IA1 (מקור-אמת יחיד).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
עוזר משפטי — Web UI (Next.js rewrite)
The Next.js 16 rewrite of legal-ai.nautilus.marcusgroup.org, currently hosted side-by-side with the legacy vanilla index.html at:
- Staging: https://legal-ai-next.nautilus.marcusgroup.org (auto-deployed from
ui-rewritebranch via Coolify) - Production FastAPI: https://legal-ai.nautilus.marcusgroup.org (same backend, old UI still default)
The rewrite talks to the existing FastAPI via proxy rewrites in next.config.ts — no CORS setup, no duplicated backend.
Stack
- Next.js 16.2.3 (App Router, Turbopack,
output: "standalone") - React 19.2 · TypeScript · Tailwind v4 · shadcn/ui (radix-nova preset)
- TanStack Query v5 + TanStack Table v8
- react-hook-form + zod for mutations
- react-dropzone for uploads; EventSource for SSE progress
- Heebo via
next/font/google; design tokens insrc/app/globals.css
Local development
npm install
npm run dev # http://localhost:3000
npm run build # full type check + production build
npm run lint
npm run api:types # regenerate src/lib/api/types.ts from FastAPI's OpenAPI
API connection
By default the dev server proxies to production FastAPI (https://legal-ai.nautilus.marcusgroup.org). To point at a different backend, set:
export NEXT_PUBLIC_API_ORIGIN=http://localhost:8000
npm run dev
Project layout
src/
├── app/ # Route segments (App Router)
│ ├── layout.tsx # Root: Providers + RTL html + fonts
│ ├── page.tsx # Home: KPIs + status donut + cases table
│ ├── error.tsx # Route-segment error boundary
│ ├── global-error.tsx # Root crash fallback
│ ├── not-found.tsx # 404
│ ├── cases/
│ │ ├── new/ # 3-step create wizard
│ │ └── [caseNumber]/
│ │ ├── page.tsx # Case detail (tabs + workflow timeline)
│ │ └── compose/ # Research analysis + chair-position editor
│ ├── training/ # Style portrait + corpus + compare (3 tabs)
│ ├── skills/ # Paperclip skills inventory
│ └── diagnostics/ # DB health + failed/stuck docs
├── components/
│ ├── app-shell.tsx # Header + nav with aria-current
│ ├── cases/ # Home + detail screens
│ ├── compose/ # Research analysis editor
│ ├── documents/ # UploadSheet
│ ├── training/ # Style report / corpus / compare panels
│ ├── wizard/ # Case create wizard + parties-field
│ └── ui/ # shadcn primitives
├── lib/
│ ├── api/ # Typed hooks per domain (cases, documents, research,
│ │ # system, skills, training)
│ ├── schemas/ # zod schemas (case create / update)
│ ├── practice-area.ts # Multi-tenant axis enum + deriveSubtype()
│ ├── sse.ts # EventSource wrapper
│ ├── providers.tsx # QueryClient + Toaster
│ └── utils.ts # cn()
Smoke test (run after every deploy)
Use any browser at the staging URL. Every step should be doable without console errors and each mutation should produce a visible toast.
1. Home · /
- Header nav shows 5 items; the current page is underlined in gold
- 4 KPI cards render real numbers (סה״כ · בהכנה · בכתיבה · מוכנים)
- Cases table lists existing cases; search filters by case number or title
- "פיזור סטטוסים" donut renders with a legend
- "+ תיק חדש" button in the top-left navigates to
/cases/new
2. Create case · /cases/new
- 3-step wizard: פרטי יסוד → צדדים → השלמות
- Type
1500-25→ appeal_subtype auto-fills to "רישוי ובנייה" - Type
8500-25→ subtype auto-fills to "היטל השבחה" - Manually pick a different subtype → auto-fill stops
- Submitting with invalid case number shows a zod field error (no crash)
- Successful create → toast "תיק חדש נוצר" → router pushes to
/cases/{number}
3. Case detail · /cases/[caseNumber]
- Header shows status badge + gold "ועדת ערר · X" practice-area badge
- Tabs switch cleanly: סקירה / מסמכים / פעולות
- Workflow timeline on the right shows the current phase highlighted in gold
- פעולות tab → "עריכת פרטי תיק" dialog opens; submitting updates the header without full reload (optimistic cache patch)
- "העלאת מסמכים" sheet opens from the tab row; drag-drop fires a POST and a live progress bar appears via SSE
4. Compose · /cases/[caseNumber]/compose
- If analysis-and-research.md exists: threshold claims + issues render as collapsible cards
- Chair-position textarea auto-saves on blur with "✓ נשמר {time}" indicator
- If 404 (no analysis yet): empty state card renders, no error toast
5. Training · /training
- Report tab: headline card, 4 KPIs, subject donut, anatomy bars, top-12 signature phrases
- Corpus tab: table of corpus decisions with a trash icon per row (aria-label present)
- Deleting a decision refreshes both the corpus table and the report KPIs
- Compare tab: two Selects, pick 2 different decisions, side-by-side panels + shared/only-A/only-B pattern lists
6. Skills · /skills
- Card grid of Paperclip skills with sync-status badges (מסונכרן / DB בלבד / לא סונכרן)
- Chars + file counts render; "לא ידוע" doesn't appear for installed skills
7. Diagnostics · /diagnostics
- DB status card shows "מחובר" in green
- Table counts populate for cases / documents / chunks / corpus / patterns
- Failed + stuck document lists render (empty states OK)
- Page self-refreshes every 10s — check the network tab for recurring calls
8. Error boundary
- Visit
/cases/NOT-REAL-999-99→ case detail shows an error card with the FastAPI message and "חזרה לרשימת התיקים" button (no white screen) - Visit
/anything-broken-xyz→ custom 404 page with "חזרה לבית" button
9. Keyboard + RTL
- Tab through the home page — focus rings are gold, visible
- Wizard progresses via Enter on the "הבא" button
- Screen reader announces nav items with "עמוד נוכחי" on the active one
Deploy
git push # → Coolify auto-build on branch ui-rewrite (~90 s)
Known issue: the Gitea → Coolify webhook is not firing at the time of writing. Trigger a manual deploy via the Coolify MCP (
mcp__coolify__deploywith app UUIDl146g36mtlp0k03vrwkyrgkk) or the Coolify UI until the webhook is fixed.
Phase tracking
See ~/.claude/plans/joyful-marinating-sutton.md for the 7-phase rewrite plan and ~/legal-ai-ui-rewrite/.taskmaster/tasks/tasks.json for the task board.
| Phase | Scope | Status |
|---|---|---|
| 1 | Scaffold + Coolify staging | ✅ |
| 2 | API client + typed hooks + probe | ✅ |
| 3 | Read views (home, case detail, compose) | ✅ |
| 4 | Mutations (wizard, edit, upload+SSE) | ✅ |
| 4.5 | Practice-area integration | ✅ |
| 5 | Secondary screens (training, skills, diagnostics) | ✅ |
| 6 | Polish, a11y, error boundaries, smoke test | ✅ |
| 7 | DNS cutover to production | pending |
Backend contract
The new UI consumes the existing FastAPI at legal-ai/web/app.py. Key endpoints currently relied on:
| Endpoint | Hook | Used by |
|---|---|---|
GET /api/cases?detail=true |
useCases |
home table, KPIs |
GET /api/cases/{n}/details |
useCase |
case detail |
POST /api/cases/create |
useCreateCase |
wizard |
PUT /api/cases/{n} |
useUpdateCase |
inline edit |
DELETE /api/cases?case_number=... |
(MCP only so far) | admin cleanup |
POST /api/cases/{n}/documents/upload-tagged |
useUploadDocument |
upload sheet |
GET /api/progress/{task_id} (SSE) |
useProgress |
upload progress |
GET /api/cases/{n}/research/analysis |
useResearchAnalysis |
compose |
PATCH .../chair-position |
useSaveChairPosition |
chair editor |
GET /api/training/style-report |
useStyleReport |
training/report tab |
GET /api/training/corpus |
useCorpus |
training/corpus tab |
GET /api/training/compare |
useCompare |
training/compare tab |
DELETE /api/training/corpus/{id} |
useDeleteCorpusEntry |
corpus tab |
GET /api/system/diagnostics |
useDiagnostics |
diagnostics page |
GET /api/admin/skills |
useSkills |
skills page |
Any new endpoint should get a typed hook in src/lib/api/ — do not reach into fetch from component code.