Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
2834564
Wire telemed waiting room, messaging contacts, and record/timeline ac…
Anurup-R-Krishnan Mar 10, 2026
336a2f4
Allow patient allergy/medication updates via profile endpoint
Anurup-R-Krishnan Mar 10, 2026
3ad770a
Add doctor admit flow for telemedicine rooms
Anurup-R-Krishnan Mar 10, 2026
19eea59
Show referral-selected doctor name during booking
Anurup-R-Krishnan Mar 10, 2026
e7db909
Hide patient id field when record view is scoped
Anurup-R-Krishnan Mar 10, 2026
cfb7eb8
Use rich timeline API and improve event id parsing
Anurup-R-Krishnan Mar 10, 2026
05cd569
Infer timeline categories from event types
Anurup-R-Krishnan Mar 10, 2026
f6bf0d1
Include lab order/result ids in timeline details
Anurup-R-Krishnan Mar 10, 2026
a9274cb
Populate doctor patient notes with allergies and history
Anurup-R-Krishnan Mar 10, 2026
8722e91
Open attachments via backend origin for relative URLs
Anurup-R-Krishnan Mar 10, 2026
364480d
Allow lab technicians to use lab result secure-view
Anurup-R-Krishnan Mar 10, 2026
a72b26b
Fix interaction sandbox auto-report generation
Anurup-R-Krishnan Mar 10, 2026
7ce3fc6
Reset interaction report auto-generate on patient change
Anurup-R-Krishnan Mar 10, 2026
2739411
Open lab attachments for generic lab timeline events
Anurup-R-Krishnan Mar 10, 2026
35a5bb5
Fix doctor initial in patient dashboard appointments
Anurup-R-Krishnan Mar 11, 2026
f99b29d
Handle paginated lab results in technician history
Anurup-R-Krishnan Mar 11, 2026
9a6c7f9
Normalize pharmacy inventory API responses
Anurup-R-Krishnan Mar 11, 2026
65bf4f7
Add error feedback to pharmacy order actions
Anurup-R-Krishnan Mar 11, 2026
c25a766
Allow emergency access without preselected patient
Anurup-R-Krishnan Mar 11, 2026
c5d0ddc
Show lab result completion time using processed_at
Anurup-R-Krishnan Mar 11, 2026
a9a9bfa
Remove hardcoded demo room from doctor dashboard
Anurup-R-Krishnan Mar 11, 2026
1cbc54a
Add toast feedback for lab worklist and history load errors
Anurup-R-Krishnan Mar 11, 2026
86dc611
Add pharmacy inventory toast feedback and safe search
Anurup-R-Krishnan Mar 11, 2026
8f18ad1
Fix lab history effect deps
Anurup-R-Krishnan Mar 11, 2026
4f3bee4
Use next/image for doctor avatar preview
Anurup-R-Krishnan Mar 11, 2026
68ab1d3
Fix appointment booking effect deps
Anurup-R-Krishnan Mar 11, 2026
80399b5
Add routes re-export for app relative imports
Anurup-R-Krishnan Mar 11, 2026
302ae53
Improve anatomy card loading states and selection reset
Anurup-R-Krishnan Mar 11, 2026
928bb1c
Avoid duplicate appointment slots during seeding
Anurup-R-Krishnan Mar 11, 2026
88d2d49
Add guided anatomy flow and spacious layout
Anurup-R-Krishnan Mar 11, 2026
f6e5c6b
Fix app routes re-export to avoid alias loop
Anurup-R-Krishnan Mar 11, 2026
10ff583
Move route helpers to app/lib for client safety
Anurup-R-Krishnan Mar 11, 2026
e0b310a
Use HODDI knowledge for interaction checks
Anurup-R-Krishnan Mar 11, 2026
03daadc
Keep minimal HODDI data local and update seed path
Anurup-R-Krishnan Mar 11, 2026
5afdc25
Trim interaction outputs and improve report labeling
Anurup-R-Krishnan Mar 11, 2026
04ba55c
Make interaction reports concise and doctor-aware
Anurup-R-Krishnan Mar 11, 2026
c143101
Fix login navigation and refine interaction summaries
Anurup-R-Krishnan Mar 11, 2026
39fcdf7
Avoid RSC prefetch errors on login close
Anurup-R-Krishnan Mar 11, 2026
d37f38c
Expose ward map for admins and add interaction summaries
Anurup-R-Krishnan Mar 11, 2026
e6b54c8
Use real billing data on admin dashboard
Anurup-R-Krishnan Mar 11, 2026
3398757
Redirect session timeout logout to homepage
Anurup-R-Krishnan Mar 11, 2026
56554f1
Fix infection trace narrative wording
Anurup-R-Krishnan Mar 11, 2026
29fb7dc
Redirect post-logout to home across RBAC
Anurup-R-Krishnan Mar 11, 2026
ed5737c
Improve anatomy matching fallback and seed spotlight data
Anurup-R-Krishnan Mar 11, 2026
544e926
Add guided anatomy wizard and improve ward/infection views
Anurup-R-Krishnan Mar 11, 2026
e6185f5
Improve ward map dialog and expand contact graph
Anurup-R-Krishnan Mar 11, 2026
c1d90a0
Redesign contact network graph canvas
Anurup-R-Krishnan Mar 11, 2026
cb98d8d
Restyle infection graph for clean clinical look
Anurup-R-Krishnan Mar 11, 2026
8848255
Defer heavy anatomy render and lighten graph load
Anurup-R-Krishnan Mar 11, 2026
d95f01c
Add radial layout fallback for dense infection graphs
Anurup-R-Krishnan Mar 11, 2026
7b6701e
Filter non-contact links from infection graph
Anurup-R-Krishnan Mar 11, 2026
cc68d61
Invalidate stale infection graph cache and add refresh
Anurup-R-Krishnan Mar 11, 2026
2d2dd8e
Enable live reload in docker dev
Anurup-R-Krishnan Mar 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ db.sqlite3-journal
media/
staticfiles/

