Siddharth Ravikumar commited on
Commit
e8a1503
Β·
1 Parent(s): 7ff4a07

feat: integrate static reference cases and update REST API

Browse files
app.py CHANGED
@@ -40,6 +40,8 @@ rule_matcher = RuleMatcher()
40
  fault_deducer = FaultDeducer()
41
  report_generator = ReportGenerator()
42
 
 
 
43
  # ── ZeroGPU: Top-level decorated function ──────────────────────────────
44
  # This MUST be a top-level function wired to a Gradio event handler.
45
 
@@ -228,6 +230,11 @@ def get_case_photos_fn(case_id):
228
  ensure_init()
229
  try:
230
  photos = run_async(db.get_photos_by_case(int(case_id)))
 
 
 
 
 
231
  return [(p["filepath"], p["filename"]) for p in photos if Path(p["filepath"]).exists()]
232
  except Exception:
233
  return []
@@ -397,19 +404,38 @@ def health_fn():
397
 
398
 
399
  def list_cases_json():
400
- """List cases as JSON."""
401
  ensure_init()
402
  cases = run_async(db.list_cases())
403
  for c in cases:
404
  photos = run_async(db.get_photos_by_case(c["id"]))
405
  c["photo_count"] = len(photos)
 
 
 
 
 
 
406
  return json.dumps({"cases": cases})
407
 
408
 
409
  def get_case_json(case_id):
410
- """Get full case details as JSON."""
411
  if not case_id:
412
  return json.dumps({"error": "No case ID"})
 
 
 
 
 
 
 
 
 
 
 
 
 
413
  ensure_init()
414
  case = run_async(db.get_case(int(case_id)))
415
  if not case:
@@ -419,8 +445,12 @@ def get_case_json(case_id):
419
  parties = run_async(db.get_parties_by_case(int(case_id)))
420
  violations = run_async(db.get_violations_by_case(int(case_id)))
421
  fault = run_async(db.get_fault_analysis(int(case_id)))
 
 
 
 
