Kaifulimaan commited on
Commit
af9984a
Β·
1 Parent(s): 6302922

Fix RLS for segmentation upload, history loading, and add About page

Browse files
backend/app/api/diagnosis.py CHANGED
@@ -865,16 +865,25 @@ async def get_diagnosis_history(
865
  ).order_by(Diagnosis.created_at.desc()).all()
866
  history_items = [_build_history_item_from_record(diagnosis) for diagnosis in diagnoses]
867
  except Exception as db_err:
868
- logger.warning(f"[HISTORY] ⚠️ SQL history query failed, trying Supabase REST fallback: {db_err}")
 
 
869
  try:
 
870
  supabase_rows = supabase.table("diagnoses").select(
871
  "id,disease_name,severity,stage,confidence_disease,confidence_severity,confidence_stage,original_image_url,created_at"
872
  ).eq("user_id", user_id).order("created_at", desc=True).execute()
873
  rows = getattr(supabase_rows, "data", None) or []
874
  history_items = [_build_history_item_from_dict(row) for row in rows]
875
  except Exception as rest_err:
876
- logger.warning(f"[HISTORY] ⚠️ Supabase REST history fallback failed, trying Storage fallback: {rest_err}")
 
 
 
 
877
  history_items = _load_diagnoses_from_storage_history(str(user_id))
 
 
878
 
879
  logger.info(f"[HISTORY] βœ… Retrieved {len(history_items)} diagnoses")
880
 
 
865
  ).order_by(Diagnosis.created_at.desc()).all()
866
  history_items = [_build_history_item_from_record(diagnosis) for diagnosis in diagnoses]
867
  except Exception as db_err:
868
+ logger.warning(f"[HISTORY] ⚠️ SQL history query failed: {db_err}")
869
+
870
+ if not history_items:
871
  try:
872
+ # Fallback path 1: Supabase REST
873
  supabase_rows = supabase.table("diagnoses").select(
874
  "id,disease_name,severity,stage,confidence_disease,confidence_severity,confidence_stage,original_image_url,created_at"
875
  ).eq("user_id", user_id).order("created_at", desc=True).execute()
876
  rows = getattr(supabase_rows, "data", None) or []
877
  history_items = [_build_history_item_from_dict(row) for row in rows]
878
  except Exception as rest_err:
879
+ logger.warning(f"[HISTORY] ⚠️ Supabase REST history fallback failed: {rest_err}")
880
+
881
+ if not history_items:
882
+ try:
883
+ # Fallback path 2: Storage
884
  history_items = _load_diagnoses_from_storage_history(str(user_id))
885
+ except Exception as storage_err:
886
+ logger.warning(f"[HISTORY] ⚠️ Storage history fallback failed: {storage_err}")
887
 
888
  logger.info(f"[HISTORY] βœ… Retrieved {len(history_items)} diagnoses")
889
 
backend/app/api/segmentation.py CHANGED
@@ -140,13 +140,27 @@ async def _download_from_supabase(file_path: str) -> bytes:
140
  )
141
 
142
 
