No alerts.
'; return; }
- el.innerHTML = alerts.map(a => `Showing active alerts only. Enable Show acknowledged history to see past items.
';
+ if (!alerts.length) { el.innerHTML = hint + 'No alerts.
'; return; }
+ el.innerHTML = hint + alerts.map(a => `
diff --git a/frontend/style.css b/frontend/style.css
index dec99a8..7b54c21 100644
--- a/frontend/style.css
+++ b/frontend/style.css
@@ -32,7 +32,14 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; b
.btn-primary:hover { opacity: 0.9; }
.btn-small { padding: 0.3rem 0.6rem; font-size: 0.8rem; }
-.stats-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 1.5rem; }
+.stats-row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 1rem; margin-bottom: 1.5rem; }
+.phone-link { color: var(--primary); font-weight: 500; text-decoration: none; font-size: 0.85rem; }
+.phone-link:hover { text-decoration: underline; }
+.btn-contact-link { background: none; border: none; color: var(--primary); font-weight: 500; font-size: 0.85rem; cursor: pointer; padding: 0; text-align: left; }
+.btn-contact-link:hover { text-decoration: underline; }
+.contact-detail-row { display: flex; gap: 1rem; padding: 0.5rem 0; border-bottom: 1px solid var(--gray-100); align-items: baseline; }
+.contact-detail-label { font-size: 0.8rem; color: var(--gray-500); min-width: 70px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.03em; }
+.contact-detail-value { font-size: 0.95rem; font-weight: 500; color: var(--gray-900); }
.stat-card { background: var(--gray-50); border: 1px solid var(--gray-200); border-radius: var(--radius); padding: 1rem 1.25rem; }
.stat-label { font-size: 0.8rem; color: var(--gray-500); margin-bottom: 0.25rem; }
.stat-value { font-size: 1.5rem; font-weight: 700; }
diff --git a/pipelines/care_recommendation.pipe b/pipelines/care_recommendation.pipe
deleted file mode 100644
index 4bf75e1..0000000
--- a/pipelines/care_recommendation.pipe
+++ /dev/null
@@ -1,137 +0,0 @@
-{
- "viewport": {
- "x": -800,
- "y": 200,
- "zoom": 1.2
- },
- "name": "CareGraph Care Plan Generator",
- "description": "Generates personalized care recommendations from Neo4j graph data for senior patients",
- "components": [
- {
- "id": "webhook_1",
- "provider": "webhook",
- "config": {
- "hideForm": true,
- "mode": "Source",
- "parameters": {},
- "type": "webhook"
- },
- "ui": {
- "position": {
- "x": 940,
- "y": 40
- },
- "measured": {
- "width": 160,
- "height": 134
- },
- "data": {
- "provider": "webhook",
- "class": "source",
- "type": "default"
- },
- "formDataValid": true
- }
- },
- {
- "id": "llm_gemini_1",
- "provider": "llm_gemini",
- "name": "gemini",
- "config": {
- "profile": "gemini-2_5-flash",
- "gemini-2_5-flash": {
- "apikey": ""
- }
- },
- "ui": {
- "position": {
- "x": 1520,
- "y": 40
- },
- "measured": {
- "width": 160,
- "height": 64
- },
- "data": {
- "provider": "llm_gemini",
- "class": "llm",
- "type": "default"
- },
- "formDataValid": true
- },
- "input": [
- {
- "lane": "questions",
- "from": "prompt_1"
- }
- ]
- },
- {
- "id": "prompt_1",
- "provider": "prompt",
- "name": "Care Plan Generator",
- "description": "You are a healthcare AI assistant for CareGraph, a senior care platform. Generate personalized care recommendations based on the patient's graph data.\n\nUsing the senior's medications, symptoms, conditions, check-in history, and graph relationships, provide:\n1. Top 3 actionable recommendations for the family\n2. Any concerns to discuss with their doctor\n3. Suggested schedule adjustments\n\nBe concise and practical. Write for a family caregiver, not a medical professional.\n",
- "config": {
- "instructions": [
- "You are a senior care AI assistant. Generate personalized care plans based on graph data including medications, symptoms, conditions, check-in history, drug interactions, and side effect matches. Be concise and write for family caregivers.",
- "Generate a care recommendation for this senior based on their care graph data:\n\n{{text}}"
- ],
- "parameters": {
- "google": {}
- }
- },
- "ui": {
- "position": {
- "x": 1240,
- "y": 80
- },
- "measured": {
- "width": 160,
- "height": 117
- },
- "data": {
- "provider": "prompt",
- "class": "text",
- "type": "default"
- },
- "formDataValid": true
- },
- "input": [
- {
- "lane": "text",
- "from": "webhook_1"
- }
- ]
- },
- {
- "id": "response_answers_1",
- "provider": "response_answers",
- "config": {
- "laneName": "answers"
- },
- "ui": {
- "position": {
- "x": 1800,
- "y": -40
- },
- "measured": {
- "width": 160,
- "height": 64
- },
- "data": {
- "provider": "response_answers",
- "class": "infrastructure",
- "type": "default"
- },
- "formDataValid": true
- },
- "input": [
- {
- "lane": "answers",
- "from": "llm_gemini_1"
- }
- ]
- }
- ],
- "project_id": ""
-}
\ No newline at end of file
diff --git a/pipelines/checkin_analysis.pipe b/pipelines/checkin_analysis.pipe
deleted file mode 100644
index 80ae081..0000000
--- a/pipelines/checkin_analysis.pipe
+++ /dev/null
@@ -1,137 +0,0 @@
-{
- "viewport": {
- "x": -800,
- "y": 200,
- "zoom": 1.2
- },
- "name": "CareGraph Check-in Analysis",
- "description": "Analyzes senior care check-in transcripts to extract symptoms, mood, and urgency",
- "components": [
- {
- "id": "webhook_1",
- "provider": "webhook",
- "config": {
- "hideForm": true,
- "mode": "Source",
- "parameters": {},
- "type": "webhook"
- },
- "ui": {
- "position": {
- "x": 940,
- "y": 40
- },
- "measured": {
- "width": 160,
- "height": 134
- },
- "data": {
- "provider": "webhook",
- "class": "source",
- "type": "default"
- },
- "formDataValid": true
- }
- },
- {
- "id": "llm_gemini_1",
- "provider": "llm_gemini",
- "name": "gemini",
- "config": {
- "profile": "gemini-2_5-flash",
- "gemini-2_5-flash": {
- "apikey": ""
- }
- },
- "ui": {
- "position": {
- "x": 1520,
- "y": 40
- },
- "measured": {
- "width": 160,
- "height": 64
- },
- "data": {
- "provider": "llm_gemini",
- "class": "llm",
- "type": "default"
- },
- "formDataValid": true
- },
- "input": [
- {
- "lane": "questions",
- "from": "prompt_1"
- }
- ]
- },
- {
- "id": "prompt_1",
- "provider": "prompt",
- "name": "Check-in Analyzer",
- "description": "You are a senior care AI assistant for CareGraph. Analyze the check-in transcript and extract health information.\n\nReturn a JSON object with:\n- mood: happy|neutral|sad|concerning\n- wellness_score: 1-10\n- medication_taken: true|false|null\n- symptoms: [list of symptoms mentioned]\n- concerns: [list of health concerns]\n- service_needs: [list of services needed like meals, transport, companionship]\n- summary: one sentence summary\n- recommendation: what the family should do next\n",
- "config": {
- "instructions": [
- "You are a senior care AI assistant. Analyze check-in transcripts to extract symptoms, mood, wellness, and service needs. Return structured JSON.",
- "Analyze this senior care check-in transcript and return JSON results:\n\n{{text}}"
- ],
- "parameters": {
- "google": {}
- }
- },
- "ui": {
- "position": {
- "x": 1240,
- "y": 80
- },
- "measured": {
- "width": 160,
- "height": 117
- },
- "data": {
- "provider": "prompt",
- "class": "text",
- "type": "default"
- },
- "formDataValid": true
- },
- "input": [
- {
- "lane": "text",
- "from": "webhook_1"
- }
- ]
- },
- {
- "id": "response_answers_1",
- "provider": "response_answers",
- "config": {
- "laneName": "answers"
- },
- "ui": {
- "position": {
- "x": 1800,
- "y": -40
- },
- "measured": {
- "width": 160,
- "height": 64
- },
- "data": {
- "provider": "response_answers",
- "class": "infrastructure",
- "type": "default"
- },
- "formDataValid": true
- },
- "input": [
- {
- "lane": "answers",
- "from": "llm_gemini_1"
- }
- ]
- }
- ],
- "project_id": ""
-}
\ No newline at end of file
diff --git a/pipelines/condition_suggestion.pipe b/pipelines/condition_suggestion.pipe
deleted file mode 100644
index 844b9d2..0000000
--- a/pipelines/condition_suggestion.pipe
+++ /dev/null
@@ -1,137 +0,0 @@
-{
- "viewport": {
- "x": -800,
- "y": 200,
- "zoom": 1.2
- },
- "name": "CareGraph Condition Suggester",
- "description": "Suggests possible conditions from symptom clusters detected in Neo4j graph",
- "components": [
- {
- "id": "webhook_1",
- "provider": "webhook",
- "config": {
- "hideForm": true,
- "mode": "Source",
- "parameters": {},
- "type": "webhook"
- },
- "ui": {
- "position": {
- "x": 940,
- "y": 40
- },
- "measured": {
- "width": 160,
- "height": 134
- },
- "data": {
- "provider": "webhook",
- "class": "source",
- "type": "default"
- },
- "formDataValid": true
- }
- },
- {
- "id": "llm_gemini_1",
- "provider": "llm_gemini",
- "name": "gemini",
- "config": {
- "profile": "gemini-2_5-flash",
- "gemini-2_5-flash": {
- "apikey": ""
- }
- },
- "ui": {
- "position": {
- "x": 1520,
- "y": 40
- },
- "measured": {
- "width": 160,
- "height": 64
- },
- "data": {
- "provider": "llm_gemini",
- "class": "llm",
- "type": "default"
- },
- "formDataValid": true
- },
- "input": [
- {
- "lane": "questions",
- "from": "prompt_1"
- }
- ]
- },
- {
- "id": "prompt_1",
- "provider": "prompt",
- "name": "Condition Suggester",
- "description": "You are a healthcare AI assistant for CareGraph. Given symptom clusters from a senior patient's graph data, suggest possible conditions.\n\nFor each condition, provide:\n- condition: name of the condition\n- likelihood: low|medium|high\n- action: recommended next step\n\nReturn as a JSON array: [{\"condition\": \"...\", \"likelihood\": \"...\", \"action\": \"...\"}]\nSuggest up to 3 conditions. Always recommend consulting a doctor. These are suggestions, not diagnoses.\n",
- "config": {
- "instructions": [
- "You are a senior care AI assistant. Suggest possible conditions from symptom clusters. Return JSON array with condition, likelihood, and recommended action. Max 3 suggestions. Always recommend consulting a doctor.",
- "Suggest possible conditions for this senior based on their reported symptoms:\n\n{{text}}"
- ],
- "parameters": {
- "google": {}
- }
- },
- "ui": {
- "position": {
- "x": 1240,
- "y": 80
- },
- "measured": {
- "width": 160,
- "height": 117
- },
- "data": {
- "provider": "prompt",
- "class": "text",
- "type": "default"
- },
- "formDataValid": true
- },
- "input": [
- {
- "lane": "text",
- "from": "webhook_1"
- }
- ]
- },
- {
- "id": "response_answers_1",
- "provider": "response_answers",
- "config": {
- "laneName": "answers"
- },
- "ui": {
- "position": {
- "x": 1800,
- "y": -40
- },
- "measured": {
- "width": 160,
- "height": 64
- },
- "data": {
- "provider": "response_answers",
- "class": "infrastructure",
- "type": "default"
- },
- "formDataValid": true
- },
- "input": [
- {
- "lane": "answers",
- "from": "llm_gemini_1"
- }
- ]
- }
- ],
- "project_id": ""
-}
\ No newline at end of file
diff --git a/pipelines/drug_interaction.pipe b/pipelines/drug_interaction.pipe
deleted file mode 100644
index 3fff4e8..0000000
--- a/pipelines/drug_interaction.pipe
+++ /dev/null
@@ -1,137 +0,0 @@
-{
- "viewport": {
- "x": -800,
- "y": 200,
- "zoom": 1.2
- },
- "name": "CareGraph Drug Interaction Explainer",
- "description": "Explains drug interactions detected in Neo4j graph in plain language for family caregivers",
- "components": [
- {
- "id": "webhook_1",
- "provider": "webhook",
- "config": {
- "hideForm": true,
- "mode": "Source",
- "parameters": {},
- "type": "webhook"
- },
- "ui": {
- "position": {
- "x": 940,
- "y": 40
- },
- "measured": {
- "width": 160,
- "height": 134
- },
- "data": {
- "provider": "webhook",
- "class": "source",
- "type": "default"
- },
- "formDataValid": true
- }
- },
- {
- "id": "llm_gemini_1",
- "provider": "llm_gemini",
- "name": "gemini",
- "config": {
- "profile": "gemini-2_5-flash",
- "gemini-2_5-flash": {
- "apikey": ""
- }
- },
- "ui": {
- "position": {
- "x": 1520,
- "y": 40
- },
- "measured": {
- "width": 160,
- "height": 64
- },
- "data": {
- "provider": "llm_gemini",
- "class": "llm",
- "type": "default"
- },
- "formDataValid": true
- },
- "input": [
- {
- "lane": "questions",
- "from": "prompt_1"
- }
- ]
- },
- {
- "id": "prompt_1",
- "provider": "prompt",
- "name": "Drug Interaction Explainer",
- "description": "You are a healthcare AI assistant for CareGraph, a senior care platform. Explain drug interactions in plain language for family caregivers.\n\nFor the given drug pair, explain:\n1. What happens when these drugs interact\n2. Severity: low, medium, or high\n3. Symptoms to watch for\n4. What the caregiver should do\n\nKeep it under 100 words. Write for a family member, not a doctor.\n",
- "config": {
- "instructions": [
- "You are a senior care AI assistant. Explain drug interactions in plain language for family caregivers. Include severity, symptoms to watch for, and recommended actions. Keep under 100 words.",
- "Explain the drug interaction for this senior patient:\n\n{{text}}"
- ],
- "parameters": {
- "google": {}
- }
- },
- "ui": {
- "position": {
- "x": 1240,
- "y": 80
- },
- "measured": {
- "width": 160,
- "height": 117
- },
- "data": {
- "provider": "prompt",
- "class": "text",
- "type": "default"
- },
- "formDataValid": true
- },
- "input": [
- {
- "lane": "text",
- "from": "webhook_1"
- }
- ]
- },
- {
- "id": "response_answers_1",
- "provider": "response_answers",
- "config": {
- "laneName": "answers"
- },
- "ui": {
- "position": {
- "x": 1800,
- "y": -40
- },
- "measured": {
- "width": 160,
- "height": 64
- },
- "data": {
- "provider": "response_answers",
- "class": "infrastructure",
- "type": "default"
- },
- "formDataValid": true
- },
- "input": [
- {
- "lane": "answers",
- "from": "llm_gemini_1"
- }
- ]
- }
- ],
- "project_id": ""
-}
\ No newline at end of file
diff --git a/tests/test_alert_engine.py b/tests/test_alert_engine.py
index 5895c50..55c071d 100644
--- a/tests/test_alert_engine.py
+++ b/tests/test_alert_engine.py
@@ -47,6 +47,19 @@ def test_chest_pain_triggers_critical():
assert any(a["severity"] == "critical" for a in alerts)
+def test_confusion_phrase_triggers_critical():
+ checkin = {
+ "senior_phone": "+14155551001",
+ "mood": "concerning",
+ "wellness_score": 4,
+ "medication_taken": True,
+ "concerns": ["Mom seems confused and disoriented this morning"],
+ "service_requests": [],
+ }
+ alerts = evaluate_checkin(checkin, "Margaret Johnson")
+ assert any(a["alert_type"] == "emergency" and a["severity"] == "critical" for a in alerts)
+
+
# ---------------------------------------------------------------------------
# Low mood / wellness alerts
# ---------------------------------------------------------------------------
diff --git a/tests/test_integration.py b/tests/test_integration.py
index fa03da1..7b1dcde 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -17,6 +17,7 @@
get_care_network,
get_checkins,
get_alerts,
+ get_senior_checkin_wellness,
)
client = TestClient(app)
@@ -76,6 +77,14 @@ def test_alerts_exist(self):
alerts = get_alerts()
assert len(alerts) >= 1
+ def test_senior_checkin_wellness_shape(self):
+ rows = get_senior_checkin_wellness(days_threshold=7)
+ assert len(rows) >= 3
+ for r in rows:
+ assert "phone" in r and "name" in r
+ assert "at_risk" in r and isinstance(r["at_risk"], bool)
+ assert r.get("days_threshold") == 7
+
# ---------------------------------------------------------------------------
# API endpoint tests
@@ -118,6 +127,15 @@ def test_alerts_endpoint(self):
resp = client.get("/api/alerts")
assert resp.status_code == 200
+ def test_wellness_overview_endpoint(self):
+ resp = client.get("/api/seniors/wellness-overview?days_threshold=7")
+ assert resp.status_code == 200
+ data = resp.json()
+ assert "at_risk_count" in data and "seniors" in data
+ assert isinstance(data["seniors"], list)
+ assert len(data["seniors"]) >= 3
+ assert data["days_threshold"] == 7
+
def test_drug_interactions_endpoint(self):
resp = client.get("/api/graph/drug-interactions/+14155551001")
assert resp.status_code == 200
diff --git a/tests/test_wellness_api.py b/tests/test_wellness_api.py
new file mode 100644
index 0000000..c6a41ad
--- /dev/null
+++ b/tests/test_wellness_api.py
@@ -0,0 +1,82 @@
+"""Tests for GET /api/seniors/wellness-overview (no Neo4j required — uses monkeypatch)."""
+
+import pytest
+from fastapi.testclient import TestClient
+
+from main import app
+
+client = TestClient(app)
+
+_SAMPLE_WELLNESS = [
+ {
+ "phone": "+14155551001",
+ "name": "Test Senior",
+ "checkin_schedule": "09:00",
+ "last_checkin_timestamp": "2026-03-20T12:00:00+00:00",
+ "days_since_checkin": 10,
+ "at_risk": True,
+ "days_threshold": 7,
+ },
+ {
+ "phone": "+14155551002",
+ "name": "Ok Senior",
+ "checkin_schedule": "10:00",
+ "last_checkin_timestamp": "2026-03-29T12:00:00+00:00",
+ "days_since_checkin": 1,
+ "at_risk": False,
+ "days_threshold": 7,
+ },
+]
+
+
+def test_wellness_overview_ok(monkeypatch):
+ def fake_wellness(days_threshold: int = 7):
+ return [{**r, "days_threshold": days_threshold} for r in _SAMPLE_WELLNESS]
+
+ monkeypatch.setattr("app.routers.seniors.get_senior_checkin_wellness", fake_wellness)
+
+ resp = client.get("/api/seniors/wellness-overview")
+ assert resp.status_code == 200
+ data = resp.json()
+ assert data["days_threshold"] == 7
+ assert data["at_risk_count"] == 1
+ assert len(data["seniors"]) == 2
+ assert any(s["phone"] == "+14155551001" and s["at_risk"] for s in data["seniors"])
+
+
+def test_wellness_overview_custom_threshold(monkeypatch):
+ def fake_wellness(days_threshold: int = 7):
+ rows = [
+ {
+ "phone": "+1",
+ "name": "A",
+ "checkin_schedule": "09:00",
+ "last_checkin_timestamp": None,
+ "days_since_checkin": None,
+ "at_risk": True,
+ "days_threshold": days_threshold,
+ }
+ ]
+ return rows
+
+ monkeypatch.setattr("app.routers.seniors.get_senior_checkin_wellness", fake_wellness)
+
+ resp = client.get("/api/seniors/wellness-overview?days_threshold=14")
+ assert resp.status_code == 200
+ assert resp.json()["days_threshold"] == 14
+
+
+def test_wellness_overview_invalid_threshold():
+ resp = client.get("/api/seniors/wellness-overview?days_threshold=0")
+ assert resp.status_code == 400
+
+ resp = client.get("/api/seniors/wellness-overview?days_threshold=100")
+ assert resp.status_code == 400
+
+
+def test_wellness_route_not_shadowed_by_phone_param(monkeypatch):
+ """Static path /wellness-overview must not be captured as {phone}."""
+ monkeypatch.setattr("app.routers.seniors.get_senior_checkin_wellness", lambda days_threshold=7: [])
+ resp = client.get("/api/seniors/wellness-overview")
+ assert resp.status_code == 200
+ assert resp.json() == {"days_threshold": 7, "at_risk_count": 0, "seniors": []}