Spaces:
Sleeping
Sleeping
| # Second Life β Project Reference (LLM Context Document) | |
| DSCI 5260 | Group 7 | Last updated: 2026-04-25 (post Session 6 β inbox, messaging, trial dashboard, document upload, dataset seeding) | |
| ## Architecture Overview | |
| Flask web app (port 5000) with two authenticated portals: | |
| - **Patient Portal** (/patient): Register/login, update medical profile, get AI trial matches, connect with hospitals | |
| - **Hospital Portal** (/hospital): Login, browse opt-in patients, search by condition, manage connections, edit profile | |
| ### Key Files | |
| - `pipeline.py` β Core ML pipeline (data loading, feature engineering, model training, matching) | |
| - `app.py` β Flask server: session auth, patient API, hospital API, pipeline API | |
| - `database.py` β SQLite layer: 5 tables, auth, connections, trial interests, messages | |
| - `templates/landing.html` β Login/register landing page | |
| - `templates/patient.html` β Patient SPA (profile, trials, connections, inbox) | |
| - `templates/hospital.html` β Hospital SPA (patients, search, my trials, inbox, connections, profile) | |
| - `llm.md` β This file | |
| ## Running the System | |
| ```powershell | |
| cd "E:\DSCI 5260\Project\PT" | |
| python app.py | |
| # open http://localhost:5000 | |
| ``` | |
| On first run (no model_cache.pkl): loads all data files (~2-3 min), trains RF model, saves cache. | |
| On subsequent runs: loads cached model immediately. | |
| Delete `model_cache.pkl` to force retrain (required after pipeline feature changes). | |
| ## Demo Credentials | |
| All hand-made patients have `open_to_trials=1` and password `pass123`. | |
| | Username | Password | Name | Conditions | Location | | |
| |----------|----------|------|-----------|----------| | |
| | john_doe | pass123 | John Doe | hypertension, diabetes, MI | Boston, MA | | |
| | jane_smith | pass123 | Jane Smith | asthma, atopic dermatitis, allergic rhinitis | Cambridge, MA | | |
| | bob_jones | pass123 | Robert Jones | CAD, hypertension, chronic pain | Cleveland, OH | | |
| | alice_brown | pass123 | Alice Brown | non-small cell lung cancer, stroke | Baltimore, MD | | |
| | david_chen | pass123 | David Chen | diabetes, osteoporosis, CAD | Chicago, IL | | |
| | Username | Password | Hospital | Location | | |
| |----------|----------|---------|----------| | |
| | mgh | mgh123 | Massachusetts General Hospital | Boston, MA | | |
| | cleveland | clinic123 | Cleveland Clinic | Cleveland, OH | | |
| | jhopkins | johns123 | Johns Hopkins Hospital | Baltimore, MD | | |
| ### Dataset-Backed Patients (Synthea) | |
| 20 real Synthea patients are auto-seeded on first run from `Final Patients Synthea Data/`. Username format: `synthea_<first 8 chars of Patient_ID>`, password `pass123`, all `open_to_trials=1`. Total DB patients: 25 (5 hand-made + 20 Synthea). | |
| List all accounts: `SELECT username, first_name, last_name FROM patient_accounts ORDER BY username;` | |
| --- | |
| ## Data Sources | |
| ### Patient Side (Synthea synthetic data) | |
| - `Final Patients Synthea Data/final_patients_conditions.csv` β ~967K rows, **265,893 patients**, 106 conditions overlapping with trials. Columns: Patient_ID, Condition_Name, Condition_End_Date | |
| - `Final Patients Synthea Data/patients_details.csv` β Demographics. Columns: Patient_ID, First_Name, Last_Name, Birth_Date (DD-MM-YYYY), Gender, Race, Ethnicity, Address | |
| - `Final Patients Synthea Data/patients_medications.csv` β **213,182 patients with med data**. Columns: Patient_ID, Medication_Name, Medication_End_Date | |
| - `Final Patients Synthea Data/patients_observations.csv` β **23,231 patients with lab data**. Columns: Patient_ID, Observation_Name | |
| ### Trial Side (ClinicalTrials.gov / AACT) | |
| - `Final Clinical Trails Data/trail_conditions.csv` β ~1M rows, **571,379 total trials**, **34,074 with matched condition profiles**. Columns: Trial_ID, Condition_Name_Lower | |
| - `Final Clinical Trails Data/trail_eligibilities.csv` β Columns: Trial_ID, Gender (leading space β stripped), Minimum_Age, Maximum_Age, Eligibility_Criteria | |
| - `Final Clinical Trails Data/trail_studies.csv` β **65,292 recruiting trials**. Columns: Trial_ID, Brief_Title, Overall_Status, Phase, Start_Date, Enrollment | |
| - `Final Clinical Trails Data/trail_facilities.csv` β **189,274 trials with US state geo data**. Columns: Trial_ID, Facility_Name, Facility_City, Facility_State (full names), Facility_Country | |
| - `Final Clinical Trails Data/trail_brief_summaries.csv` β Columns: Trial_ID, Brief_Summary | |
| - `Final Clinical Trails Data/trail_interventions.csv` β **196,865 trials with drug intervention data**. Columns: Trial_ID, Intervention_Type, Intervention_Name | |
| - `Final Clinical Trails Data/trail_countries.csv` β Not used for geo scoring (superseded by facility-level state data) | |
| - `Final Clinical Trails Data/trail_keywords.csv` β Columns: Trial_ID, Keyword_Name_Lower | |
| ### MIMIC-IV Demo (code-level validation only, not in UI) | |
| - `mimic-iv-clinical-database-demo-2.2/hosp/patients.csv.gz` | |
| - `mimic-iv-clinical-database-demo-2.2/hosp/diagnoses_icd.csv.gz` | |
| - `mimic-iv-clinical-database-demo-2.2/hosp/d_icd_diagnoses.csv.gz` | |
| --- | |
| ## SQLite Database (secondlife.db) | |
| ### Tables | |
| ```sql | |
| patient_accounts(id, username, password_hash, synthea_id, first_name, last_name, | |
| dob, gender, address, conditions TEXT DEFAULT '[]', | |
| medications TEXT DEFAULT '[]', documents TEXT DEFAULT '[]', | |
| open_to_trials INTEGER DEFAULT 0, created_at) | |
| hospital_accounts(id, username, password_hash, hospital_name, location, | |
| research_conditions TEXT DEFAULT '[]', created_at) | |
| patient_trial_interests(id, patient_id, trial_id, trial_title, match_score, | |
| status DEFAULT 'interested', created_at, | |
| UNIQUE(patient_id, trial_id)) | |
| connections(id, patient_id, hospital_id, trial_id, trial_title, | |
| initiated_by DEFAULT 'patient', status DEFAULT 'pending', | |
| message, created_at, | |
| UNIQUE(patient_id, hospital_id, trial_id)) | |
| -- Post-schema index handles NULL trial_id: | |
| -- CREATE UNIQUE INDEX idx_conn_unique ON connections(patient_id, hospital_id, COALESCE(trial_id, '')) | |
| connection_messages(id, connection_id, sender_role TEXT, -- 'patient' or 'hospital' | |
| sender_id TEXT, body TEXT, | |
| created_at, is_read INTEGER DEFAULT 0) | |
| -- Index: idx_msgs_conn ON connection_messages(connection_id, created_at) | |
| ``` | |
| JSON fields (conditions, medications, documents, research_conditions) are stored as TEXT and parsed via `_row_to_dict()`. | |
| ### Key database.py Functions | |
| | Function | Purpose | | |
| |---------|---------| | |
| | `get_open_patients_for_hospital(hid, condition_filter, include_connected)` | Returns open_to_trials=1 patients; when `include_connected=False` excludes already-connected patients | | |
| | `get_connection_messages(connection_id)` | All messages for a connection, ASC order | | |
| | `create_connection_message(connection_id, sender_role, sender_id, body)` | Insert message, returns dict | | |
| | `mark_messages_read(connection_id, reader_role)` | Mark all messages from the other role as read | | |
| | `unread_count(connection_id, reader_role)` | Count of unread messages from the other role | | |
| | `get_hospital_inbox_threads(hospital_id)` | All threads with last_message, last_message_at, unread_count; sorted by activity | | |
| | `get_patient_inbox_threads(patient_id)` | Same for patient side | | |
| | `_seed_dataset_patients(c, max_patients=20)` | Seeds Synthea patients from CSV on first run | | |
| --- | |
| ## Feature Engineering (17 features in FEATURE_COLS) | |
| | Feature | Description | | |
| |---------|-------------| | |
| | condition_overlap | Raw count of shared conditions | | |
| | jaccard_similarity | overlap / union | | |
| | overlap_ratio_trial | overlap / len(trial_conditions) | | |
| | overlap_ratio_patient | overlap / len(patient_conditions) | | |
| | condition_rarity_score | mean(1/log2(n_trials_per_cond+2)), normalised 0-1 | | |
| | trial_specificity | 1 / trial_condition_count | | |
| | condition_burden | total_patient_conds / 10 | | |
| | active_ratio | active_conds / total_conds | | |
| | resolved_ratio | resolved_conds / total_conds | | |
| | age_distance | normalised distance outside age range (0 if within) | | |
| | age_centered | position within age range (-1 to +1) | | |
| | age_compatibility | 1.0 in range, decays over 30-year gap | | |
| | gender_compatibility | 1.0 match/all, 0.1 mismatch | | |
| | **geo_feasibility** | **State-level: 1.0 same state, 0.75 other US state, 0.5 no data** | | |
| | **med_compatibility** | **Keyword overlap: patient meds vs trial drug interventions** | | |
| | **lab_availability** | **Patient observation/lab type coverage (0-1, normalised by 20)** | | |
| | data_completeness | fraction of key fields present | | |
| --- | |
| ## Flask API Routes | |
| ### Auth (no login required) | |
| - `POST /auth/patient/register` β {success, patient} or {error} | |
| - `POST /auth/patient/login` β {success, patient} or {error} | |
| - `POST /auth/hospital/register` β {success, hospital} or {error} | |
| - `POST /auth/hospital/login` β {success, hospital} or {error} | |
| - `POST /auth/logout` β {success} | |
| ### Patient API (requires patient session) | |
| - `GET /api/patient/profile` β patient dict (no password_hash) | |
| - `POST /api/patient/profile` β updated patient dict; allowed fields: first_name, last_name, dob, gender, address, conditions, medications, open_to_trials | |
| - `GET /api/patient/matches` β {results: [...trials], total} | |
| - `GET /api/patient/interests` β {interests: [...]} | |
| - `POST /api/patient/interest` β {success}; body: {trial_id, trial_title, match_score} | |
| - `DELETE /api/patient/interest/<trial_id>` β {success} | |
| - `GET /api/patient/connections` β {connections: [...]} joined with hospital_name | |
| - `POST /api/patient/connect` β {success, connection} or 409 if duplicate; body: {hospital_id, trial_id, trial_title, message} | |
| - `GET /api/patient/hospitals-for-trial?trial_id=NCT...` β {hospitals: [...]} tiered matching (see below) | |
| - `GET /api/patient/connections/<cid>/messages` β {messages: [...]}; marks hospital messages read | |
| - `POST /api/patient/connections/<cid>/messages` β {success, message}; body: {body} | |
| - `GET /api/patient/inbox` β {threads: [...]} each with last_message, last_message_at, unread_count, hospital_name | |
| - `GET /api/patient/documents` β {documents: [...]} | |
| - `POST /api/patient/documents` β {success, document}; multipart/form-data file upload (max 10 MB, .pdf/.docx/.doc/.txt/.png/.jpg/.jpeg) | |
| - `DELETE /api/patient/documents/<doc_id>` β {success}; removes file from disk and DB | |
| - `GET /api/patient/documents/<doc_id>/download` β file download (as_attachment) | |
| ### Hospital API (requires hospital session) | |
| - `GET /api/hospital/profile` β hospital dict (no password_hash) | |
| - `POST /api/hospital/profile` β updated hospital dict; allowed fields: hospital_name, location, research_conditions | |
| - `GET /api/hospital/patients?condition=&include_connected=` β {patients: [...]} open_to_trials=1; `include_connected=true` to include already-connected patients (used by Search tab) | |
| - `POST /api/hospital/connect` β {success, connection} or 409 if duplicate; body: {patient_id, trial_id, trial_title, message} | |
| - `GET /api/hospital/connections` β {connections: [...]} joined with patient fields | |
| - `PUT /api/hospital/connections/<cid>/status` β {success}; body: {status: pending|accepted|rejected|completed} | |
| - `GET /api/hospital/connections/<cid>/messages` β {messages: [...]}; marks patient messages read | |
| - `POST /api/hospital/connections/<cid>/messages` β {success, message}; body: {body} | |
| - `GET /api/hospital/inbox` β {threads: [...]} each with last_message, last_message_at, unread_count, first_name, last_name | |
| - `GET /api/hospital/trials` β {trials: [...]} active trials matched to hospital profile (see below) | |
| ### Shared | |
| - `GET /api/status` β {ready, stats} or {ready: false, message} | |
| - `GET /api/conditions/autocomplete?q=...` β {results: [...]} | |
| --- | |
| ## Hospital Trial Dashboard (`/api/hospital/trials`) | |
| `pipeline.trials_for_hospital(hospital_name, location, research_conditions, top_k=20)` β reverse of patient matching: given a hospital's profile, find active clinical trials it is most relevant to. | |
| | Tier | Match condition | `match_reason` field | | |
| |------|----------------|----------------------| | |
| | 1 | Jaccard(hospital name tokens, trial facility name tokens) β₯ 0.25 | "name matched to trial site" | | |
| | 2 | Hospital state matches a US trial facility state | "in same state as trial site" | | |
| | 3 | Hospital `research_conditions` overlaps trial conditions | "researches related conditions" | | |
| Active-only filter (`is_active` check) applied at every tier. Returns list of dicts: | |
| `trial_id, title, phase, status, summary, location, facility_name, n_sites, match_tier, match_reason` | |
| --- | |
| ## Tiered Hospital Matching (`/api/patient/hospitals-for-trial`) | |
| For each trial, hospitals in the DB are scored and returned in tier order (Tier 1 first): | |
| | Tier | Match condition | `match_reason` field | UI label | | |
| |------|----------------|----------------------|----------| | |
| | 1 | Jaccard(hospital name tokens, any trial facility name tokens) β₯ 0.25 | "verified site on this trial" | Green β Verified Trial Sites | | |
| | 2 | Hospital state (from "City, ST" location) matches a trial US facility state | "in same state as a trial site" | Grey β Related Hospitals | | |
| | 3 | Hospital `research_conditions` overlaps trial conditions | "researches related conditions" | Grey β Related Hospitals | | |
| | 4 | Fallback β trial has no facility/state data at all | "" | Grey β Related Hospitals | | |
| The patient connect modal groups Tier 1 hospitals under a green "VERIFIED TRIAL SITES" header and Tiers 2-4 under a grey "RELATED HOSPITALS β not confirmed trial sites" header. | |
| **Pipeline lookups used:** | |
| - `pipeline.trial_facility_tokens[trial_id]` β list of frozensets of significant words from facility names | |
| - `pipeline.trial_us_states[trial_id]` β set of US state full names (e.g. {"Massachusetts"}) | |
| - `pipeline.trial_profiles[trial_id]["conditions"]` β set of condition strings | |
| **Stopword set for facility name tokenisation** (same in pipeline.py and app.py): | |
| hospital, medical, center, centre, clinic, university, health, care, healthcare, system, institute, foundation, research, general, regional, national, community, services, department, division, college, school, the, of, and, at, for, in, a, an, is, by | |
| --- | |
| ## Trial Match Result Fields | |
| Each item in `/api/patient/matches` results: | |
| - trial_id, title, phase, status, min_age, max_age, sex, enrollment, start_date | |
| - eligibility_probability (0-100, calibrated RF probability Γ 100) | |
| - match_score (0-100, rule-based: overlap_ratio weighted) | |
| - combined_score (0-100, 0.6 Γ eligibility + 0.4 Γ match_score) | |
| - age_compatibility, gender_compatibility, geo_feasibility, med_compatibility (all 0-100) | |
| - condition_rarity_score (0-1) | |
| - overlap_conditions (list of conditions shared with patient) | |
| - trial_conditions (all trial conditions) | |
| - criteria (eligibility criteria text, truncated 500 chars) | |
| - summary (brief summary, truncated 400 chars) | |
| - **facility_name** (lead US facility name, or "" if not available) | |
| - location (lead US facility city/state/country string) | |
| - n_sites (total facility count for this trial) | |
| - interest_status (null | 'interested' | 'withdrawn', from patient_trial_interests) | |
| --- | |
| ## Data Privacy Model | |
| 1. Hospitals see only patients with open_to_trials=1 (name, age, gender, conditions) | |
| 2. Full details accessible only after patient-initiated connection | |
| 3. Hospital cannot contact a patient unless patient is open to trials | |
| 4. Connection record: patient_id, hospital_id, trial_id, initiated_by, status, message | |
| 5. Hospital can also initiate connections with opt-in patients from the hospital portal | |
| --- | |
| ## XSS Prevention | |
| All user-controlled strings use DOM API (never innerHTML for user data): | |
| ```javascript | |
| function escH(s) { // text content in innerHTML contexts | |
| const d = document.createElement('div'); | |
| d.appendChild(document.createTextNode(String(s||''))); | |
| return d.innerHTML; | |
| } | |
| function escA(s) { // HTML attribute values | |
| return String(s||'').replace(/&/g,'&').replace(/"/g,'"') | |
| .replace(/</g,'<').replace(/>/g,'>'); | |
| } | |
| ``` | |
| Event listeners use addEventListener only. Tags and cards built via createElement + textContent. | |
| --- | |
| ## ML Model | |
| - Random Forest (n_estimators=200, max_depth=12, class_weight="balanced") | |
| - CalibratedClassifierCV (isotonic, cv=3) for probability calibration | |
| - GroupShuffleSplit (patient-level, 80/20, no leakage) for train/test split | |
| - GroupKFold (5-fold, patient-level) for cross-validation | |
| - Training sample: 3000 patients Γ 30 trials each + random negatives | |
| - Cache: `model_cache.pkl` (delete to force retrain) | |
| ### Actual Metrics (from verified live run, 2026-04-25) | |
| | Metric | Value | | |
| |--------|-------| | |
| | Accuracy | 85.15% | | |
| | AUC-ROC | 0.5976 | | |
| | CV AUC (5-fold) | 0.5992 Β± 0.0052 | | |
| | F1 | 0.9168 | | |
| | Precision | 0.8518 | | |
| | Recall | 0.9925 | | |
| | Brier score | 0.1263 | | |
| | Avg precision | 0.8540 | | |
| | Train size | 90,994 pairs | | |
| | Test size | 22,681 pairs | | |
| | Positive label rate | 82.1% | | |
| > **Note on AUC:** The 82.1% positive rate in pseudo-labels (weighted 6-feature labelling threshold at 0.5) makes the classification task easy to solve trivially β high accuracy/recall but lower AUC. To improve AUC, the pseudo-label threshold should be raised (e.g. 0.6) or positive/negative sampling balanced more aggressively. | |
| ### Feature Importance (Random Forest, ranked) | |
| | Rank | Feature | Importance | | |
| |------|---------|-----------| | |
| | 1 | age_distance | 0.2040 | | |
| | 2 | age_compatibility | 0.1691 | | |
| | 3 | gender_compatibility | 0.1288 | | |
| | 4 | age_centered | 0.0791 | | |
| | 5 | jaccard_similarity | 0.0751 | | |
| | 6 | condition_rarity_score | 0.0621 | | |
| | 7 | overlap_ratio_trial | 0.0519 | | |
| | 8 | overlap_ratio_patient | 0.0512 | | |
| | 9 | condition_overlap | 0.0396 | | |
| | 10 | condition_burden | 0.0274 | | |
| | 11 | resolved_ratio | 0.0263 | | |
| | 12 | active_ratio | 0.0258 | | |
| | 13 | lab_availability | 0.0208 | | |
| | 14 | geo_feasibility | 0.0158 | | |
| | 15 | med_compatibility | 0.0144 | | |
| | 16 | trial_specificity | 0.0087 | | |
| | 17 | data_completeness | 0.0000 | | |
| --- | |
| ## MIMIC-IV Validation (code-level only, not in UI) | |
| - 100 demo patients, ~90% match rate after 3-tier ICD β condition mapping | |
| - Call: `pipeline.validate_mimic()` β list of {subject_id, mapped_conditions, n_matches, top_match} | |
| - 3-tier mapping: exact β substring containment β word-overlap β₯ 75% | |
| - Not exposed via any Flask route | |
| --- | |
| ## All Bug Fixes by Session | |
| ### Session 3 Fixes (2026-04-25) β Two-portal foundation | |
| #### Fix 1 β patient_id not passed to match_patient() (CRITICAL) | |
| **Before:** `pipeline.match_patient(conditions, age, gender, top_k=20)` | |
| **After:** `pipeline.match_patient(conditions, age, gender, top_k=20, patient_id=session["patient_id"], address=address)` | |
| Without this, patient-specific medication keywords and lab scores defaulted to empty / 0.3 for all users β med_compatibility and lab_availability were effectively constants. | |
| #### Fix 2 β Pseudo-label used only 3 features (HIGH) | |
| **Before:** AND gate on age/gender/condition β geo/med/lab had near-zero training influence. | |
| **After:** 6-feature weighted score with 15% random noise: | |
| ```python | |
| score = ( | |
| 0.30 * float(row["age_compatibility"] > 0.6) + | |
| 0.15 * float(row["gender_compatibility"] > 0.5) + | |
| 0.25 * float(row["jaccard_similarity"] > 0.05) + | |
| 0.10 * float(row["geo_feasibility"]) + | |
| 0.10 * float(row["med_compatibility"]) + | |
| 0.10 * float(row["lab_availability"]) | |
| ) | |
| base = int(score >= 0.5) | |
| # 15% hash-deterministic noise for realism | |
| ``` | |
| #### Fix 3 β geo_feasibility was country-level heuristic (MEDIUM) | |
| **Before:** Float from `trail_countries.csv` (1.0 US, 0.7 multi-national, 0.35 non-US). No patient location. | |
| **After:** State-level matching using `trail_facilities.csv` + patient address regex: | |
| ```python | |
| def _geo_score(patient_state_full, trial_states): | |
| if not trial_states: return 0.5 # no US facility data β neutral | |
| if patient_state_full in trial_states: return 1.0 | |
| return 0.75 # other US state | |
| ``` | |
| #### Fix 4 β NameError `trial_geo` in _compute_features return dict | |
| `"geo_feasibility": float(trial_geo)` β `"geo_feasibility": geo_feasibility` | |
| #### Fix 5 β XSS in condition tag onclick handlers (MEDIUM) | |
| `addConditionTag('${c}')` broke for conditions with apostrophes (e.g. "alzheimer's disease"). | |
| Fixed with DOM-based `makeTag()` using textContent + addEventListener. No inline onclick anywhere. | |
| #### Fix 6 β Duplicate connection prevention (was: no guard) | |
| - `connections` table: added `UNIQUE(patient_id, hospital_id, trial_id)` schema constraint | |
| - `init_db()`: runs `CREATE UNIQUE INDEX IF NOT EXISTS idx_conn_unique ON connections(patient_id, hospital_id, COALESCE(trial_id, ''))` to handle NULL trial_id and backfill existing DBs | |
| - `create_connection()`: pre-checks `connection_exists()` before insert; returns `None` on duplicate | |
| - `/api/patient/connect` and `/api/hospital/connect`: return 409 when `create_connection()` returns None | |
| #### Fix 7 β Hospital patient feed showed already-contacted patients (was: no exclusion) | |
| `get_open_patients_for_hospital()` now uses: | |
| ```sql | |
| WHERE open_to_trials=1 | |
| AND id NOT IN (SELECT DISTINCT patient_id FROM connections WHERE hospital_id=?) | |
| ``` | |
| #### Fix 8 β Demo seed: john_doe starts with open_to_trials=1 | |
| Hospital portal was empty on a fresh database. `_seed_demo_data()` now seeds john_doe with `open_to_trials=1`. | |
| #### Fix 9 β Hospital registration silently ignored research_conditions | |
| `templates/landing.html` hospital register form now collects comma-separated research conditions and sends them as a parsed lowercase array to the backend. | |
| #### Fix 10 β Trial cards only showed site count, not facility name or location | |
| `pipeline.py match_patient()` now extracts `facility_name` from `Facility_Name` column; prefers US facilities. Patient portal detail grid shows "Lead Site" and "Location" when available. | |
| --- | |
| ### Session 4 Fixes (2026-04-25) β Hospital matching overhaul + profile editing | |
| #### Fix 11 β Hospital suggestion logic replaced (was: research_conditions overlap only) | |
| Complete replacement of `/api/patient/hospitals-for-trial`: | |
| **Before:** looped all hospitals, included any whose `research_conditions` overlapped trial conditions. No tier concept, no facility data used. | |
| **After:** 4-tier system using two new pipeline lookups: | |
| - `pipeline.trial_facility_tokens[trial_id]` β built from `Facility_Name` column in trail_facilities.csv, US rows only. Each facility name tokenised by stripping stopwords + words < 3 chars. | |
| - `pipeline.trial_us_states[trial_id]` β set of full US state names for the trial | |
| Helper functions in `app.py`: | |
| ```python | |
| def _hospital_name_tokens(name: str) -> frozenset: | |
| # strips stopwords, keeps words β₯ 3 chars | |
| ... | |
| def _facility_match_score(h_tokens: frozenset, facility_token_list: list) -> float: | |
| # best Jaccard score against any facility in the trial | |
| ... | |
| ``` | |
| Each hospital gets one tier assigned and a `match_reason` + `match_tier` in the response. | |
| Result list sorted by `match_tier` ascending (best first). | |
| #### Fix 12 β Hospital portal had no profile editing | |
| `POST /api/hospital/profile` added (was GET-only). `database.py update_hospital_profile()` added. `templates/hospital.html` now has a **My Profile** tab with editable hospital name, location, and research condition tags. On save, the navbar hospital name updates live without a page reload. | |
| #### Fix 13 β Model disclaimer missing from patient trial results | |
| `templates/patient.html` trial results section now shows an alert above results: | |
| > "Match percentages are predictions from a model trained on synthetic patient data and rule-based labels β not validated clinical eligibility determinations. Always consult a healthcare provider before enrolling in any trial." | |
| --- | |
| ### Session 5 Fixes (2026-04-25) β Modal UX + bug fixes | |
| #### Fix 14 β Patient connect modal showed all hospitals in one flat list | |
| **Before:** All hospitals (all tiers) in a single flat list, sorted by tier, with coloured badges as the only visual distinction. | |
| **After:** Modal renders two visually separated sections: | |
| - **"VERIFIED TRIAL SITES"** (green `sec-head`) β Tier 1 hospitals only | |
| - **"RELATED HOSPITALS β not confirmed trial sites"** (grey `sec-head` with inline subtitle) β Tiers 2, 3, 4 | |
| Both sections only render if they have entries. Click delegation on the outer `#hospitalList` wrapper still works for both sections. | |
| #### Fix 15 β Close button invisible on hospital profile condition tags | |
| `hospital.html renderProfTags()`: `btn-close-white` (white X) on `bg-info text-dark` badge (light blue background) β `btn-close` (dark X). The X was invisible before. | |
| #### Fix 16 β Login/register forms required mouse click, no Enter key support | |
| `templates/landing.html`: Added `_onEnter(inputId, fn)` helper and wired Enter key on all login and register inputs (both patient and hospital portals). Works on username field too (not just password). | |
| #### Fix 17 β System ready banner never auto-cleared on slow boot | |
| `templates/patient.html`: `checkStatus()` was called once at DOMContentLoaded and never again. If the pipeline was still training when the user opened the page, the yellow banner persisted even after the pipeline finished. | |
| **After:** `startStatusPoll()` starts a `setInterval` (5s) when the initial check finds `ready: false`. The interval clears itself once `ready: true` is received. | |
| ```javascript | |
| checkStatus().then(() => { if (!sysReady) startStatusPoll(); }); | |
| ``` | |
| --- | |
| ## Live Run Verification (2026-04-25) | |
| End-to-end test results after full retrain with no model_cache.pkl: | |
| | Test | Result | | |
| |------|--------| | |
| | Landing page GET / | 200 OK | | |
| | Patient login john_doe/pass123 | OK β returns patient JSON | | |
| | Hospital login mgh/mgh123 | OK β returns hospital JSON | | |
| | Pipeline ready (api/status) | ready: true | | |
| | /api/patient/matches for john_doe | 20 results, all score fields populated | | |
| | Top match geo score for Greece trial | 50% (no US facility β correct) | | |
| | Hospital browses open_to_trials patients | 25 patients visible (5 demo + 20 Synthea) | | |
| | Hospital condition search ?condition=hypertension | results including Synthea patients | | |
| | Hospital β patient connect (POST) | OK, status=pending | | |
| | Patient sees hospital connection (GET) | 1 connection, hospital_name present | | |
| | /api/conditions/autocomplete?q=hyper | ["hypertension"] | | |
| | Duplicate connect attempt | 409 error | | |
| | Hospital profile save | navbar name updates live | | |
| | Patient connect modal | Two sections render correctly | | |
| | /api/hospital/trials for mgh | Active trials with match tiers | | |
| | /api/hospital/inbox | Threads with unread counts | | |
| | /api/patient/inbox | Threads with hospital names | | |
| | Patient document upload | File saved, metadata in DB | | |
| | Inbox Synthea seeding | 25 patients total confirmed | | |
| --- | |
| ### Session 6 Fixes (2026-04-25) β Messaging, inbox, trial dashboard, document upload, dataset seeding | |
| #### Fix 18 β Hospital trial dashboard (My Trials tab) | |
| **Before:** Hospital portal had no way to see which clinical trials were relevant to it. | |
| **After:** New "My Trials" nav tab in `hospital.html`. Calls `GET /api/hospital/trials` β `pipeline.trials_for_hospital()`. Active-only filter at all 3 tiers. Cards show status badge, phase, match tier, facility name, summary excerpt, and a "View on ClinicalTrials.gov β" link. | |
| Active-only filter: `if not self.trial_profiles[trial_id].get("is_active", False): continue` at each tier loop in `pipeline.py`. | |
| #### Fix 19 β Messaging (chat in connections) | |
| **Before:** Connections table had no messaging. Patients and hospitals could only see connection status. | |
| **After:** | |
| - New `connection_messages` table with `sender_role`, `sender_id`, `body`, `is_read`. | |
| - `GET/POST /api/patient/connections/<cid>/messages` and `GET/POST /api/hospital/connections/<cid>/messages`. | |
| - Both portals have a messages modal (`#msgModal`) opened by a Chat button in the Connections table. | |
| - `mark_messages_read()` called on GET to auto-mark messages as read when the recipient opens the thread. | |
| #### Fix 20 β Dedicated Inbox tab (both portals) | |
| **Before:** Chat only accessible from the My Connections table row β no inbox overview. | |
| **After:** New "Inbox" nav tab in both `hospital.html` and `patient.html`. | |
| - Calls `GET /api/hospital/inbox` or `GET /api/patient/inbox`. | |
| - Backed by `get_hospital_inbox_threads()` / `get_patient_inbox_threads()` β SQL subqueries aggregate last_message, last_message_at, unread_count per thread. | |
| - Threads sorted by most recent activity (Python-side sort on `last_message_at or created_at`). | |
| - Unread count badge on nav tab button updates when inbox loads. | |
| - "Open" button reuses the existing `openMsgModal()` and messages modal. | |
| #### Fix 21 β Document upload (patient portal) | |
| **Before:** Patient profile had no file upload section. | |
| **After:** "My Documents" card added to patient profile tab. 4 routes: | |
| - `POST /api/patient/documents` β werkzeug `secure_filename`, 10 MB limit, allowed extensions: `.pdf/.docx/.doc/.txt/.png/.jpg/.jpeg`. Saves to `uploads/patient_docs/<patient_id>/`. Metadata stored as JSON array in `patient_accounts.documents`. | |
| - `DELETE /api/patient/documents/<doc_id>` β removes file from disk and metadata from DB. | |
| - `GET /api/patient/documents/<doc_id>/download` β serves file as attachment. | |
| - `app.config["MAX_CONTENT_LENGTH"] = 10 * 1024 * 1024` enforced Flask-side. | |
| #### Fix 22 β Dataset-backed patient seeding | |
| **Before:** Only hand-made demo patients in DB (john_doe only had open_to_trials=1 initially). Hospital search returned 0 results on fresh DB. | |
| **After:** `_seed_dataset_patients(c, max_patients=20)` in `database.py` seeds 20 real Synthea patients from CSV files on first run. Skips deceased patients (Death_Date not empty). Reads up to 8 conditions + 6 medications per patient. Username: `synthea_<first8chars_of_Patient_ID>`, password `pass123`, `open_to_trials=1`. | |
| All 5 hand-made demo patients also set to `open_to_trials=1`. Total: 25 patients in DB. | |
| #### Fix 23 β Hospital search include_connected toggle | |
| **Before:** Hospital Search tab also excluded already-connected patients, same as Available Patients tab β making it useless for re-searching. | |
| **After:** Search tab adds `include_connected=true` query param. `GET /api/hospital/patients?include_connected=true` bypasses the exclusion subquery. Available Patients tab retains strict exclusion. Toggle checkbox in Search tab UI. | |
| #### Fix 24 β Template auto-reload | |
| **Before:** `app.run(debug=False, use_reloader=False)` β template edits required server restart to take effect. | |
| **After:** `app.config["TEMPLATES_AUTO_RELOAD"] = True` added after other config lines. Templates now reload on every request without enabling full debug mode or the reloader. | |
| --- | |
| ## Known Issues / Future Improvements | |
| 1. **High pseudo-label positive rate (82.1%)** β lowers AUC-ROC to ~0.60. Fix: raise label threshold from 0.5 to 0.6, or explicitly sample equal positive/negative pairs. | |
| 2. **data_completeness feature importance = 0** β nearly constant across training pairs (all synthetic patients have complete data). Consider removing from FEATURE_COLS. | |
| 3. **Fuzzy condition matching not implemented** β only exact condition name overlaps used (106 conditions). Substring/semantic fuzzy matching would expand coverage significantly. | |
| 4. **lab_availability coverage is low (8.7%)** β observations file is sparse. Consider normalising denominator to the subset that has any lab data. | |
| 5. **Hospital portal does not rank patients by match quality** β listed in insertion order. Could rank by condition overlap with the hospital's research_conditions. | |
| 6. **No email/notification system** β connection requests visible only inside the portal. | |
| 7. **No automated tests** β syntax checking only (`python -m py_compile`). Key flows to cover: patient registration β profile update β match β connect; hospital registration β patient browse β connect β status update; duplicate connection rejection. | |
| 8. **Inbox badge not auto-refreshed** β unread count badge only updates when the user clicks the Inbox tab. No real-time push; would require polling or WebSockets. | |
| 9. **Document access control** β uploaded files are served from disk by doc_id only; no additional hospital-side access to patient documents (by design β privacy model). Hospital sees document count in patient profile only after connection. | |
| 10. **Synthea patients have Synthea-style names** (e.g. "Geovany567 Reichert456") β cosmetically odd but functionally correct. No fix needed for demo. | |