143
- def _upload_to_storage(path: str, file_bytes: bytes, content_type: str) -> None:
144
- supabase = get_supabase_client()
145
- supabase.storage.from_(IMAGES_BUCKET).upload(
146
- path=path,
147
- file=file_bytes,
148
- file_options={"content-type": content_type, "upsert": "true"},
149
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
 
152
  def _resolve_user_id(credentials: Optional[HTTPAuthorizationCredentials]) -> str:
@@ -177,6 +191,7 @@ async def predict_segmentation(
177
  )
178
 
179
  user_id = _resolve_user_id(credentials)
 
180
  timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
181
  request_id = uuid4().hex
182
 
@@ -214,14 +229,14 @@ async def predict_segmentation(
214
  original_path = f"segmentations/{user_id}/originals/{timestamp}_{request_id}.png"
215
  original_png = io.BytesIO()
216
  image.save(original_png, format="PNG")
217
- _upload_to_storage(original_path, original_png.getvalue(), "image/png")
218
 
219
  pipeline = get_segmentation_pipeline()
220
  mask, metadata = pipeline.segment(image)
221
  mask_png_bytes = pipeline.mask_to_png_bytes(mask)
222
 
223
  mask_path = f"segmentations/{user_id}/masks/{timestamp}_{request_id}_mask.png"
224
- _upload_to_storage(mask_path, mask_png_bytes, "image/png")
225
 
226
  original_url = _create_signed_url(original_path)
227
  mask_url = _create_signed_url(mask_path)
 
140
  )
141
 
142
 
143
+ def _upload_to_storage(path: str, file_bytes: bytes, content_type: str, token: Optional[str] = None) -> None:
144
+ if token:
145
+ import httpx
146
+ url = f"{settings.supabase_url}/storage/v1/object/{IMAGES_BUCKET}/{path}"
147
+ headers = {
148
+ "Authorization": f"Bearer {token}",
149
+ "apikey": settings.supabase_key,
150
+ "Content-Type": content_type,
151
+ "x-upsert": "true"
152
+ }
153
+ resp = httpx.post(url, headers=headers, content=file_bytes)
154
+ if resp.status_code >= 400:
155
+ logger.error(f"Storage upload failed: {resp.status_code} {resp.text}")
156
+ resp.raise_for_status()
157
+ else:
158
+ supabase = get_supabase_client()
159
+ supabase.storage.from_(IMAGES_BUCKET).upload(
160
+ path=path,
161
+ file=file_bytes,
162
+ file_options={"content-type": content_type, "upsert": "true"},
163
+ )
164
 
165
 
166
  def _resolve_user_id(credentials: Optional[HTTPAuthorizationCredentials]) -> str:
 
191
  )
192
 
193
  user_id = _resolve_user_id(credentials)
194
+ token = credentials.credentials if credentials else None
195
  timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
196
  request_id = uuid4().hex
197
 
 
229
  original_path = f"segmentations/{user_id}/originals/{timestamp}_{request_id}.png"
230
  original_png = io.BytesIO()
231
  image.save(original_png, format="PNG")
232
+ _upload_to_storage(original_path, original_png.getvalue(), "image/png", token)
233
 
234
  pipeline = get_segmentation_pipeline()
235
  mask, metadata = pipeline.segment(image)
236
  mask_png_bytes = pipeline.mask_to_png_bytes(mask)
237
 
238
  mask_path = f"segmentations/{user_id}/masks/{timestamp}_{request_id}_mask.png"
239
+ _upload_to_storage(mask_path, mask_png_bytes, "image/png", token)
240
 
241
  original_url = _create_signed_url(original_path)
242
  mask_url = _create_signed_url(mask_path)
src/App.tsx CHANGED
@@ -10,6 +10,7 @@ import Upload from "./pages/Upload";
10
  import DiagnosisResults from "./pages/DiagnosisResults";
11
  import SegmentationResults from "./pages/SegmentationResults";
12
  import History from "./pages/History";
 
13
  import NotFound from "./pages/NotFound";
14
 
15
  const queryClient = new QueryClient();