# Local HODDI datasets (kept out of git)
securemed-backend/data/hoddi/

# Local artifacts
securemed-backend/tmp/

# Python packaging / builds
build/
dist/
Expand Down
14 changes: 11 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ services:
restart: unless-stopped
ports:
- "8000:8000"
volumes:
- ./securemed-backend:/app
- backend_media:/app/media
- backend_static:/app/staticfiles
environment:
DB_ENGINE: django.db.backends.postgresql
DB_NAME: ${DB_NAME:-securemed}
Expand All @@ -56,9 +60,7 @@ services:
ALLOWED_HOSTS: "localhost,127.0.0.1,backend"
SECRET_KEY: ${SECRET_KEY:-ci-secret-key}
DEBUG: ${DEBUG:-True}
command: >
sh -c "python manage.py migrate --noinput &&
gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 2"
DEV_SERVER: "true"
depends_on:
db:
condition: service_healthy
Expand All @@ -74,6 +76,10 @@ services:
restart: unless-stopped
ports:
- "3000:3000"
volumes:
- ./securemed-frontend:/app
- /app/node_modules
- /app/.next
environment:
BACKEND_URL: http://backend:8000
NEXT_PUBLIC_API_URL: /api
Expand All @@ -83,3 +89,5 @@ services:

volumes:
postgres_data:
backend_media:
backend_static:
2 changes: 1 addition & 1 deletion securemed-backend/apps/accounts/patients/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@ class Meta:
]
read_only_fields = [
'patient_id', 'date_of_birth', 'gender', 'blood_group', 'emergency_contacts',
'allergies', 'chronic_conditions', 'current_medications' # Clinical data must be clinician-managed
'chronic_conditions' # Clinician-managed
]
9 changes: 6 additions & 3 deletions securemed-backend/apps/clinical/diagnostics/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@ class LabOrderViewSet(viewsets.ModelViewSet):

def get_queryset(self):
user = self.request.user
base = LabOrder.objects.all().order_by('-created_at', '-id')
if hasattr(user, 'patient_profile'):
# patient FK on LabOrder points to AUTH_USER_MODEL, not Patient
return LabOrder.objects.filter(patient=user)
return base.filter(patient=user)
elif hasattr(user, 'doctor_profile') or user.role == 'doctor':
return LabOrder.objects.filter(doctor=user)
return base.filter(doctor=user)
elif user.is_staff:
return LabOrder.objects.all()
return base
return LabOrder.objects.none()

def perform_create(self, serializer):
Expand Down Expand Up @@ -255,6 +256,8 @@ def secure_view(self, request, pk=None):
elif hasattr(request.user, 'doctor_profile') or request.user.is_staff:
if result.order.doctor_id not in [None, request.user.id] and not request.user.is_staff:
return Response({"error": "Not authorized"}, status=status.HTTP_403_FORBIDDEN)
elif request.user.role == 'lab_technician':
pass
else:
return Response({"error": "Not authorized"}, status=status.HTTP_403_FORBIDDEN)