422
  return json.dumps({
423
- "case": case,
424
  "photos": photos,
425
  "analyses": analyses,
426
  "parties": parties,
 
40
  fault_deducer = FaultDeducer()
41
  report_generator = ReportGenerator()
42
 
43
+ from backend.app.core.reference_data import REFERENCE_CASES
44
+
45
  # ── ZeroGPU: Top-level decorated function ──────────────────────────────
46
  # This MUST be a top-level function wired to a Gradio event handler.
47
 
 
230
  ensure_init()
231
  try:
232
  photos = run_async(db.get_photos_by_case(int(case_id)))
233
+ if not photos:
234
+ # Check reference cases
235
+ ref = REFERENCE_CASES.get(int(case_id))
236
+ if ref:
237
+ return [(p["filepath"], p["filename"]) for p in ref["photos"]]
238
  return [(p["filepath"], p["filename"]) for p in photos if Path(p["filepath"]).exists()]
239
  except Exception:
240
  return []
 
404
 
405
 
406
  def list_cases_json():
407
+ """List cases as JSON, including reference cases."""
408
  ensure_init()
409
  cases = run_async(db.list_cases())
410
  for c in cases:
411
  photos = run_async(db.get_photos_by_case(c["id"]))
412
  c["photo_count"] = len(photos)
413
+ c["is_reference"] = False
414
+
415
+ # Add reference cases
416
+ ref_list = [v["case"] for v in REFERENCE_CASES.values()]
417
+ cases = ref_list + cases
418
+
419
  return json.dumps({"cases": cases})
420
 
421
 
422
  def get_case_json(case_id):
423
+ """Get full case details as JSON, handling reference cases."""
424
  if not case_id:
425
  return json.dumps({"error": "No case ID"})
426
+
427
+ # Check reference cases first
428
+ ref = REFERENCE_CASES.get(int(case_id))
429
+ if ref:
430
+ data = ref.copy()
431
+ data["stats"] = {
432
+ "total_photos": len(data["photos"]),
433
+ "analyzed_photos": len(data["analyses"]),
434
+ "violations_found": len(data["violations"]),
435
+ "parties_identified": len(data["parties"]),
436
+ }
437
+ return json.dumps(data)
438
+
439
  ensure_init()
440
  case = run_async(db.get_case(int(case_id)))
441
  if not case:
 
445
  parties = run_async(db.get_parties_by_case(int(case_id)))
446
  violations = run_async(db.get_violations_by_case(int(case_id)))
447
  fault = run_async(db.get_fault_analysis(int(case_id)))
448
+
449
+ case_dict = dict(case)
450
+ case_dict["is_reference"] = False
451
+
452
  return json.dumps({
453
+ "case": case_dict,
454
  "photos": photos,
455
  "analyses": analyses,
456
  "parties": parties,
backend/app/api/routes.py CHANGED
@@ -22,6 +22,7 @@ from backend.app.core.fault_deducer import fault_deducer
22
  from backend.app.core.report_generator import report_generator
23
  from backend.app.rules.rule_loader import rule_loader
24
  from backend.app.utils.logger import get_logger
 
25
 
26
  logger = get_logger("api")
27
 
@@ -82,18 +83,46 @@ async def create_case(
82
 
83
  @router.get("/cases")
84
  async def list_cases():
85
- """List all cases with photo counts."""
86
- cases = await db.list_cases()
87
- # Add photo counts
88
- for case in cases:
89
- photos = await db.get_photos_by_case(case["id"])
90
- case["photo_count"] = len(photos)
 
 
 
 
 
 
 
 
 
 
91
  return {"cases": cases}
92
 
93
 
94
  @router.get("/cases/{case_id}")
95
  async def get_case(case_id: int):
96
- """Get case details with photos and analysis status."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  case = await db.get_case(case_id)
98
  if not case:
99
  raise HTTPException(404, "Case not found")
@@ -104,8 +133,11 @@ async def get_case(case_id: int):
104
  violations = await db.get_violations_by_case(case_id)
105
  fault = await db.get_fault_analysis(case_id)
106
 
 
 
 
107
  return {
108
- "case": case,
109
  "photos": photos,
110
  "analyses": analyses,
111
  "parties": parties,
 
22
  from backend.app.core.report_generator import report_generator
23
  from backend.app.rules.rule_loader import rule_loader
24
  from backend.app.utils.logger import get_logger
25
+ from backend.app.core.reference_data import REFERENCE_CASES
26
 
27
  logger = get_logger("api")
28
 
 
83
 
84
  @router.get("/cases")
85
  async def list_cases():
86
+ """List all cases with photo counts, including reference cases."""
87
+ db_cases = await db.list_cases()
88
+ cases = []
89
+ for case in db_cases:
90
+ case_dict = dict(case)
91
+ photos = await db.get_photos_by_case(case_dict["id"])
92
+ case_dict["photo_count"] = len(photos)
93
+ case_dict["is_reference"] = False
94
+ cases.append(case_dict)
95
+
96
+ # Add reference cases
97
+ for ref_id, ref_data in REFERENCE_CASES.items():
98
+ ref_case = ref_data["case"].copy()
99
+ ref_case["photo_count"] = len(ref_data["photos"])
100
+ cases.append(ref_case)
101
+
102
  return {"cases": cases}
103
 
104
 
105
  @router.get("/cases/{case_id}")
106
  async def get_case(case_id: int):
107
+ """Get case details with photos and analysis status, handling reference cases."""
108
+ # Check reference cases first
109
+ if case_id in REFERENCE_CASES:
110
+ ref_data = REFERENCE_CASES[case_id]
111
+ return {
112
+ "case": ref_data["case"],
113
+ "photos": ref_data["photos"],
114
+ "analyses": ref_data["analyses"],
115
+ "parties": ref_data["parties"],
116
+ "violations": ref_data["violations"],
117
+ "fault_analysis": ref_data["fault_analysis"],
118
+ "stats": {
119
+ "total_photos": len(ref_data["photos"]),
120
+ "analyzed_photos": len(ref_data["analyses"]),
121
+ "violations_found": len(ref_data["violations"]),
122
+ "parties_identified": len(ref_data["parties"]),
123
+ },
124
+ }
125
+
126
  case = await db.get_case(case_id)
127
  if not case:
128
  raise HTTPException(404, "Case not found")
 
133
  violations = await db.get_violations_by_case(case_id)
134
  fault = await db.get_fault_analysis(case_id)
135
 
136
+ case_dict = dict(case)
137
+ case_dict["is_reference"] = False
138
+
139
  return {
140
+ "case": case_dict,
141
  "photos": photos,
142
  "analyses": analyses,
143
  "parties": parties,
backend/app/core/reference_data.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ """
3
+ Static reference cases for the TraceScene demo.
4
+ Sourced from deployment_ui_only/js/alt_app.js
5
+ """
6
+
7
+ REFERENCE_CASES = {
8
+ 9001: {
9
+ "case": {
10
+ "id": 9001,
11
+ "case_number": "REF-2026-042",
12
+ "officer_name": "Sgt. Miller",
13
+ "location": "Hwy 101 N",
14
+ "incident_date": "2026-03-01",
15
+ "status": "complete",
16
+ "created_at": "2026-03-01T08:14:00Z",
17
+ "is_reference": True
18
+ },
19
+ "photos": [
20
+ {"id": "ref_img_1", "filename": "01a252ba0708_car_8.jpg", "filepath": "/static/images/demo/01a252ba0708_car_8.jpg"},
21
+ {"id": "ref_img_2", "filename": "bb7313c8f601_car_10.jpg", "filepath": "/static/images/demo/bb7313c8f601_car_10.jpg"},
22
+ {"id": "ref_img_3", "filename": "413495119937_accident_6.jpg", "filepath": "/static/images/demo/413495119937_accident_6.jpg"}
23
+ ],
24
+ "analyses": [
25
+ {
26
+ "filename": "01a252ba0708_car_8.jpg",
27
+ "raw_analysis": "[AI Observation]\\nVehicles Involved: 1\\nVehicle 1 Make/Model: 2018 Toyota Highlander (Silver SUV)\\nVehicle 1 License Plate: CA 8XYZ123\\nVehicle 1 Geographic Location: California, USA\\nVehicle 1 Owner: Sarah Jenkins\\nNon-vehicle Parties: None\\n\\n[Accident Severity]\\nAccident Category: critical\\nSurvival/Injury Estimation: minor injury\\n\\n[Condition Assessment]\\nTime of Incident: Day\\nWeather: Clear\\nRoad Type: 2-way\\nRoad Surface: Dry\\nVehicle 1 Position: Facing right, near the curb\\nVehicle 1 Tyre Direction: Straight\\nVehicle 1 Tyre Condition: Front-Left (Good, 6mm tread), Front-Right (Good, 6mm tread), Rears (Worn, 3mm tread)\\nArea of Impact: Front\\nVisible Debris/Skid Marks: Glass shards and fluid leak near the front bumper\\nAirbag/Safety: Deployed\\n\\n[Damage Analysis & Insurance Assessment]\\n- Front Bumper: Severe crush damage (Intensity: High). \\n- Radiator & Core Support: Destroyed (Intensity: High).\\n- Right Fender: Crumpled (Intensity: High).\\n\\n[Summary]\\nSevere front-end deformation observed consistent with high-speed frontal impact. Airbags deployed. Vehicle is likely a total loss."
28
+ }
29
+ ],
30
+ "violations": [
31
+ {"rule_id": "SPEED-001", "rule_title": "Excessive Speed", "severity": "CRITICAL", "confidence": 0.92, "party_label": "Silver SUV", "evidence_summary": "Long skid marks and severe front-end crush profile indicate speeds greater than safe for conditions."},
32
+ {"rule_id": "FOLLOW-001", "rule_title": "Following Too Closely (Tailgating)", "severity": "HIGH", "confidence": 0.96, "party_label": "Silver SUV", "evidence_summary": "Impact pattern consistent with rear-ending a leading vehicle without sufficient braking distance."}
33
+ ],
34
+ "fault_analysis": {
35
+ "determined": True,
36
+ "primary_fault_party": "Silver SUV",
37
+ "probable_cause": "Following too closely combined with excessive speed.",
38
+ "fault_distribution_json": {"Silver SUV": 100, "Other": 0},
39
+ "overall_confidence": 0.94,
40
+ "analysis_summary": "Based on severe front-end damage and skid marks, the Silver SUV is entirely at fault for failing to maintain a safe following distance and speed."
41
+ },
42
+ "parties": [
43
+ {"label": "Silver SUV", "vehicle_type": "SUV", "vehicle_color": "Silver", "description": "2018 Toyota Highlander"}
44
+ ]
45
+ },
46
+ 9002: {
47
+ "case": {
48
+ "id": 9002,
49
+ "case_number": "REF-2026-043",
50
+ "officer_name": "Off. Davis",
51
+ "location": "Downtown Intersection",
52
+ "incident_date": "2026-03-02",
53
+ "status": "complete",
54
+ "created_at": "2026-03-02T10:30:00Z",
55
+ "is_reference": True
56
+ },
57
+ "photos": [
58
+ {"id": "ref_img_4", "filename": "a358e234c528_accident_scene_2.jpg", "filepath": "/static/images/demo/a358e234c528_accident_scene_2.jpg"},
59
+ {"id": "ref_img_5", "filename": "e05bd82a2cfa_accident_9.jpg", "filepath": "/static/images/demo/e05bd82a2cfa_accident_9.jpg"}
60
+ ],
61
+ "analyses": [
62
+ {
63
+ "filename": "a358e234c528_accident_scene_2.jpg",
64
+ "raw_analysis": "[AI Observation]\\nVehicles Involved: 2\\nVehicle 1 Make/Model: 2020 Honda Civic (Blue Sedan)\\nVehicle 1 License Plate: TX LMN-4567\\nVehicle 2 Make/Model: 2015 Ford F-150 (White Truck)\\nVehicle 2 License Plate: TX PQR-8910\\n\\n[Accident Severity]\\nAccident Category: medium\\n\\n[Summary]\\nT-bone intersection collision. Blue Sedan sustained heavy side impact; White Truck shows front-end damage. Airbags deployed on Blue Sedan."
65
+ }
66
+ ],
67
+ "violations": [
68
+ {"rule_id": "SIGNAL-001", "rule_title": "Red Light Violation", "severity": "CRITICAL", "confidence": 0.95, "party_label": "White Truck", "evidence_summary": "Point of impact suggests White Truck entered intersection after signal turned red."},
69
+ {"rule_id": "MAINT-002", "rule_title": "Defective Tires", "severity": "HIGH", "confidence": 0.88, "party_label": "White Truck", "evidence_summary": "Rear left tire on White Truck is severely worn (bald)."}
70
+ ],
71
+ "fault_analysis": {
72
+ "determined": True,
73
+ "primary_fault_party": "White Truck",
74
+ "probable_cause": "Failure to stop at red light.",
75
+ "fault_distribution_json": {"White Truck": 100, "Blue Sedan": 0},
76
+ "overall_confidence": 0.91,
77
+ "analysis_summary": "White Truck failed to stop for a red signal, directly causing the T-bone collision."
78
+ },
79
+ "parties": [
80
+ {"label": "Blue Sedan", "vehicle_type": "Sedan", "vehicle_color": "Blue", "description": "2020 Honda Civic"},
81
+ {"label": "White Truck", "vehicle_type": "Truck", "vehicle_color": "White", "description": "2015 Ford F-150"}
82
+ ]
83
+ }
84
+ }
frontend/css/styles.css CHANGED
@@ -374,6 +374,39 @@ button {
374
  display: grid;
375
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
376
  gap: 1rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
377
  }
378
 
379
  .case-card {
 
374
  display: grid;
375
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
376
  gap: 1rem;
377
+ align-items: start;
378
+ }
379
+
380
+ .grid-header {
381
+ grid-column: 1 / -1;
382
+ font-size: 1.1rem;
383
+ color: var(--text-secondary);
384
+ margin-bottom: 0.5rem;
385
+ padding-bottom: 0.5rem;
386
+ border-bottom: 1px solid var(--glass-border);
387
+ display: flex;
388
+ align-items: center;
389
+ gap: 0.5rem;
390
+ }
391
+
392
+ .grid-header::before {
393
+ content: '';
394
+ display: inline-block;
395
+ width: 4px;
396
+ height: 18px;
397
+ background: var(--accent);
398
+ border-radius: 2px;
399
+ }
400
+
401
+ .reference-card {
402
+ background: rgba(30, 58, 138, 0.03);
403
+ border-style: solid;
404
+ border-color: rgba(30, 58, 138, 0.1);
405
+ }
406
+
407
+ .reference-card:hover {
408
+ background: rgba(30, 58, 138, 0.05);
409
+ border-color: var(--accent);
410
  }
411
 
412
  .case-card {
frontend/js/alt_app.js CHANGED
@@ -321,8 +321,10 @@ async function loadCases(vertical = 'le') {
321
  }
322
 
323
  function renderCases(cases, vertical) {
324
- const grid = document.getElementById(vertical === 'le' ? 'le-cases-grid' : 'ins-cases-grid');
325
- const empty = document.getElementById(vertical === 'le' ? 'le-empty-state-dashboard' : 'ins-empty-state-dashboard');
 
 
326
 
327
  if (!cases.length) {
328
  grid.innerHTML = '';
@@ -334,11 +336,32 @@ function renderCases(cases, vertical) {
334
  grid.style.display = 'grid';
335
  empty.style.display = 'none';
336
 
337
- grid.innerHTML = cases.map(c => `
338
- <div class="case-card" onclick="openCase(${c.id})">
339
- <button class="btn-delete-card" onclick="event.stopPropagation(); deleteCase(${c.id}, '${escHtml(c.case_number)}', currentVertical)" title="Delete Case">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  <i class="fa-solid fa-trash-can"></i>
341
- </button>
342
  <div class="card-header">
343
  <span class="case-number">${escHtml(c.case_number)}</span>
344
  <span class="status-badge ${c.status}">
@@ -352,10 +375,10 @@ function renderCases(cases, vertical) {
352
  </div>
353
  <div class="card-footer">
354
  <span class="photo-count"><i class="fa-solid fa-camera"></i> ${c.photo_count || 0} photos</span>
355
- <span style="font-size:0.72rem;color:var(--text-muted)">${formatDate(c.created_at)}</span>
356
  </div>
357
  </div>
358
- `).join('');
359
  }
360
 
361
  async function deleteCase(id, caseNum, vertical = 'le') {
@@ -664,7 +687,21 @@ function renderCaseDetail(data) {
664
  // Enable/disable buttons
665
  const btnAnalysis = document.getElementById('btn-run-analysis');
666
  const btnReport = document.getElementById('btn-view-report');
667
- btnAnalysis.disabled = !data.photos?.length;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
668
  btnReport.disabled = c.status !== 'complete';
669
 
670
  // Photos
@@ -672,11 +709,14 @@ function renderCaseDetail(data) {
672
  document.getElementById('photo-badge').textContent = data.photos?.length || 0;
673
 
674
  if (data.photos?.length) {
675
- photosGrid.innerHTML = data.photos.map(p => `
676
- <div class="detail-photo">
677
- <img src="/${p.filepath}" alt="${escHtml(p.filename)}" onerror="this.onerror=null; this.src='/static/placeholder.jpg';" loading="lazy">
678
- </div>
679
- `).join('');
 
 
 
680
  } else {
681
  photosGrid.innerHTML = '<p class="placeholder-text">No photos uploaded yet.</p>';
682
  }
 
321
  }
322
 
323
  function renderCases(cases, vertical) {
324
+ const gridId = vertical === 'le' ? 'le-cases-grid' : 'ins-cases-grid';
325
+ const emptyId = vertical === 'le' ? 'le-empty-state-dashboard' : 'ins-empty-state-dashboard';
326
+ const grid = document.getElementById(gridId);
327
+ const empty = document.getElementById(emptyId);
328
 
329
  if (!cases.length) {
330
  grid.innerHTML = '';
 
336
  grid.style.display = 'grid';
337
  empty.style.display = 'none';
338
 
339
+ const referenceCases = cases.filter(c => c.is_reference);
340
+ const userCases = cases.filter(c => !c.is_reference);
341
+
342
+ let html = '';
343
+
344
+ if (referenceCases.length) {
345
+ html += `<h3 class="grid-header">Reference cases</h3>`;
346
+ html += referenceCases.map(c => renderCaseCard(c, vertical)).join('');
347
+ }
348
+
349
+ if (userCases.length) {
350
+ const myCasesLabel = vertical === 'ins' ? 'All cases' : 'My cases';
351
+ html += `<h3 class="grid-header" style="margin-top: 1.5rem;">${myCasesLabel}</h3>`;
352
+ html += userCases.map(c => renderCaseCard(c, vertical)).join('');
353
+ }
354
+
355
+ grid.innerHTML = html;
356
+ }
357
+
358
+ function renderCaseCard(c, vertical) {
359
+ return `
360
+ <div class="case-card ${c.is_reference ? 'reference-card' : ''}" onclick="openCase(${c.id})">
361
+ ${!c.is_reference ? `
362
+ <button class="btn-delete-card" onclick="event.stopPropagation(); deleteCase(${c.id}, '${escHtml(c.case_number)}', '${vertical}')" title="Delete Case">
363
  <i class="fa-solid fa-trash-can"></i>
364
+ </button>` : ''}
365
  <div class="card-header">
366
  <span class="case-number">${escHtml(c.case_number)}</span>
367
  <span class="status-badge ${c.status}">
 
375
  </div>
376
  <div class="card-footer">
377
  <span class="photo-count"><i class="fa-solid fa-camera"></i> ${c.photo_count || 0} photos</span>
378
+ <span style="font-size:0.72rem;color:var(--text-muted)">${c.is_reference ? 'Static Reference' : formatDate(c.created_at)}</span>
379
  </div>
380
  </div>
381
+ `;
382
  }
383
 
384
  async function deleteCase(id, caseNum, vertical = 'le') {
 
687
  // Enable/disable buttons
688
  const btnAnalysis = document.getElementById('btn-run-analysis');
689
  const btnReport = document.getElementById('btn-view-report');
690
+ const btnEdit = document.getElementById('btn-edit-case');
691
+ const btnAddPhotos = document.getElementById('btn-add-photos-inline');
692
+
693
+ if (c.is_reference) {
694
+ btnAnalysis.disabled = true;
695
+ btnAnalysis.title = "Analytics is disabled for reference cases";
696
+ if (btnEdit) btnEdit.disabled = true;
697
+ if (btnAddPhotos) btnAddPhotos.disabled = true;
698
+ } else {
699
+ btnAnalysis.disabled = !data.photos?.length;
700
+ btnAnalysis.title = "";
701
+ if (btnEdit) btnEdit.disabled = false;
702
+ if (btnAddPhotos) btnAddPhotos.disabled = false;
703
+ }
704
+
705
  btnReport.disabled = c.status !== 'complete';
706
 
707
  // Photos
 
709
  document.getElementById('photo-badge').textContent = data.photos?.length || 0;
710
 
711
  if (data.photos?.length) {
712
+ photosGrid.innerHTML = data.photos.map(p => {
713
+ const imgSrc = c.is_reference ? p.filepath : `/${p.filepath}`;
714
+ return `
715
+ <div class="detail-photo">
716
+ <img src="${imgSrc}" alt="${escHtml(p.filename)}" onerror="this.onerror=null; this.src='/static/placeholder.jpg';" loading="lazy">
717
+ </div>
718
+ `;
719
+ }).join('');
720
  } else {
721
  photosGrid.innerHTML = '<p class="placeholder-text">No photos uploaded yet.</p>';
722
  }
frontend/js/app.js CHANGED
@@ -180,11 +180,31 @@ function renderCases(cases) {
180
  grid.style.display = 'grid';
181
  empty.style.display = 'none';
182
 
183
- grid.innerHTML = cases.map(c => `
184
- <div class="case-card" onclick="openCase(${c.id})">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  <button class="btn-delete-card" onclick="event.stopPropagation(); deleteCase(${c.id}, '${escHtml(c.case_number)}')" title="Delete Case">
186
  <i class="fa-solid fa-trash-can"></i>
187
- </button>
188
  <div class="card-header">
189
  <span class="case-number">${escHtml(c.case_number)}</span>
190
  <span class="status-badge ${c.status}">
@@ -198,10 +218,10 @@ function renderCases(cases) {
198
  </div>
199
  <div class="card-footer">
200
  <span class="photo-count"><i class="fa-solid fa-camera"></i> ${c.photo_count || 0} photos</span>
201
- <span style="font-size:0.72rem;color:var(--text-muted)">${formatDate(c.created_at)}</span>
202
  </div>
203
  </div>
204
- `).join('');
205
  }
206
 
207
  async function deleteCase(id, caseNum) {
@@ -498,19 +518,32 @@ function renderCaseDetail(data) {
498
  // Enable/disable buttons
499
  const btnAnalysis = document.getElementById('btn-run-analysis');
500
  const btnReport = document.getElementById('btn-view-report');
501
- btnAnalysis.disabled = !data.photos?.length;
502
- btnReport.disabled = c.status !== 'complete';
 
 
 
 
 
 
 
 
 
 
 
 
503
 
504
- // Photos
505
- const photosGrid = document.getElementById('detail-photos-grid');
506
- document.getElementById('photo-badge').textContent = data.photos?.length || 0;
507
 
508
  if (data.photos?.length) {
509
- photosGrid.innerHTML = data.photos.map(p => `
510
- <div class="detail-photo">
511
- <img src="${API_BASE.replace('/api', '')}/api/photos/${p.id}/image" alt="${escHtml(p.filename)}" loading="lazy">
512
- </div>
513
- `).join('');
 
 
 
514
  } else {
515
  photosGrid.innerHTML = '<p class="placeholder-text">No photos uploaded yet.</p>';
516
  }
 
180
  grid.style.display = 'grid';
181
  empty.style.display = 'none';
182
 
183
+ const referenceCases = cases.filter(c => c.is_reference);
184
+ const userCases = cases.filter(c => !c.is_reference);
185
+
186
+ let html = '';
187
+
188
+ if (referenceCases.length) {
189
+ html += `<h3 class="grid-header">Reference cases</h3>`;
190
+ html += referenceCases.map(c => renderCaseCard(c)).join('');
191
+ }
192
+
193
+ if (userCases.length) {
194
+ html += `<h3 class="grid-header" style="margin-top: 1.5rem;">My cases</h3>`;
195
+ html += userCases.map(c => renderCaseCard(c)).join('');
196
+ }
197
+
198
+ grid.innerHTML = html;
199
+ }
200
+
201
+ function renderCaseCard(c) {
202
+ return `
203
+ <div class="case-card ${c.is_reference ? 'reference-card' : ''}" onclick="openCase(${c.id})">
204
+ ${!c.is_reference ? `
205
  <button class="btn-delete-card" onclick="event.stopPropagation(); deleteCase(${c.id}, '${escHtml(c.case_number)}')" title="Delete Case">
206
  <i class="fa-solid fa-trash-can"></i>
207
+ </button>` : ''}
208
  <div class="card-header">
209
  <span class="case-number">${escHtml(c.case_number)}</span>
210
  <span class="status-badge ${c.status}">
 
218
  </div>
219
  <div class="card-footer">
220
  <span class="photo-count"><i class="fa-solid fa-camera"></i> ${c.photo_count || 0} photos</span>
221
+ <span style="font-size:0.72rem;color:var(--text-muted)">${c.is_reference ? 'Static Reference' : formatDate(c.created_at)}</span>
222
  </div>
223
  </div>
224
+ `;
225
  }
226
 
227
  async function deleteCase(id, caseNum) {
 
518
  // Enable/disable buttons
519
  const btnAnalysis = document.getElementById('btn-run-analysis');
520
  const btnReport = document.getElementById('btn-view-report');
521
+ const btnEdit = document.getElementById('btn-edit-case');
522
+ const btnAddPhotos = document.getElementById('btn-add-photos-inline');
523
+
524
+ if (c.is_reference) {
525
+ btnAnalysis.disabled = true;
526
+ btnAnalysis.title = "Analytics is disabled for reference cases";
527
+ if (btnEdit) btnEdit.disabled = true;
528
+ if (btnAddPhotos) btnAddPhotos.disabled = true;
529
+ } else {
530
+ btnAnalysis.disabled = !data.photos?.length;
531
+ btnAnalysis.title = "";
532
+ if (btnEdit) btnEdit.disabled = false;
533
+ if (btnAddPhotos) btnAddPhotos.disabled = false;
534
+ }
535
 
536
+ btnReport.disabled = c.status !== 'complete';
 
 
537
 
538
  if (data.photos?.length) {
539
+ photosGrid.innerHTML = data.photos.map(p => {
540
+ const imgSrc = c.is_reference ? p.filepath : `${API_BASE.replace('/api', '')}/api/photos/${p.id}/image`;
541
+ return `
542
+ <div class="detail-photo">
543
+ <img src="${imgSrc}" alt="${escHtml(p.filename)}" loading="lazy">
544
+ </div>
545
+ `;
546
+ }).join('');
547
  } else {
548
  photosGrid.innerHTML = '<p class="placeholder-text">No photos uploaded yet.</p>';
549
  }