@@ -34,6 +35,7 @@ const App = () => (
34
  <Route path="/diagnosis-results" element={<DiagnosisResults />} />
35
  <Route path="/segmentation-results" element={<SegmentationResults />} />
36
  <Route path="/history" element={<History />} />
 
37
 
38
  {/* Catch-all for 404 */}
39
  <Route path="*" element={<NotFound />} />
 
10
  import DiagnosisResults from "./pages/DiagnosisResults";
11
  import SegmentationResults from "./pages/SegmentationResults";
12
  import History from "./pages/History";
13
+ import About from "./pages/About";
14
  import NotFound from "./pages/NotFound";
15
 
16
  const queryClient = new QueryClient();
 
35
  <Route path="/diagnosis-results" element={<DiagnosisResults />} />
36
  <Route path="/segmentation-results" element={<SegmentationResults />} />
37
  <Route path="/history" element={<History />} />
38
+ <Route path="/about" element={<About />} />
39
 
40
  {/* Catch-all for 404 */}
41
  <Route path="*" element={<NotFound />} />
src/pages/About.tsx ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useNavigate } from "react-router-dom";
2
+ import { ArrowLeft } from "lucide-react";
3
+ import CellularBackground from "@/components/CellularBackground";
4
+ import CytoSightLogo from "@/components/CytoSightLogo";
5
+ import GlassCard from "@/components/GlassCard";
6
+
7
+ const About = () => {
8
+ const navigate = useNavigate();
9
+
10
+ return (
11
+ <CellularBackground>
12
+ <div className="min-h-screen flex flex-col p-4 md:p-8">
13
+ <header className="flex items-center mb-8">
14
+ <button
15
+ onClick={() => navigate(-1)}
16
+ className="flex items-center text-muted-foreground hover:text-foreground transition-colors glass-card px-4 py-2 rounded-full border border-border/30"
17
+ >
18
+ <ArrowLeft className="w-4 h-4 mr-2" />
19
+ Back
20
+ </button>
21
+ </header>
22
+
23
+ <main className="flex-1 flex flex-col items-center justify-center max-w-4xl mx-auto w-full">
24
+ <GlassCard variant="bordered" className="w-full text-center py-16 px-8 md:px-16 animate-fade-in">
25
+ {/* Logo */}
26
+ <div className="flex justify-center mb-10">
27
+ <CytoSightLogo size="xl" />
28
+ </div>
29
+
30
+ {/* Description Text */}
31
+ <div className="max-w-3xl mx-auto">
32
+ <h1 className="text-2xl md:text-3xl font-bold text-foreground leading-relaxed mb-6">
33
+ Welcome to CytoSight: An Explainable Unified Framework for Heterogeneous Cellular Disease Diagnosis and unsupervised segmentation mask generation for blood smears
34
+ </h1>
35
+ </div>
36
+
37
+ <div className="mt-12 h-px bg-gradient-to-r from-transparent via-border to-transparent w-full" />
38
+
39
+ <div className="mt-8 text-muted-foreground text-sm">
40
+ <p>Β© {new Date().getFullYear()} CytoSight. All rights reserved.</p>
41
+ </div>
42
+ </GlassCard>
43
+ </main>
44
+ </div>
45
+ </CellularBackground>
46
+ );
47
+ };
48
+
49
+ export default About;
src/pages/Dashboard.tsx CHANGED
@@ -1,6 +1,6 @@
1
  import { useState, useEffect } from "react";
2
  import { useNavigate } from "react-router-dom";
3
- import { User, ChevronDown, LogOut, Settings, History, Trash2 } from "lucide-react";
4
  import CellularBackground from "@/components/CellularBackground";
5
  import CytoSightLogo from "@/components/CytoSightLogo";
6
  import GlassCard from "@/components/GlassCard";