Expand Down
68 changes: 42 additions & 26 deletions securemed-backend/apps/clinical/records/interaction_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@
from typing import Dict, Iterable, List, Optional, Sequence, Tuple

from django.core.cache import cache
from django.db.models import Q

from .models import (
DrugInteraction,
MedicationInteractionKnowledge,
MedicationInteractionReport,
MedicationInteractionReportJob,
Expand Down Expand Up @@ -71,6 +69,22 @@ def resolve_medications_for_knowledge(medications: Sequence[str]) -> List[str]:
return sorted(set(resolved))


def _build_medication_display_resolver(
normalized_meds: Sequence[str],
original_inputs: Sequence[str],
) -> Dict[str, str]:
original_map = {
normalize_medication_name(med): med
for med in original_inputs
if med
}
if not normalized_meds:
return original_map
refs = MedicationReference.objects.filter(identifier__in=normalized_meds).order_by("id")
display_map = {ref.identifier: ref.display_name for ref in refs}
return {**original_map, **display_map}


def _get_active_medications_for_patient(patient_id: int) -> List[str]:
rows = Prescription.objects.filter(
medical_record__patient_id=patient_id,
Expand Down Expand Up @@ -127,28 +141,6 @@ def _knowledge_findings_for_combos(combo_groups: Iterable[Tuple[str, ...]]) -> L
return findings


def _fallback_pair_findings(pair_groups: Iterable[Tuple[str, str]]) -> List[Dict]:
findings: List[Dict] = []
for a, b in pair_groups:
inter = DrugInteraction.objects.filter(
Q(drug_a__iexact=a, drug_b__iexact=b) | Q(drug_a__iexact=b, drug_b__iexact=a)
).first()
if inter:
findings.append(
{
"finding_type": "interaction",
"medications": [a, b],
"combination_size": 2,
"side_effect": inter.description or f"Interaction between {a} and {b}",
"severity": inter.severity,
"description": inter.description,
"source": "SecureMed Seed",
"source_reference": "",
}
)
return findings


def _compute_medication_safety(medications: Sequence[str]) -> Dict:
normalized = resolve_medications_for_knowledge(medications)
if not normalized:
Expand All @@ -171,7 +163,6 @@ def _compute_medication_safety(medications: Sequence[str]) -> Dict:
findings.extend(_single_drug_findings(normalized))
findings.extend(_knowledge_findings_for_combos(triplets))
findings.extend(_knowledge_findings_for_combos(pairs))
findings.extend(_fallback_pair_findings(pairs))

# Deduplicate by semantic identity.
dedup_key = set()
Expand Down Expand Up @@ -232,6 +223,7 @@ def bump_safety_cache_namespace() -> str:


def evaluate_medication_safety(medications: Sequence[str]) -> Dict:
original_inputs = list(medications)
normalized = resolve_medications_for_knowledge(medications)
signature = canonical_signature(normalized)
if not signature:
Expand All @@ -246,6 +238,20 @@ def evaluate_medication_safety(medications: Sequence[str]) -> Dict:
return cached

result = _compute_medication_safety(normalized)
display_resolver = _build_medication_display_resolver(
result.get("medications", []),
original_inputs,
)
if display_resolver:
result["medications"] = [
display_resolver.get(med, med)
for med in result.get("medications", [])
]
for finding in result.get("findings", []):
finding["medications"] = [
display_resolver.get(med, med)
for med in finding.get("medications", [])
]
try:
cache.set(key, result, SAFETY_CACHE_TTL_SECONDS)
except Exception:
Expand Down Expand Up @@ -321,7 +327,7 @@ def enqueue_report_generation(
trigger_event: str = "manual_refresh",
candidate_medications: Optional[Sequence[str]] = None,
) -> MedicationInteractionReportJob:
from .tasks import generate_interaction_report_job
from django.conf import settings

task_id = uuid4().hex
candidate_meds = [normalize_medication_name(m) for m in (candidate_medications or []) if m]
Expand All @@ -333,6 +339,16 @@ def enqueue_report_generation(
candidate_medications=candidate_meds,
status="queued",
)
if getattr(settings, "CELERY_TASK_ALWAYS_EAGER", False) or getattr(settings, "DEBUG", False):
try:
run_report_job(job.id)
job.refresh_from_db()
except Exception as exc:
job.status = "failed"
job.error_message = str(exc)
job.save(update_fields=["status", "error_message"])
return job
from .tasks import generate_interaction_report_job
try:
generate_interaction_report_job.delay(job.id)
except Exception as exc:
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,90 +1,56 @@
from django.core.management.base import BaseCommand

from apps.clinical.records.interaction_service import canonical_signature, normalize_medication_name
from apps.clinical.records.models import (
MedicationInteractionKnowledge,
MedicationReference,
MedicationSideEffect,
)


MINI_DRUGS = [
{"identifier": "db00945", "display": "Aspirin"},
{"identifier": "db00682", "display": "Warfarin"},
{"identifier": "db01050", "display": "Ibuprofen"},
]
import os
from pathlib import Path

MINI_SIDE_EFFECTS = [
{"med": "db00945", "effect": "Nausea", "severity": "low"},
{"med": "db00682", "effect": "Bleeding", "severity": "high"},
{"med": "db01050", "effect": "Gastric irritation", "severity": "moderate"},
]

MINI_INTERACTIONS = [
{
"meds": ["db00682", "db00945"],
"effect": "Increased bleeding risk",
"severity": "high",
},
{
"meds": ["db00945", "db01050"],
"effect": "Gastrointestinal irritation",
"severity": "moderate",
},
{
"meds": ["db00682", "db00945", "db01050"],
"effect": "Elevated bleeding with NSAIDs",
"severity": "high",
},
]

SOURCE_VERSION = "HODDI_MINI"
from django.core.management.base import BaseCommand
from django.core.management import call_command


class Command(BaseCommand):
help = "Seed a minimal HODDI-like dataset for CI/E2E."
help = "Seed HODDI interactions from a dataset path (prefers local data/hoddi)."

def handle(self, *args, **options):
self.stdout.write("[-] Seeding mini HODDI dataset...")

for item in MINI_DRUGS:
normalized = normalize_medication_name(item["display"])
MedicationReference.objects.get_or_create(
identifier=item["identifier"],
normalized_name=normalized,
defaults={
"display_name": item["display"],
"source": "HODDI",
},
)

for item in MINI_SIDE_EFFECTS:
MedicationSideEffect.objects.get_or_create(
medication_name=item["med"],
side_effect=item["effect"],
source_version=SOURCE_VERSION,
defaults={
"severity": item["severity"],
"description": "",
"source": "HODDI",
},
candidates = [
os.environ.get("HODDI_DATASET_PATH"),
"/app/data/hoddi/HODDI_v2",
"/app/.data/hoddi/HODDI_v2",
"/tmp/HODDI/dataset/HODDI_v2",
]
dataset_path = next((path for path in candidates if path and Path(path).exists()), None)
dataset_version = os.environ.get("HODDI_DATASET_VERSION", "HODDI_v2")
side_effect_map = os.environ.get("HODDI_SIDE_EFFECT_MAP")
drug_map = os.environ.get("HODDI_DRUG_MAP")
include_negative = os.environ.get("HODDI_INCLUDE_NEGATIVE", "").lower() in {"1", "true", "yes"}

if not dataset_path:
self.stdout.write(
self.style.WARNING(
"HODDI dataset not found. Set HODDI_DATASET_PATH or place data under "
"/app/data/hoddi/HODDI_v2."
)
)

for item in MINI_INTERACTIONS:
meds = [normalize_medication_name(m) for m in item["meds"]]
signature = canonical_signature(meds)
MedicationInteractionKnowledge.objects.get_or_create(
combination_signature=signature,
side_effect=item["effect"],
source_version=SOURCE_VERSION,
defaults={
"medications": meds,
"combination_size": len(meds),
"severity": item["severity"],
"description": "",
"source": "HODDI",
"evidence": {},
},
)

self.stdout.write(self.style.SUCCESS("Mini HODDI seed complete."))
return

if not side_effect_map:
candidate = Path(dataset_path) / "dictionary/Side_effects_unique.csv"
if candidate.exists():
side_effect_map = str(candidate)
if not drug_map:
candidate = Path(dataset_path) / "dictionary/Drugbank_ID_SMILE_all_structure links.csv"
if candidate.exists():
drug_map = str(candidate)

kwargs = {
"path": dataset_path,
"dataset_version": dataset_version,
"truncate": True,
}
if side_effect_map:
kwargs["side_effect_map"] = side_effect_map
if drug_map:
kwargs["drug_map"] = drug_map
if include_negative:
kwargs["include_negative"] = True

self.stdout.write(self.style.WARNING("[HODDI] Importing dataset..."))
call_command("import_hoddi", **kwargs)
self.stdout.write(self.style.SUCCESS("[HODDI] Dataset import complete."))
Loading
Loading