@@ -76,11 +76,6 @@ const Dashboard = () => {
76
  </p>
77
  <p className="text-xs text-muted-foreground">{displayUser.email}</p>
78
  </div>
79
- <DropdownMenuItem className="cursor-pointer">
80
- <Settings className="w-4 h-4 mr-2" />
81
- Settings
82
- </DropdownMenuItem>
83
- <DropdownMenuSeparator />
84
  <DropdownMenuItem
85
  className="cursor-pointer text-destructive focus:text-destructive"
86
  onClick={handleLogout}
@@ -149,7 +144,10 @@ const Dashboard = () => {
149
  </div>
150
 
151
  {/* About Link */}
152
- <button className="mt-10 text-muted-foreground hover:text-primary transition-colors text-sm">
 
 
 
153
  About CytoSight
154
  </button>
155
  </GlassCard>
 
1
  import { useState, useEffect } from "react";
2
  import { useNavigate } from "react-router-dom";
3
+ import { User, ChevronDown, LogOut, History, Trash2 } from "lucide-react";
4
  import CellularBackground from "@/components/CellularBackground";
5
  import CytoSightLogo from "@/components/CytoSightLogo";
6
  import GlassCard from "@/components/GlassCard";
 
76
  </p>
77
  <p className="text-xs text-muted-foreground">{displayUser.email}</p>
78
  </div>
 
 
 
 
 
79
  <DropdownMenuItem
80
  className="cursor-pointer text-destructive focus:text-destructive"
81
  onClick={handleLogout}
 
144
  </div>
145
 
146
  {/* About Link */}
147
+ <button
148
+ className="mt-10 text-muted-foreground hover:text-primary transition-colors text-sm"
149
+ onClick={() => navigate("/about")}
150
+ >
151
  About CytoSight
152
  </button>
153
  </GlassCard>
test_e2e.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import random
3
+ import string
4
+ from playwright.sync_api import sync_playwright
5
+ from PIL import Image
6
+ import io
7
+ import os
8
+
9
+ # Configuration
10
+ BASE_URL = "http://localhost:8080" # Your local frontend port is 8080
11
+ TEST_PASSWORD = "Password123!"
12
+
13
+ def generate_random_email():
14
+ random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
15
+ return f"test_{random_str}@example.com"
16
+
17
+ def create_test_image(path="test_sample.jpg"):
18
+ """Creates a simple dummy image for testing upload."""
19
+ img = Image.new('RGB', (224, 224), color = (73, 109, 137))
20
+ img.save(path)
21
+ return os.path.abspath(path)
22
+
23
+ def run_test():
24
+ email = generate_random_email()
25
+ test_image = create_test_image()
26
+
27
+ with sync_playwright() as p:
28
+ print("\n" + "="*50)
29
+ print("πŸš€ STARTING CYTOSIGHT E2E TEST SUITE")
30
+ print("="*50 + "\n")
31
+
32
+ # Launch browser (headless=False so you can watch it!)
33
+ # slow_mo adds a small delay between actions so it's visible
34
+ browser = p.chromium.launch(headless=False, slow_mo=800)
35
+ context = browser.new_context()
36
+ page = context.new_page()
37
+
38
+ try:
39
+ # --- 1. SIGNUP ---
40
+ print(f"πŸ“ STEP 1: Signing up as {email}...")
41
+ page.goto(f"{BASE_URL}/signup")
42
+ page.fill('input[name="fullName"]', "Test User")
43
+ page.fill('input[name="email"]', email)
44
+ page.fill('input[name="password"]', TEST_PASSWORD)
45
+ page.fill('input[name="confirmPassword"]', TEST_PASSWORD)
46
+ page.click('button[type="submit"]')
47
+
48
+ # Wait for dashboard redirect
49
+ page.wait_for_url("**/dashboard", timeout=10000)
50
+ print(" βœ… Signup Successful! Redirected to Dashboard.\n")
51
+
52
+ # --- 2. UPLOAD & DIAGNOSIS ---
53
+ print("πŸ“€ STEP 2: Testing Image Upload...")
54
+ page.goto(f"{BASE_URL}/upload")
55
+
56
+ # Upload the file (handles the hidden input automatically)
57
+ page.set_input_files('input[type="file"]', test_image)
58
+ print(f" βœ… Image selected: {test_image}")
59
+
60
+ # Click Diagnosis
61
+ print("πŸ” STEP 3: Running Diagnosis (hitting HF Backend)...")
62
+ page.click('button:has-text("Disease Diagnosis")')
63
+
64
+ # Wait for Results Page (might take a few seconds for AI to process)
65
+ print(" ⏳ Waiting for AI results (up to 60s)...")
66
+ page.wait_for_selector('text=Diagnosis Completed', timeout=60000)
67
+ print(" βœ… Diagnosis Successful!\n")
68
+
69
+ # --- 3. UPLOAD & SEGMENTATION ---
70
+ print("πŸ“€ STEP 4: Testing Binary Segmentation...")
71
+ page.goto(f"{BASE_URL}/upload")
72
+
73
+ # Upload the file again
74
+ page.set_input_files('input[type="file"]', test_image)
75
+ print(f" βœ… Image selected for segmentation")
76
+
77
+ # Click Segmentation
78
+ print("🎭 STEP 5: Running Segmentation (hitting HF Backend)...")
79
+ page.click('button:has-text("Binary Segmentation")')
80
+
81
+ # Wait for Results Page
82
+ print(" ⏳ Waiting for Segmentation results (up to 60s)...")
83
+ page.wait_for_selector('text=Segmentation Results', timeout=60000)
84
+ print(" βœ… Segmentation Successful!")
85
+
86
+ print("\n" + "*"*50)
87
+ print("πŸŽ‰ ALL TESTS PASSED SUCCESSFULLY!")
88
+ print("*"*50 + "\n")
89
+
90
+ except Exception as e:
91
+ print(f"\n❌ TEST FAILED: {str(e)}")
92
+ # Take a screenshot on failure to see what happened
93
+ page.screenshot(path="test_failure.png")
94
+ print("πŸ“Έ Screenshot of failure saved to 'test_failure.png'")
95
+ raise e
96
+
97
+ finally:
98
+ # Clean up
99
+ print("🧹 Cleaning up...")
100
+ time.sleep(3) # Give you a moment to see the success
101
+ browser.close()
102
+ if os.path.exists(test_image):
103
+ os.remove(test_image)
104
+
105
+ if __name__ == "__main__":
106
+ run_test()