From ea32814ef1d4aff63edee0fb73c48b53c25969ee Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Thu, 20 Nov 2025 15:04:04 -0500 Subject: [PATCH 01/14] Add performance tracking to embedding search --- server/api/services/embedding_services.py | 94 ++++++++++++++++++++--- 1 file changed, 84 insertions(+), 10 deletions(-) diff --git a/server/api/services/embedding_services.py b/server/api/services/embedding_services.py index 6fd34d35..2c51d8cb 100644 --- a/server/api/services/embedding_services.py +++ b/server/api/services/embedding_services.py @@ -1,5 +1,7 @@ # services/embedding_services.py +import time +import logging from pgvector.django import L2Distance from .sentencetTransformer_model import TransformerModel @@ -7,9 +9,12 @@ # Adjust import path as needed from ..models.model_embeddings import Embeddings +# Configure logging +logger = logging.getLogger(__name__) + def get_closest_embeddings( - user, message_data, document_name=None, guid=None, num_results=10 + user, message_data, document_name=None, guid=None, num_results=10, return_metrics=False ): """ Find the closest embeddings to a given message for a specific user. @@ -26,22 +31,46 @@ def get_closest_embeddings( Filter results to a specific document GUID (takes precedence over document_name) num_results : int, default 10 Maximum number of results to return + return_metrics : bool, default False + If True, return a tuple of (results, metrics) instead of just results Returns ------- - list[dict] - List of dictionaries containing embedding results with keys: - - name: document name - - text: embedded text content - - page_number: page number in source document - - chunk_number: chunk number within the document - - distance: L2 distance from query embedding - - file_id: GUID of the source file + list[dict] or tuple[list[dict], dict] + If return_metrics is False (default): + List of dictionaries containing embedding results with keys: + - name: document name + - text: embedded text content + - page_number: page number in source document + - chunk_number: chunk number within the document + - distance: L2 distance from query embedding + - file_id: GUID of the source file + + If return_metrics is True: + Tuple of (results, metrics) where metrics is a dictionary containing: + - encoding_time: Time to encode query (seconds) + - db_query_time: Time for database query (seconds) + - total_time: Total execution time (seconds) + - total_embeddings: Number of embeddings searched + - num_results_returned: Number of results returned + - avg_similarity: Average similarity score (0-1) + - min_distance: Minimum L2 distance + - max_distance: Maximum L2 distance + - avg_distance: Average L2 distance """ - # + # Track total execution time + start_time = time.time() + + # Track transformer encoding time + encoding_start = time.time() transformerModel = TransformerModel.get_instance().model embedding_message = transformerModel.encode(message_data) + encoding_time = time.time() - encoding_start + + # Track database query time + db_query_start = time.time() + # Start building the query based on the message's embedding closest_embeddings_query = ( Embeddings.objects.filter(upload_file__uploaded_by=user) @@ -51,6 +80,9 @@ def get_closest_embeddings( .order_by("distance") ) + # Get total embeddings in search space before filtering + total_embeddings = closest_embeddings_query.count() + # Filter by GUID if provided, otherwise filter by document name if provided if guid: closest_embeddings_query = closest_embeddings_query.filter( @@ -75,4 +107,46 @@ def get_closest_embeddings( for obj in closest_embeddings_query ] + db_query_time = time.time() - db_query_start + total_time = time.time() - start_time + + # Calculate distance/similarity statistics + num_results_returned = len(results) + if num_results_returned > 0: + distances = [r["distance"] for r in results] + min_distance = min(distances) + max_distance = max(distances) + avg_distance = sum(distances) / num_results_returned + # Convert distance to similarity score (1 - distance for L2) + avg_similarity = 1 - avg_distance + else: + min_distance = max_distance = avg_distance = avg_similarity = 0.0 + + # Log performance metrics similar to assistant/views.py pattern + logger.info( + f"Embedding search completed: " + f"Encoding time: {encoding_time:.3f}s, " + f"DB query time: {db_query_time:.3f}s, " + f"Total time: {total_time:.3f}s, " + f"Searched: {total_embeddings} embeddings, " + f"Returned: {num_results_returned} results, " + f"Avg similarity: {avg_similarity:.3f}, " + f"Distance range: [{min_distance:.3f}, {max_distance:.3f}]" + ) + + # Optionally return metrics along with results + if return_metrics: + metrics = { + "encoding_time": encoding_time, + "db_query_time": db_query_time, + "total_time": total_time, + "total_embeddings": total_embeddings, + "num_results_returned": num_results_returned, + "avg_similarity": avg_similarity, + "min_distance": min_distance, + "max_distance": max_distance, + "avg_distance": avg_distance, + } + return results, metrics + return results From 1fc41a76ba12963b707ecc46157645bba56db449 Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Mon, 24 Nov 2025 17:08:21 -0500 Subject: [PATCH 02/14] Simplify embedding search --- server/api/services/embedding_services.py | 45 +++++++---------------- 1 file changed, 13 insertions(+), 32 deletions(-) diff --git a/server/api/services/embedding_services.py b/server/api/services/embedding_services.py index 2c51d8cb..1828b81c 100644 --- a/server/api/services/embedding_services.py +++ b/server/api/services/embedding_services.py @@ -1,18 +1,13 @@ -# services/embedding_services.py - import time import logging + from pgvector.django import L2Distance from .sentencetTransformer_model import TransformerModel - -# Adjust import path as needed from ..models.model_embeddings import Embeddings -# Configure logging logger = logging.getLogger(__name__) - def get_closest_embeddings( user, message_data, document_name=None, guid=None, num_results=10, return_metrics=False ): @@ -51,24 +46,19 @@ def get_closest_embeddings( - encoding_time: Time to encode query (seconds) - db_query_time: Time for database query (seconds) - total_time: Total execution time (seconds) - - total_embeddings: Number of embeddings searched - num_results_returned: Number of results returned - - avg_similarity: Average similarity score (0-1) - min_distance: Minimum L2 distance - max_distance: Maximum L2 distance - avg_distance: Average L2 distance """ - # Track total execution time start_time = time.time() - # Track transformer encoding time encoding_start = time.time() transformerModel = TransformerModel.get_instance().model embedding_message = transformerModel.encode(message_data) encoding_time = time.time() - encoding_start - # Track database query time db_query_start = time.time() # Start building the query based on the message's embedding @@ -80,10 +70,7 @@ def get_closest_embeddings( .order_by("distance") ) - # Get total embeddings in search space before filtering - total_embeddings = closest_embeddings_query.count() - - # Filter by GUID if provided, otherwise filter by document name if provided + # Filtering results to a document GUID takes precedence over filtering results to document name if guid: closest_embeddings_query = closest_embeddings_query.filter( upload_file__guid=guid @@ -95,6 +82,7 @@ def get_closest_embeddings( closest_embeddings_query = closest_embeddings_query[:num_results] # Format the results to be returned + # TODO: Research improving the query evaluation performance results = [ { "name": obj.name, @@ -112,37 +100,30 @@ def get_closest_embeddings( # Calculate distance/similarity statistics num_results_returned = len(results) - if num_results_returned > 0: - distances = [r["distance"] for r in results] - min_distance = min(distances) - max_distance = max(distances) - avg_distance = sum(distances) / num_results_returned - # Convert distance to similarity score (1 - distance for L2) - avg_similarity = 1 - avg_distance - else: - min_distance = max_distance = avg_distance = avg_similarity = 0.0 - - # Log performance metrics similar to assistant/views.py pattern + + #TODO: Handle user having no uploaded docs or doc filtering returning no matches + + distances = [r["distance"] for r in results] + min_distance = min(distances) + max_distance = max(distances) + avg_distance = sum(distances) / num_results_returned + logger.info( f"Embedding search completed: " f"Encoding time: {encoding_time:.3f}s, " f"DB query time: {db_query_time:.3f}s, " f"Total time: {total_time:.3f}s, " - f"Searched: {total_embeddings} embeddings, " f"Returned: {num_results_returned} results, " - f"Avg similarity: {avg_similarity:.3f}, " - f"Distance range: [{min_distance:.3f}, {max_distance:.3f}]" + f"Distance range: [{min_distance:.3f}, {max_distance:.3f}], " + f"Average distance: {avg_distance:.3f}" ) - # Optionally return metrics along with results if return_metrics: metrics = { "encoding_time": encoding_time, "db_query_time": db_query_time, "total_time": total_time, - "total_embeddings": total_embeddings, "num_results_returned": num_results_returned, - "avg_similarity": avg_similarity, "min_distance": min_distance, "max_distance": max_distance, "avg_distance": avg_distance, From 156644be05058b6afe8519bf2ae266158a9d00f2 Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Tue, 25 Nov 2025 19:18:40 -0500 Subject: [PATCH 03/14] Add persistent tracking for semantic search performance and usage --- server/api/models/model_search_usage.py | 42 +++++++++ server/api/services/embedding_services.py | 110 ++++++++++------------ 2 files changed, 92 insertions(+), 60 deletions(-) create mode 100644 server/api/models/model_search_usage.py diff --git a/server/api/models/model_search_usage.py b/server/api/models/model_search_usage.py new file mode 100644 index 00000000..cdc3dee6 --- /dev/null +++ b/server/api/models/model_search_usage.py @@ -0,0 +1,42 @@ +import uuid + +from django.db import models +from django.conf import settings + +class SemanticSearchUsage(models.Model): + """ + Tracks performance metrics and usage data for embedding searches. + """ + guid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) + timestamp = models.DateTimeField(auto_now_add=True) + query_text = models.TextField(blank=True, null=True, help_text="The search query text") + document_name = models.TextField(blank=True, null=True, help_text="Document name filter if used") + document_guid = models.UUIDField(blank=True, null=True, help_text="Document GUID filter if used") + num_results_requested = models.IntegerField(default=10, help_text="Number of results requested") + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='semantic_searches', + null=True, + blank=True, + help_text="User who performed the search (null for unauthenticated users)" + ) + encoding_time = models.FloatField(help_text="Time to encode query in seconds") + db_query_time = models.FloatField(help_text="Time for database query in seconds") + num_results_returned = models.IntegerField(help_text="Number of results returned") + min_distance = models.FloatField(null=True, blank=True, help_text="Minimum L2 distance (null if no results)") + max_distance = models.FloatField(null=True, blank=True, help_text="Maximum L2 distance (null if no results)") + median_distance = models.FloatField(null=True, blank=True, help_text="Median L2 distance (null if no results)") + + + class Meta: + ordering = ['-timestamp'] + indexes = [ + models.Index(fields=['-timestamp']), + models.Index(fields=['user', '-timestamp']), + ] + + def __str__(self): + total_time = self.encoding_time + self.db_query_time + user_display = self.user.email if self.user else "Anonymous" + return f"Search by {user_display} at {self.timestamp} ({total_time:.3f}s)" diff --git a/server/api/services/embedding_services.py b/server/api/services/embedding_services.py index 1828b81c..c937f757 100644 --- a/server/api/services/embedding_services.py +++ b/server/api/services/embedding_services.py @@ -1,15 +1,17 @@ import time import logging +from statistics import median from pgvector.django import L2Distance from .sentencetTransformer_model import TransformerModel from ..models.model_embeddings import Embeddings +from ..models.model_search_usage import SemanticSearchUsage logger = logging.getLogger(__name__) def get_closest_embeddings( - user, message_data, document_name=None, guid=None, num_results=10, return_metrics=False + user, message_data, document_name=None, guid=None, num_results=10 ): """ Find the closest embeddings to a given message for a specific user. @@ -26,34 +28,19 @@ def get_closest_embeddings( Filter results to a specific document GUID (takes precedence over document_name) num_results : int, default 10 Maximum number of results to return - return_metrics : bool, default False - If True, return a tuple of (results, metrics) instead of just results Returns ------- - list[dict] or tuple[list[dict], dict] - If return_metrics is False (default): - List of dictionaries containing embedding results with keys: - - name: document name - - text: embedded text content - - page_number: page number in source document - - chunk_number: chunk number within the document - - distance: L2 distance from query embedding - - file_id: GUID of the source file - - If return_metrics is True: - Tuple of (results, metrics) where metrics is a dictionary containing: - - encoding_time: Time to encode query (seconds) - - db_query_time: Time for database query (seconds) - - total_time: Total execution time (seconds) - - num_results_returned: Number of results returned - - min_distance: Minimum L2 distance - - max_distance: Maximum L2 distance - - avg_distance: Average L2 distance + list[dict] + List of dictionaries containing embedding results with keys: + - name: document name + - text: embedded text content + - page_number: page number in source document + - chunk_number: chunk number within the document + - distance: L2 distance from query embedding + - file_id: GUID of the source file """ - start_time = time.time() - encoding_start = time.time() transformerModel = TransformerModel.get_instance().model embedding_message = transformerModel.encode(message_data) @@ -61,7 +48,7 @@ def get_closest_embeddings( db_query_start = time.time() - # Start building the query based on the message's embedding + # Django QuerySets are lazily evaluated closest_embeddings_query = ( Embeddings.objects.filter(upload_file__uploaded_by=user) .annotate( @@ -70,7 +57,7 @@ def get_closest_embeddings( .order_by("distance") ) - # Filtering results to a document GUID takes precedence over filtering results to document name + # Filtering to a document GUID takes precedence over a document name if guid: closest_embeddings_query = closest_embeddings_query.filter( upload_file__guid=guid @@ -78,10 +65,10 @@ def get_closest_embeddings( elif document_name: closest_embeddings_query = closest_embeddings_query.filter(name=document_name) - # Slice the results to limit to num_results + # Slicing is equivalent to SQL's LIMIT clause closest_embeddings_query = closest_embeddings_query[:num_results] - # Format the results to be returned + # Iterating evaluates the QuerySet and hits the database # TODO: Research improving the query evaluation performance results = [ { @@ -96,38 +83,41 @@ def get_closest_embeddings( ] db_query_time = time.time() - db_query_start - total_time = time.time() - start_time - - # Calculate distance/similarity statistics - num_results_returned = len(results) - - #TODO: Handle user having no uploaded docs or doc filtering returning no matches - - distances = [r["distance"] for r in results] - min_distance = min(distances) - max_distance = max(distances) - avg_distance = sum(distances) / num_results_returned - - logger.info( - f"Embedding search completed: " - f"Encoding time: {encoding_time:.3f}s, " - f"DB query time: {db_query_time:.3f}s, " - f"Total time: {total_time:.3f}s, " - f"Returned: {num_results_returned} results, " - f"Distance range: [{min_distance:.3f}, {max_distance:.3f}], " - f"Average distance: {avg_distance:.3f}" - ) - if return_metrics: - metrics = { - "encoding_time": encoding_time, - "db_query_time": db_query_time, - "total_time": total_time, - "num_results_returned": num_results_returned, - "min_distance": min_distance, - "max_distance": max_distance, - "avg_distance": avg_distance, - } - return results, metrics + try: + # Handle user having no uploaded docs or doc filtering returning no matches + if results: + distances = [r["distance"] for r in results] + SemanticSearchUsage.objects.create( + query_text=message_data, + user=user if (user and user.is_authenticated) else None, + document_guid=guid, + document_name=document_name, + num_results_requested=num_results, + encoding_time=encoding_time, + db_query_time=db_query_time, + num_results_returned=len(results), + max_distance=max(distances), + median_distance=median(distances), + min_distance=min(distances) + ) + else: + logger.warning("Semantic search returned no results") + + SemanticSearchUsage.objects.create( + query_text=message_data, + user=user if (user and user.is_authenticated) else None, + document_guid=guid, + document_name=document_name, + num_results_requested=num_results, + encoding_time=encoding_time, + db_query_time=db_query_time, + num_results_returned=0, + max_distance=None, + median_distance=None, + min_distance=None + ) + except Exception as e: + logger.error(f"Failed to create semantic search usage database record: {e}") return results From 6a843596d50076e1c1877b0ebf0a32396880a0e0 Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Wed, 26 Nov 2025 17:20:25 -0500 Subject: [PATCH 04/14] Add semantic search usage migration file --- .../migrations/0015_semanticsearchusage.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 server/api/migrations/0015_semanticsearchusage.py diff --git a/server/api/migrations/0015_semanticsearchusage.py b/server/api/migrations/0015_semanticsearchusage.py new file mode 100644 index 00000000..0475b71f --- /dev/null +++ b/server/api/migrations/0015_semanticsearchusage.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.3 on 2025-11-26 21:02 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0014_alter_medrule_rule_type'), + ] + + operations = [ + migrations.CreateModel( + name='SemanticSearchUsage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('guid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('query_text', models.TextField(blank=True, help_text='The search query text', null=True)), + ('document_name', models.TextField(blank=True, help_text='Document name filter if used', null=True)), + ('document_guid', models.UUIDField(blank=True, help_text='Document GUID filter if used', null=True)), + ('num_results_requested', models.IntegerField(default=10, help_text='Number of results requested')), + ('encoding_time', models.FloatField(help_text='Time to encode query in seconds')), + ('db_query_time', models.FloatField(help_text='Time for database query in seconds')), + ('num_results_returned', models.IntegerField(help_text='Number of results returned')), + ('min_distance', models.FloatField(blank=True, help_text='Minimum L2 distance (null if no results)', null=True)), + ('max_distance', models.FloatField(blank=True, help_text='Maximum L2 distance (null if no results)', null=True)), + ('median_distance', models.FloatField(blank=True, help_text='Median L2 distance (null if no results)', null=True)), + ('user', models.ForeignKey(blank=True, help_text='User who performed the search (null for unauthenticated users)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='semantic_searches', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-timestamp'], + 'indexes': [models.Index(fields=['-timestamp'], name='api_semanti_timesta_0b5730_idx'), models.Index(fields=['user', '-timestamp'], name='api_semanti_user_id_e11ecb_idx')], + }, + ), + ] From bf79b4a9c4a9e9582d79de2a37690f0e18980493 Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Fri, 19 Dec 2025 16:03:11 -0500 Subject: [PATCH 05/14] Fix User logged as None --- frontend/src/api/apiClient.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts index 915226d6..0a566613 100644 --- a/frontend/src/api/apiClient.ts +++ b/frontend/src/api/apiClient.ts @@ -289,7 +289,9 @@ const sendAssistantMessage = async ( previousResponseId?: string, ) => { try { - const response = await publicApi.post(`/v1/api/assistant`, { + // The adminApi interceptor will automatically include your JWT token + // if you're authenticated, and gracefully omit it if you're not + const response = await adminApi.post(`/v1/api/assistant`, { message, previous_response_id: previousResponseId, }); From 163e1072ef2e51dd87773e47c61d35355812b397 Mon Sep 17 00:00:00 2001 From: Sahil D Shah Date: Fri, 19 Dec 2025 18:23:50 -0500 Subject: [PATCH 06/14] Address the user tracking issue without a 401 error --- frontend/src/api/apiClient.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts index 0a566613..08719bb4 100644 --- a/frontend/src/api/apiClient.ts +++ b/frontend/src/api/apiClient.ts @@ -289,9 +289,9 @@ const sendAssistantMessage = async ( previousResponseId?: string, ) => { try { - // The adminApi interceptor will automatically include your JWT token - // if you're authenticated, and gracefully omit it if you're not - const response = await adminApi.post(`/v1/api/assistant`, { + // The adminApi interceptor doesn't gracefully omit the JWT token if you're not authenticated + const api = localStorage.getItem("access") ? adminApi : publicApi; + const response = await api.post(`/v1/api/assistant`, { message, previous_response_id: previousResponseId, }); From 764048cc4e0e3721d309205027b94bd0f6147cfc Mon Sep 17 00:00:00 2001 From: Christopher Tineo Date: Sun, 28 Dec 2025 11:54:59 -0500 Subject: [PATCH 07/14] Update VITE_API_BASE_URL to point to the new prod url --- frontend/.env.production | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/.env.production b/frontend/.env.production index a05a022d..71adcf10 100644 --- a/frontend/.env.production +++ b/frontend/.env.production @@ -1 +1 @@ -VITE_API_BASE_URL=https://balancer.live.k8s.phl.io/ \ No newline at end of file +VITE_API_BASE_URL=https://balancerproject.org/ \ No newline at end of file From b8a3619435a2a8dea31798a933e2c4d5282a44b9 Mon Sep 17 00:00:00 2001 From: Christopher Tineo Date: Tue, 6 Jan 2026 18:54:03 -0500 Subject: [PATCH 08/14] Add centralized API endpoints configuration and refactor API calls - Introduced a new `endpoints.ts` file to centralize all API endpoint paths for better maintainability and type safety. - Updated various components and services to utilize the new centralized endpoints, enhancing consistency across the codebase. - Created a comprehensive `API_ENDPOINTS_REFACTORING.md` guide detailing the refactoring process and usage patterns for the new endpoints. - Removed hardcoded URLs in favor of the centralized configuration, improving code readability and reducing the risk of errors. --- frontend/API_ENDPOINTS_REFACTORING.md | 216 ++++++++++++++++++ frontend/src/api/apiClient.ts | 37 +-- frontend/src/api/endpoints.ts | 137 +++++++++++ .../src/pages/DocumentManager/UploadFile.tsx | 3 +- frontend/src/pages/DrugSummary/PDFViewer.tsx | 6 +- frontend/src/pages/Files/FileRow.tsx | 4 +- frontend/src/pages/Files/ListOfFiles.tsx | 6 +- .../src/pages/Layout/Layout_V2_Sidebar.tsx | 3 +- .../src/pages/ListMeds/useMedications.tsx | 6 +- frontend/src/pages/ManageMeds/ManageMeds.tsx | 7 +- .../pages/PatientManager/NewPatientForm.tsx | 3 +- .../pages/PatientManager/PatientSummary.tsx | 12 +- .../src/pages/RulesManager/RulesManager.tsx | 6 +- .../src/pages/Settings/SettingsManager.tsx | 7 +- frontend/src/services/actions/auth.tsx | 19 +- server/balancer_backend/urls.py | 26 ++- 16 files changed, 420 insertions(+), 78 deletions(-) create mode 100644 frontend/API_ENDPOINTS_REFACTORING.md create mode 100644 frontend/src/api/endpoints.ts diff --git a/frontend/API_ENDPOINTS_REFACTORING.md b/frontend/API_ENDPOINTS_REFACTORING.md new file mode 100644 index 00000000..a765fd71 --- /dev/null +++ b/frontend/API_ENDPOINTS_REFACTORING.md @@ -0,0 +1,216 @@ +# API Endpoints Refactoring Guide + +This document explains how to refactor API URLs to use the centralized endpoints configuration. + +## Overview + +All API endpoints are now centralized in `src/api/endpoints.ts`. This makes it: +- **Maintainable**: Change URLs in one place +- **Type-safe**: TypeScript ensures correct usage +- **Discoverable**: All endpoints are documented in one file +- **Consistent**: No more typos or inconsistent paths + +## Usage Patterns + +### 1. Simple Static Endpoints + +**Before:** +```typescript +const url = `/api/v1/api/feedback/`; +await publicApi.post(url, data); +``` + +**After:** +```typescript +import { V1_API_ENDPOINTS } from "../api/endpoints"; + +await publicApi.post(V1_API_ENDPOINTS.FEEDBACK, data); +``` + +### 2. Dynamic Endpoints with Parameters + +**Before:** +```typescript +const url = `/api/v1/api/uploadFile/${guid}`; +await fetch(url); +``` + +**After:** +```typescript +import { endpoints } from "../api/endpoints"; + +const url = endpoints.uploadFile(guid); +await fetch(url); +``` + +### 3. Endpoints with Query Parameters + +**Before:** +```typescript +const endpoint = guid + ? `/api/v1/api/embeddings/ask_embeddings?guid=${guid}` + : '/api/v1/api/embeddings/ask_embeddings'; +``` + +**After:** +```typescript +import { endpoints } from "../api/endpoints"; + +const endpoint = endpoints.embeddingsAsk(guid); +``` + +## Available Endpoint Groups + +### Authentication Endpoints +```typescript +import { AUTH_ENDPOINTS } from "../api/endpoints"; + +AUTH_ENDPOINTS.JWT_VERIFY +AUTH_ENDPOINTS.JWT_CREATE +AUTH_ENDPOINTS.USER_ME +AUTH_ENDPOINTS.RESET_PASSWORD +AUTH_ENDPOINTS.RESET_PASSWORD_CONFIRM +``` + +### V1 API Endpoints +```typescript +import { V1_API_ENDPOINTS } from "../api/endpoints"; + +V1_API_ENDPOINTS.FEEDBACK +V1_API_ENDPOINTS.UPLOAD_FILE +V1_API_ENDPOINTS.GET_FULL_LIST_MED +V1_API_ENDPOINTS.MED_RULES +// ... and more +``` + +### Conversation Endpoints +```typescript +import { CONVERSATION_ENDPOINTS } from "../api/endpoints"; + +CONVERSATION_ENDPOINTS.CONVERSATIONS +CONVERSATION_ENDPOINTS.EXTRACT_TEXT +``` + +### AI Settings Endpoints +```typescript +import { AI_SETTINGS_ENDPOINTS } from "../api/endpoints"; + +AI_SETTINGS_ENDPOINTS.SETTINGS +``` + +### Helper Functions +```typescript +import { endpoints } from "../api/endpoints"; + +endpoints.embeddingsAsk(guid?) +endpoints.embeddingsAskStream(guid?) +endpoints.ruleExtraction(guid) +endpoints.conversation(id) +endpoints.continueConversation(id) +endpoints.updateConversationTitle(id) +endpoints.uploadFile(guid) +endpoints.editMetadata(guid) +``` + +## Files to Refactor + +The following files still need to be updated to use the centralized endpoints: + +1. `src/pages/Settings/SettingsManager.tsx` - Use `AI_SETTINGS_ENDPOINTS.SETTINGS` +2. `src/pages/RulesManager/RulesManager.tsx` - Use `V1_API_ENDPOINTS.MED_RULES` +3. `src/pages/PatientManager/NewPatientForm.tsx` - Use `V1_API_ENDPOINTS.GET_MED_RECOMMEND` +4. `src/pages/ManageMeds/ManageMeds.tsx` - Use `V1_API_ENDPOINTS.*` for all medication endpoints +5. `src/pages/ListMeds/useMedications.tsx` - Use `V1_API_ENDPOINTS.GET_FULL_LIST_MED` +6. `src/pages/Layout/Layout_V2_Sidebar.tsx` - Use `V1_API_ENDPOINTS.UPLOAD_FILE` +7. `src/pages/Files/ListOfFiles.tsx` - Use `V1_API_ENDPOINTS.UPLOAD_FILE` +8. `src/pages/DocumentManager/UploadFile.tsx` - Use `V1_API_ENDPOINTS.UPLOAD_FILE` +9. `src/pages/Files/FileRow.tsx` - Use `endpoints.editMetadata(guid)` +10. `src/pages/DrugSummary/PDFViewer.tsx` - Use `endpoints.uploadFile(guid)` +11. `src/pages/PatientManager/PatientSummary.tsx` - Use `endpoints.uploadFile(guid)` + +## Example Refactoring + +### Example 1: SettingsManager.tsx + +**Before:** +```typescript +const baseUrl = import.meta.env.VITE_API_BASE_URL || "http://localhost:8000"; +const url = `${baseUrl}/ai_settings/settings/`; +``` + +**After:** +```typescript +import { AI_SETTINGS_ENDPOINTS } from "../../api/endpoints"; + +const url = AI_SETTINGS_ENDPOINTS.SETTINGS; +``` + +### Example 2: FileRow.tsx + +**Before:** +```typescript +const baseUrl = import.meta.env.VITE_API_BASE_URL as string; +await fetch(`${baseUrl}/v1/api/editmetadata/${file.guid}`, { +``` + +**After:** +```typescript +import { endpoints } from "../../api/endpoints"; + +await fetch(endpoints.editMetadata(file.guid), { +``` + +### Example 3: ManageMeds.tsx + +**Before:** +```typescript +const baseUrl = import.meta.env.VITE_API_BASE_URL; +const url = `${baseUrl}/v1/api/get_full_list_med`; +await adminApi.delete(`${baseUrl}/v1/api/delete_med`, { data: { name } }); +await adminApi.post(`${baseUrl}/v1/api/add_medication`, { ... }); +``` + +**After:** +```typescript +import { V1_API_ENDPOINTS } from "../../api/endpoints"; + +const url = V1_API_ENDPOINTS.GET_FULL_LIST_MED; +await adminApi.delete(V1_API_ENDPOINTS.DELETE_MED, { data: { name } }); +await adminApi.post(V1_API_ENDPOINTS.ADD_MEDICATION, { ... }); +``` + +## Benefits + +1. **Single Source of Truth**: All endpoints defined in one place +2. **Easy Updates**: Change an endpoint once, updates everywhere +3. **Type Safety**: TypeScript catches typos and incorrect usage +4. **Better IDE Support**: Autocomplete for all available endpoints +5. **Documentation**: Endpoints are self-documenting with clear names +6. **Refactoring Safety**: Rename endpoints safely across the codebase + +## Adding New Endpoints + +When adding a new endpoint: + +1. Add it to the appropriate group in `src/api/endpoints.ts` +2. If it needs dynamic parameters, add a helper function to `endpoints` object +3. Use the new endpoint in your code +4. Update this guide if needed + +Example: +```typescript +// In endpoints.ts +export const V1_API_ENDPOINTS = { + // ... existing endpoints + NEW_ENDPOINT: `${API_BASE}/v1/api/new_endpoint`, +} as const; + +// If it needs parameters: +export const endpoints = { + // ... existing helpers + newEndpoint: (id: string, param: string): string => { + return `${V1_API_ENDPOINTS.NEW_ENDPOINT}/${id}?param=${param}`; + }, +} as const; +``` + diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts index 915226d6..81859828 100644 --- a/frontend/src/api/apiClient.ts +++ b/frontend/src/api/apiClient.ts @@ -1,7 +1,14 @@ import axios from "axios"; import { FormValues } from "../pages/Feedback/FeedbackForm"; import { Conversation } from "../components/Header/Chat"; -const baseURL = import.meta.env.VITE_API_BASE_URL; +import { + V1_API_ENDPOINTS, + CONVERSATION_ENDPOINTS, + endpoints, +} from "./endpoints"; + +// Use empty string for relative URLs - all API calls will be relative to current domain +const baseURL = ""; export const publicApi = axios.create({ baseURL }); @@ -31,7 +38,7 @@ const handleSubmitFeedback = async ( message: FormValues["message"], ) => { try { - const response = await publicApi.post(`/v1/api/feedback/`, { + const response = await publicApi.post(V1_API_ENDPOINTS.FEEDBACK, { feedbacktype: feedbackType, name, email, @@ -49,7 +56,7 @@ const handleSendDrugSummary = async ( guid: string, ) => { try { - const endpoint = guid ? `/v1/api/embeddings/ask_embeddings?guid=${guid}` : '/v1/api/embeddings/ask_embeddings'; + const endpoint = endpoints.embeddingsAsk(guid); const response = await adminApi.post(endpoint, { message, }); @@ -63,7 +70,7 @@ const handleSendDrugSummary = async ( const handleRuleExtraction = async (guid: string) => { try { - const response = await adminApi.get(`/v1/api/rule_extraction_openai?guid=${guid}`); + const response = await adminApi.get(endpoints.ruleExtraction(guid)); // console.log("Rule extraction response:", JSON.stringify(response.data, null, 2)); return response.data; } catch (error) { @@ -77,7 +84,7 @@ const fetchRiskDataWithSources = async ( source: "include" | "diagnosis" | "diagnosis_depressed" = "include", ) => { try { - const response = await publicApi.post(`/v1/api/riskWithSources`, { + const response = await publicApi.post(V1_API_ENDPOINTS.RISK_WITH_SOURCES, { drug: medication, source: source, }); @@ -101,12 +108,10 @@ const handleSendDrugSummaryStream = async ( callbacks: StreamCallbacks, ): Promise => { const token = localStorage.getItem("access"); - const endpoint = `/v1/api/embeddings/ask_embeddings?stream=true${ - guid ? `&guid=${guid}` : "" - }`; + const endpoint = endpoints.embeddingsAskStream(guid); try { - const response = await fetch(baseURL + endpoint, { + const response = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json", @@ -206,7 +211,7 @@ const handleSendDrugSummaryStreamLegacy = async ( const fetchConversations = async (): Promise => { try { - const response = await publicApi.get(`/chatgpt/conversations/`); + const response = await publicApi.get(CONVERSATION_ENDPOINTS.CONVERSATIONS); return response.data; } catch (error) { console.error("Error(s) during getConversations: ", error); @@ -216,7 +221,7 @@ const fetchConversations = async (): Promise => { const fetchConversation = async (id: string): Promise => { try { - const response = await publicApi.get(`/chatgpt/conversations/${id}/`); + const response = await publicApi.get(endpoints.conversation(id)); return response.data; } catch (error) { console.error("Error(s) during getConversation: ", error); @@ -226,7 +231,7 @@ const fetchConversation = async (id: string): Promise => { const newConversation = async (): Promise => { try { - const response = await adminApi.post(`/chatgpt/conversations/`, { + const response = await adminApi.post(CONVERSATION_ENDPOINTS.CONVERSATIONS, { messages: [], }); return response.data; @@ -243,7 +248,7 @@ const continueConversation = async ( ): Promise<{ response: string; title: Conversation["title"] }> => { try { const response = await adminApi.post( - `/chatgpt/conversations/${id}/continue_conversation/`, + endpoints.continueConversation(id), { message, page_context, @@ -258,7 +263,7 @@ const continueConversation = async ( const deleteConversation = async (id: string) => { try { - const response = await adminApi.delete(`/chatgpt/conversations/${id}/`); + const response = await adminApi.delete(endpoints.conversation(id)); return response.data; } catch (error) { console.error("Error(s) during deleteConversation: ", error); @@ -273,7 +278,7 @@ const updateConversationTitle = async ( { status: string; title: Conversation["title"] } | { error: string } > => { try { - const response = await adminApi.patch(`/chatgpt/conversations/${id}/update_title/`, { + const response = await adminApi.patch(endpoints.updateConversationTitle(id), { title: newTitle, }); return response.data; @@ -289,7 +294,7 @@ const sendAssistantMessage = async ( previousResponseId?: string, ) => { try { - const response = await publicApi.post(`/v1/api/assistant`, { + const response = await publicApi.post(V1_API_ENDPOINTS.ASSISTANT, { message, previous_response_id: previousResponseId, }); diff --git a/frontend/src/api/endpoints.ts b/frontend/src/api/endpoints.ts new file mode 100644 index 00000000..6066b2ce --- /dev/null +++ b/frontend/src/api/endpoints.ts @@ -0,0 +1,137 @@ +/** + * Centralized API endpoints configuration + * + * This file contains all API endpoint paths used throughout the application. + * Update endpoints here to change them across the entire frontend. + */ + +const API_BASE = '/api'; + +/** + * Authentication endpoints + */ +export const AUTH_ENDPOINTS = { + JWT_VERIFY: `${API_BASE}/auth/jwt/verify/`, + JWT_CREATE: `${API_BASE}/auth/jwt/create/`, + USER_ME: `${API_BASE}/auth/users/me/`, + RESET_PASSWORD: `${API_BASE}/auth/users/reset_password/`, + RESET_PASSWORD_CONFIRM: `${API_BASE}/auth/users/reset_password_confirm/`, +} as const; + +/** + * V1 API endpoints + */ +export const V1_API_ENDPOINTS = { + // Feedback + FEEDBACK: `${API_BASE}/v1/api/feedback/`, + + // Embeddings + EMBEDDINGS_ASK: `${API_BASE}/v1/api/embeddings/ask_embeddings`, + RULE_EXTRACTION: `${API_BASE}/v1/api/rule_extraction_openai`, + + // Risk + RISK_WITH_SOURCES: `${API_BASE}/v1/api/riskWithSources`, + + // Assistant + ASSISTANT: `${API_BASE}/v1/api/assistant`, + + // File Management + UPLOAD_FILE: `${API_BASE}/v1/api/uploadFile`, + EDIT_METADATA: `${API_BASE}/v1/api/editmetadata`, + + // Medications + GET_FULL_LIST_MED: `${API_BASE}/v1/api/get_full_list_med`, + GET_MED_RECOMMEND: `${API_BASE}/v1/api/get_med_recommend`, + ADD_MEDICATION: `${API_BASE}/v1/api/add_medication`, + DELETE_MED: `${API_BASE}/v1/api/delete_med`, + + // Medication Rules + MED_RULES: `${API_BASE}/v1/api/medRules`, +} as const; + +/** + * ChatGPT/Conversations endpoints + */ +export const CONVERSATION_ENDPOINTS = { + CONVERSATIONS: `${API_BASE}/chatgpt/conversations/`, + EXTRACT_TEXT: `${API_BASE}/chatgpt/extract_text/`, +} as const; + +/** + * AI Settings endpoints + */ +export const AI_SETTINGS_ENDPOINTS = { + SETTINGS: `${API_BASE}/ai_settings/settings/`, +} as const; + +/** + * Helper functions for dynamic endpoints + */ +export const endpoints = { + /** + * Get embeddings endpoint with optional GUID + */ + embeddingsAsk: (guid?: string): string => { + const base = V1_API_ENDPOINTS.EMBEDDINGS_ASK; + return guid ? `${base}?guid=${guid}` : base; + }, + + /** + * Get embeddings streaming endpoint + */ + embeddingsAskStream: (guid?: string): string => { + const base = `${V1_API_ENDPOINTS.EMBEDDINGS_ASK}?stream=true`; + return guid ? `${base}&guid=${guid}` : base; + }, + + /** + * Get rule extraction endpoint with GUID + */ + ruleExtraction: (guid: string): string => { + return `${V1_API_ENDPOINTS.RULE_EXTRACTION}?guid=${guid}`; + }, + + /** + * Get conversation by ID + */ + conversation: (id: string): string => { + return `${CONVERSATION_ENDPOINTS.CONVERSATIONS}${id}/`; + }, + + /** + * Continue conversation endpoint + */ + continueConversation: (id: string): string => { + return `${CONVERSATION_ENDPOINTS.CONVERSATIONS}${id}/continue_conversation/`; + }, + + /** + * Update conversation title endpoint + */ + updateConversationTitle: (id: string): string => { + return `${CONVERSATION_ENDPOINTS.CONVERSATIONS}${id}/update_title/`; + }, + + /** + * Get upload file endpoint with GUID + */ + uploadFile: (guid: string): string => { + return `${V1_API_ENDPOINTS.UPLOAD_FILE}/${guid}`; + }, + + /** + * Edit metadata endpoint with GUID + */ + editMetadata: (guid: string): string => { + return `${V1_API_ENDPOINTS.EDIT_METADATA}/${guid}`; + }, +} as const; + +/** + * Type-safe endpoint values + */ +export type AuthEndpoint = typeof AUTH_ENDPOINTS[keyof typeof AUTH_ENDPOINTS]; +export type V1ApiEndpoint = typeof V1_API_ENDPOINTS[keyof typeof V1_API_ENDPOINTS]; +export type ConversationEndpoint = typeof CONVERSATION_ENDPOINTS[keyof typeof CONVERSATION_ENDPOINTS]; +export type AiSettingsEndpoint = typeof AI_SETTINGS_ENDPOINTS[keyof typeof AI_SETTINGS_ENDPOINTS]; + diff --git a/frontend/src/pages/DocumentManager/UploadFile.tsx b/frontend/src/pages/DocumentManager/UploadFile.tsx index f3d0f477..2ee7b5db 100644 --- a/frontend/src/pages/DocumentManager/UploadFile.tsx +++ b/frontend/src/pages/DocumentManager/UploadFile.tsx @@ -22,9 +22,8 @@ const UploadFile: React.FC = () => { formData.append("file", file); try { - const baseUrl = import.meta.env.VITE_API_BASE_URL; const response = await axios.post( - `${baseUrl}/v1/api/uploadFile`, + `/api/v1/api/uploadFile`, formData, { headers: { diff --git a/frontend/src/pages/DrugSummary/PDFViewer.tsx b/frontend/src/pages/DrugSummary/PDFViewer.tsx index 39ddfbfc..e4aae111 100644 --- a/frontend/src/pages/DrugSummary/PDFViewer.tsx +++ b/frontend/src/pages/DrugSummary/PDFViewer.tsx @@ -10,6 +10,7 @@ import { import { Document, Page, pdfjs } from "react-pdf"; import { useLocation, useNavigate } from "react-router-dom"; import axios from "axios"; +import { endpoints } from "../../api/endpoints"; import "react-pdf/dist/esm/Page/AnnotationLayer.css"; import "react-pdf/dist/esm/Page/TextLayer.css"; import ZoomMenu from "./ZoomMenu"; @@ -50,11 +51,10 @@ const PDFViewer = () => { const params = new URLSearchParams(location.search); const guid = params.get("guid"); const pageParam = params.get("page"); - const baseURL = import.meta.env.VITE_API_BASE_URL as string | undefined; const pdfUrl = useMemo(() => { - return guid && baseURL ? `${baseURL}/v1/api/uploadFile/${guid}` : null; - }, [guid, baseURL]); + return guid ? endpoints.uploadFile(guid) : null; + }, [guid]); useEffect(() => setUiScalePct(Math.round(scale * 100)), [scale]); diff --git a/frontend/src/pages/Files/FileRow.tsx b/frontend/src/pages/Files/FileRow.tsx index 19665855..57ed66bf 100644 --- a/frontend/src/pages/Files/FileRow.tsx +++ b/frontend/src/pages/Files/FileRow.tsx @@ -1,5 +1,6 @@ import React, { useState } from "react"; import { Link } from "react-router-dom"; +import { endpoints } from "../../api/endpoints"; interface File { id: number; @@ -42,8 +43,7 @@ const FileRow: React.FC = ({ const handleSave = async () => { setLoading(true); try { - const baseUrl = import.meta.env.VITE_API_BASE_URL as string; - await fetch(`${baseUrl}/v1/api/editmetadata/${file.guid}`, { + await fetch(endpoints.editMetadata(file.guid), { method: "PATCH", headers: { "Content-Type": "application/json", diff --git a/frontend/src/pages/Files/ListOfFiles.tsx b/frontend/src/pages/Files/ListOfFiles.tsx index efed19e5..b6fff4ee 100644 --- a/frontend/src/pages/Files/ListOfFiles.tsx +++ b/frontend/src/pages/Files/ListOfFiles.tsx @@ -30,12 +30,10 @@ const ListOfFiles: React.FC<{ showTable?: boolean }> = ({ const [downloading, setDownloading] = useState(null); const [opening, setOpening] = useState(null); - const baseUrl = import.meta.env.VITE_API_BASE_URL; - useEffect(() => { const fetchFiles = async () => { try { - const url = `${baseUrl}/v1/api/uploadFile`; + const url = `/api/v1/api/uploadFile`; const { data } = await publicApi.get(url); @@ -50,7 +48,7 @@ const ListOfFiles: React.FC<{ showTable?: boolean }> = ({ }; fetchFiles(); - }, [baseUrl]); + }, []); const updateFileName = (guid: string, updatedFile: Partial) => { setFiles((prevFiles) => diff --git a/frontend/src/pages/Layout/Layout_V2_Sidebar.tsx b/frontend/src/pages/Layout/Layout_V2_Sidebar.tsx index bec32d50..b947c2d6 100644 --- a/frontend/src/pages/Layout/Layout_V2_Sidebar.tsx +++ b/frontend/src/pages/Layout/Layout_V2_Sidebar.tsx @@ -24,8 +24,7 @@ const Sidebar: React.FC = () => { useEffect(() => { const fetchFiles = async () => { try { - const baseUrl = import.meta.env.VITE_API_BASE_URL; - const response = await axios.get(`${baseUrl}/v1/api/uploadFile`); + const response = await axios.get(`/api/v1/api/uploadFile`); if (Array.isArray(response.data)) { setFiles(response.data); } diff --git a/frontend/src/pages/ListMeds/useMedications.tsx b/frontend/src/pages/ListMeds/useMedications.tsx index 022eb07a..d78702db 100644 --- a/frontend/src/pages/ListMeds/useMedications.tsx +++ b/frontend/src/pages/ListMeds/useMedications.tsx @@ -11,12 +11,10 @@ export function useMedications() { const [medications, setMedications] = useState([]); const [errors, setErrors] = useState([]); - const baseUrl = import.meta.env.VITE_API_BASE_URL; - useEffect(() => { const fetchMedications = async () => { try { - const url = `${baseUrl}/v1/api/get_full_list_med`; + const url = `/api/v1/api/get_full_list_med`; const { data } = await publicApi.get(url); @@ -44,7 +42,7 @@ export function useMedications() { }; fetchMedications(); - }, [baseUrl]); + }, []); console.log(medications); diff --git a/frontend/src/pages/ManageMeds/ManageMeds.tsx b/frontend/src/pages/ManageMeds/ManageMeds.tsx index 23493f7e..c2372b9e 100644 --- a/frontend/src/pages/ManageMeds/ManageMeds.tsx +++ b/frontend/src/pages/ManageMeds/ManageMeds.tsx @@ -18,11 +18,10 @@ function ManageMedications() { const [newMedRisks, setNewMedRisks] = useState(""); const [showAddMed, setShowAddMed] = useState(false); const [hoveredMed, setHoveredMed] = useState(null); - const baseUrl = import.meta.env.VITE_API_BASE_URL; // Fetch Medications const fetchMedications = async () => { try { - const url = `${baseUrl}/v1/api/get_full_list_med`; + const url = `/api/v1/api/get_full_list_med`; const { data } = await adminApi.get(url); data.sort((a: MedData, b: MedData) => a.name.localeCompare(b.name)); setMedications(data); @@ -36,7 +35,7 @@ function ManageMedications() { // Handle Delete Medication const handleDelete = async (name: string) => { try { - await adminApi.delete(`${baseUrl}/v1/api/delete_med`, { data: { name } }); + await adminApi.delete(`/api/v1/api/delete_med`, { data: { name } }); setMedications((prev) => prev.filter((med) => med.name !== name)); setConfirmDelete(null); } catch (e: unknown) { @@ -56,7 +55,7 @@ function ManageMedications() { return; } try { - await adminApi.post(`${baseUrl}/v1/api/add_medication`, { + await adminApi.post(`/api/v1/api/add_medication`, { name: newMedName, benefits: newMedBenefits, risks: newMedRisks, diff --git a/frontend/src/pages/PatientManager/NewPatientForm.tsx b/frontend/src/pages/PatientManager/NewPatientForm.tsx index b2ff2e01..94c718de 100644 --- a/frontend/src/pages/PatientManager/NewPatientForm.tsx +++ b/frontend/src/pages/PatientManager/NewPatientForm.tsx @@ -152,8 +152,7 @@ const NewPatientForm = ({ setIsLoading(true); // Start loading try { - const baseUrl = import.meta.env.VITE_API_BASE_URL; - const url = `${baseUrl}/v1/api/get_med_recommend`; + const url = `/api/v1/api/get_med_recommend`; const { data } = await publicApi.post(url, payload); diff --git a/frontend/src/pages/PatientManager/PatientSummary.tsx b/frontend/src/pages/PatientManager/PatientSummary.tsx index 9b8c462c..faab5e6a 100644 --- a/frontend/src/pages/PatientManager/PatientSummary.tsx +++ b/frontend/src/pages/PatientManager/PatientSummary.tsx @@ -67,7 +67,6 @@ const MedicationItem = ({ loading, onTierClick, isAuthenticated, - baseURL, }: { medication: string; source: string; @@ -76,7 +75,6 @@ const MedicationItem = ({ loading: boolean; onTierClick: () => void; isAuthenticated: boolean | null; - baseURL: string; }) => { if (medication === "None") { return ( @@ -183,7 +181,7 @@ const MedicationItem = ({ ) : ( @@ -233,7 +231,6 @@ const MedicationTier = ({ loading, onTierClick, isAuthenticated, - baseURL, }: { title: string; tier: string; @@ -243,7 +240,6 @@ const MedicationTier = ({ loading: boolean; onTierClick: (medication: MedicationWithSource) => void; isAuthenticated: boolean | null; - baseURL: string; }) => ( <>
@@ -261,7 +257,6 @@ const MedicationTier = ({ loading={loading} onTierClick={() => onTierClick(medicationObj)} isAuthenticated={isAuthenticated} - baseURL={baseURL} /> ))} @@ -280,7 +275,7 @@ const PatientSummary = ({ isPatientDeleted, isAuthenticated = false, }: PatientSummaryProps) => { - const baseURL = import.meta.env.VITE_API_BASE_URL || ''; + // Using relative URLs - no baseURL needed const [loading, setLoading] = useState(false); const [riskData, setRiskData] = useState(null); const [clickedMedication, setClickedMedication] = useState( @@ -423,7 +418,6 @@ const PatientSummary = ({ loading={loading} onTierClick={handleTierClick} isAuthenticated={isAuthenticated} - baseURL={baseURL} />
@@ -448,7 +441,6 @@ const PatientSummary = ({ loading={loading} onTierClick={handleTierClick} isAuthenticated={isAuthenticated} - baseURL={baseURL} />
diff --git a/frontend/src/pages/RulesManager/RulesManager.tsx b/frontend/src/pages/RulesManager/RulesManager.tsx index 0268a4c8..e77b39cd 100644 --- a/frontend/src/pages/RulesManager/RulesManager.tsx +++ b/frontend/src/pages/RulesManager/RulesManager.tsx @@ -63,12 +63,10 @@ function RulesManager() { const [isLoading, setIsLoading] = useState(true); const [expandedMeds, setExpandedMeds] = useState>(new Set()); - const baseUrl = import.meta.env.VITE_API_BASE_URL; - useEffect(() => { const fetchMedRules = async () => { try { - const url = `${baseUrl}/v1/api/medRules`; + const url = `/api/v1/api/medRules`; const { data } = await adminApi.get(url); if (!data || !Array.isArray(data.results)) { @@ -86,7 +84,7 @@ function RulesManager() { }; fetchMedRules(); - }, [baseUrl]); + }, []); const toggleMedication = (ruleId: number, medName: string) => { const medKey = `${ruleId}-${medName}`; diff --git a/frontend/src/pages/Settings/SettingsManager.tsx b/frontend/src/pages/Settings/SettingsManager.tsx index c16ded96..3854298c 100644 --- a/frontend/src/pages/Settings/SettingsManager.tsx +++ b/frontend/src/pages/Settings/SettingsManager.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from "react"; import axios from "axios"; +import { AI_SETTINGS_ENDPOINTS } from "../../api/endpoints"; // Define an interface for the setting items interface SettingItem { @@ -36,10 +37,8 @@ const SettingsManager: React.FC = () => { }, }; - // Use an environment variable for the base URL or directly insert the URL if not available - const baseUrl = - import.meta.env.VITE_API_BASE_URL || "http://localhost:8000"; - const url = `${baseUrl}/ai_settings/settings/`; + // Use centralized endpoint + const url = AI_SETTINGS_ENDPOINTS.SETTINGS; try { const response = await axios.get(url, config); setSettings(response.data); diff --git a/frontend/src/services/actions/auth.tsx b/frontend/src/services/actions/auth.tsx index 3dcfcac5..a6a30ff3 100644 --- a/frontend/src/services/actions/auth.tsx +++ b/frontend/src/services/actions/auth.tsx @@ -20,6 +20,7 @@ import { FACEBOOK_AUTH_FAIL, LOGOUT, } from "./types"; +import { AUTH_ENDPOINTS } from "../../api/endpoints"; import { ThunkAction } from "redux-thunk"; import { RootState } from "../reducers"; @@ -75,9 +76,7 @@ export const checkAuthenticated = () => async (dispatch: AppDispatch) => { }; const body = JSON.stringify({ token: localStorage.getItem("access") }); - const baseUrl = import.meta.env.VITE_API_BASE_URL; - console.log(baseUrl); - const url = `${baseUrl}/auth/jwt/verify/`; + const url = AUTH_ENDPOINTS.JWT_VERIFY; try { const res = await axios.post(url, body, config); @@ -113,9 +112,7 @@ export const load_user = (): ThunkType => async (dispatch: AppDispatch) => { Accept: "application/json", }, }; - const baseUrl = import.meta.env.VITE_API_BASE_URL; - console.log(baseUrl); - const url = `${baseUrl}/auth/users/me/`; + const url = AUTH_ENDPOINTS.USER_ME; try { const res = await axios.get(url, config); @@ -145,9 +142,7 @@ export const login = }; const body = JSON.stringify({ email, password }); - const baseUrl = import.meta.env.VITE_API_BASE_URL; - console.log(baseUrl); - const url = `${baseUrl}/auth/jwt/create/`; + const url = AUTH_ENDPOINTS.JWT_CREATE; try { const res = await axios.post(url, body, config); @@ -195,8 +190,7 @@ export const reset_password = }; console.log("yes"); const body = JSON.stringify({ email }); - const baseUrl = import.meta.env.VITE_API_BASE_URL; - const url = `${baseUrl}/auth/users/reset_password/`; + const url = AUTH_ENDPOINTS.RESET_PASSWORD; try { await axios.post(url, body, config); @@ -225,8 +219,7 @@ export const reset_password_confirm = }; const body = JSON.stringify({ uid, token, new_password, re_new_password }); - const baseUrl = import.meta.env.VITE_API_BASE_URL; - const url = `${baseUrl}/auth/users/reset_password_confirm/`; + const url = AUTH_ENDPOINTS.RESET_PASSWORD_CONFIRM; try { const response = await axios.post(url, body, config); dispatch({ diff --git a/server/balancer_backend/urls.py b/server/balancer_backend/urls.py index 56f307e4..d34c532f 100644 --- a/server/balancer_backend/urls.py +++ b/server/balancer_backend/urls.py @@ -8,15 +8,10 @@ import importlib # Import the importlib module for dynamic module importing # Define a list of URL patterns for the application +# Keep admin outside /api/ prefix urlpatterns = [ # Map 'admin/' URL to the Django admin interface path("admin/", admin.site.urls), - # Include Djoser's URL patterns under 'auth/' for basic auth - path("auth/", include("djoser.urls")), - # Include Djoser's JWT auth URL patterns under 'auth/' - path("auth/", include("djoser.urls.jwt")), - # Include Djoser's social auth URL patterns under 'auth/' - path("auth/", include("djoser.social.urls")), ] # List of application names for which URL patterns will be dynamically added @@ -34,15 +29,30 @@ "assistant", ] +# Build API URL patterns to be included under /api/ prefix +api_urlpatterns = [ + # Include Djoser's URL patterns under 'auth/' for basic auth + path("auth/", include("djoser.urls")), + # Include Djoser's JWT auth URL patterns under 'auth/' + path("auth/", include("djoser.urls.jwt")), + # Include Djoser's social auth URL patterns under 'auth/' + path("auth/", include("djoser.social.urls")), +] + # Loop through each application name and dynamically import and add its URL patterns for url in urls: # Dynamically import the URL module for each app url_module = importlib.import_module(f"api.views.{url}.urls") # Append the URL patterns from each imported module - urlpatterns += getattr(url_module, "urlpatterns", []) + api_urlpatterns += getattr(url_module, "urlpatterns", []) + +# Wrap all API routes under /api/ prefix +urlpatterns += [ + path("api/", include(api_urlpatterns)), +] # Add a catch-all URL pattern for handling SPA (Single Page Application) routing -# Serve 'index.html' for any unmatched URL +# Serve 'index.html' for any unmatched URL (must come after /api/ routes) urlpatterns += [ re_path(r"^.*$", TemplateView.as_view(template_name="index.html")), ] From 7a590e502800a29aad8ce710e9ef3e2cfb2a3f24 Mon Sep 17 00:00:00 2001 From: Christopher Tineo Date: Tue, 6 Jan 2026 18:54:54 -0500 Subject: [PATCH 09/14] refactor: use relative URLs and centralize API endpoints - Update Django URLs to serve all APIs under /api/ prefix - Change frontend to use relative URLs (empty baseURL) instead of environment-specific domains - Create centralized endpoints.ts for maintainable API URL management - Update all frontend components to use centralized endpoints - Remove all VITE_API_BASE_URL and REACT_APP_API_BASE_URL dependencies - Add helper functions for dynamic endpoints with parameters This ensures the same Docker image works in both production and sandbox environments without requiring environment-specific configuration. Fixes: - Frontend calling old domain (balancer.live.k8s.phl.io) - API calls failing after domain migration - /login and /adminportal pages not working Closes #431 --- server/balancer_backend/settings.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/balancer_backend/settings.py b/server/balancer_backend/settings.py index 58148617..9f917a94 100644 --- a/server/balancer_backend/settings.py +++ b/server/balancer_backend/settings.py @@ -106,13 +106,13 @@ # Build database configuration db_config = { - "ENGINE": os.environ.get("SQL_ENGINE", "django.db.backends.sqlite3"), - "NAME": os.environ.get("SQL_DATABASE", BASE_DIR / "db.sqlite3"), - "USER": os.environ.get("SQL_USER", "user"), - "PASSWORD": os.environ.get("SQL_PASSWORD", "password"), + "ENGINE": os.environ.get("SQL_ENGINE", "django.db.backends.sqlite3"), + "NAME": os.environ.get("SQL_DATABASE", BASE_DIR / "db.sqlite3"), + "USER": os.environ.get("SQL_USER", "user"), + "PASSWORD": os.environ.get("SQL_PASSWORD", "password"), "HOST": SQL_HOST, - "PORT": os.environ.get("SQL_PORT", "5432"), -} + "PORT": os.environ.get("SQL_PORT", "5432"), + } # Configure SSL/TLS based on connection type # CloudNativePG within cluster typically doesn't require SSL From 70e26efe188ebc6a81d15218dcb8ec5e653a4427 Mon Sep 17 00:00:00 2001 From: Christopher Tineo Date: Tue, 6 Jan 2026 18:56:40 -0500 Subject: [PATCH 10/14] remove fiel --- frontend/API_ENDPOINTS_REFACTORING.md | 216 -------------------------- 1 file changed, 216 deletions(-) delete mode 100644 frontend/API_ENDPOINTS_REFACTORING.md diff --git a/frontend/API_ENDPOINTS_REFACTORING.md b/frontend/API_ENDPOINTS_REFACTORING.md deleted file mode 100644 index a765fd71..00000000 --- a/frontend/API_ENDPOINTS_REFACTORING.md +++ /dev/null @@ -1,216 +0,0 @@ -# API Endpoints Refactoring Guide - -This document explains how to refactor API URLs to use the centralized endpoints configuration. - -## Overview - -All API endpoints are now centralized in `src/api/endpoints.ts`. This makes it: -- **Maintainable**: Change URLs in one place -- **Type-safe**: TypeScript ensures correct usage -- **Discoverable**: All endpoints are documented in one file -- **Consistent**: No more typos or inconsistent paths - -## Usage Patterns - -### 1. Simple Static Endpoints - -**Before:** -```typescript -const url = `/api/v1/api/feedback/`; -await publicApi.post(url, data); -``` - -**After:** -```typescript -import { V1_API_ENDPOINTS } from "../api/endpoints"; - -await publicApi.post(V1_API_ENDPOINTS.FEEDBACK, data); -``` - -### 2. Dynamic Endpoints with Parameters - -**Before:** -```typescript -const url = `/api/v1/api/uploadFile/${guid}`; -await fetch(url); -``` - -**After:** -```typescript -import { endpoints } from "../api/endpoints"; - -const url = endpoints.uploadFile(guid); -await fetch(url); -``` - -### 3. Endpoints with Query Parameters - -**Before:** -```typescript -const endpoint = guid - ? `/api/v1/api/embeddings/ask_embeddings?guid=${guid}` - : '/api/v1/api/embeddings/ask_embeddings'; -``` - -**After:** -```typescript -import { endpoints } from "../api/endpoints"; - -const endpoint = endpoints.embeddingsAsk(guid); -``` - -## Available Endpoint Groups - -### Authentication Endpoints -```typescript -import { AUTH_ENDPOINTS } from "../api/endpoints"; - -AUTH_ENDPOINTS.JWT_VERIFY -AUTH_ENDPOINTS.JWT_CREATE -AUTH_ENDPOINTS.USER_ME -AUTH_ENDPOINTS.RESET_PASSWORD -AUTH_ENDPOINTS.RESET_PASSWORD_CONFIRM -``` - -### V1 API Endpoints -```typescript -import { V1_API_ENDPOINTS } from "../api/endpoints"; - -V1_API_ENDPOINTS.FEEDBACK -V1_API_ENDPOINTS.UPLOAD_FILE -V1_API_ENDPOINTS.GET_FULL_LIST_MED -V1_API_ENDPOINTS.MED_RULES -// ... and more -``` - -### Conversation Endpoints -```typescript -import { CONVERSATION_ENDPOINTS } from "../api/endpoints"; - -CONVERSATION_ENDPOINTS.CONVERSATIONS -CONVERSATION_ENDPOINTS.EXTRACT_TEXT -``` - -### AI Settings Endpoints -```typescript -import { AI_SETTINGS_ENDPOINTS } from "../api/endpoints"; - -AI_SETTINGS_ENDPOINTS.SETTINGS -``` - -### Helper Functions -```typescript -import { endpoints } from "../api/endpoints"; - -endpoints.embeddingsAsk(guid?) -endpoints.embeddingsAskStream(guid?) -endpoints.ruleExtraction(guid) -endpoints.conversation(id) -endpoints.continueConversation(id) -endpoints.updateConversationTitle(id) -endpoints.uploadFile(guid) -endpoints.editMetadata(guid) -``` - -## Files to Refactor - -The following files still need to be updated to use the centralized endpoints: - -1. `src/pages/Settings/SettingsManager.tsx` - Use `AI_SETTINGS_ENDPOINTS.SETTINGS` -2. `src/pages/RulesManager/RulesManager.tsx` - Use `V1_API_ENDPOINTS.MED_RULES` -3. `src/pages/PatientManager/NewPatientForm.tsx` - Use `V1_API_ENDPOINTS.GET_MED_RECOMMEND` -4. `src/pages/ManageMeds/ManageMeds.tsx` - Use `V1_API_ENDPOINTS.*` for all medication endpoints -5. `src/pages/ListMeds/useMedications.tsx` - Use `V1_API_ENDPOINTS.GET_FULL_LIST_MED` -6. `src/pages/Layout/Layout_V2_Sidebar.tsx` - Use `V1_API_ENDPOINTS.UPLOAD_FILE` -7. `src/pages/Files/ListOfFiles.tsx` - Use `V1_API_ENDPOINTS.UPLOAD_FILE` -8. `src/pages/DocumentManager/UploadFile.tsx` - Use `V1_API_ENDPOINTS.UPLOAD_FILE` -9. `src/pages/Files/FileRow.tsx` - Use `endpoints.editMetadata(guid)` -10. `src/pages/DrugSummary/PDFViewer.tsx` - Use `endpoints.uploadFile(guid)` -11. `src/pages/PatientManager/PatientSummary.tsx` - Use `endpoints.uploadFile(guid)` - -## Example Refactoring - -### Example 1: SettingsManager.tsx - -**Before:** -```typescript -const baseUrl = import.meta.env.VITE_API_BASE_URL || "http://localhost:8000"; -const url = `${baseUrl}/ai_settings/settings/`; -``` - -**After:** -```typescript -import { AI_SETTINGS_ENDPOINTS } from "../../api/endpoints"; - -const url = AI_SETTINGS_ENDPOINTS.SETTINGS; -``` - -### Example 2: FileRow.tsx - -**Before:** -```typescript -const baseUrl = import.meta.env.VITE_API_BASE_URL as string; -await fetch(`${baseUrl}/v1/api/editmetadata/${file.guid}`, { -``` - -**After:** -```typescript -import { endpoints } from "../../api/endpoints"; - -await fetch(endpoints.editMetadata(file.guid), { -``` - -### Example 3: ManageMeds.tsx - -**Before:** -```typescript -const baseUrl = import.meta.env.VITE_API_BASE_URL; -const url = `${baseUrl}/v1/api/get_full_list_med`; -await adminApi.delete(`${baseUrl}/v1/api/delete_med`, { data: { name } }); -await adminApi.post(`${baseUrl}/v1/api/add_medication`, { ... }); -``` - -**After:** -```typescript -import { V1_API_ENDPOINTS } from "../../api/endpoints"; - -const url = V1_API_ENDPOINTS.GET_FULL_LIST_MED; -await adminApi.delete(V1_API_ENDPOINTS.DELETE_MED, { data: { name } }); -await adminApi.post(V1_API_ENDPOINTS.ADD_MEDICATION, { ... }); -``` - -## Benefits - -1. **Single Source of Truth**: All endpoints defined in one place -2. **Easy Updates**: Change an endpoint once, updates everywhere -3. **Type Safety**: TypeScript catches typos and incorrect usage -4. **Better IDE Support**: Autocomplete for all available endpoints -5. **Documentation**: Endpoints are self-documenting with clear names -6. **Refactoring Safety**: Rename endpoints safely across the codebase - -## Adding New Endpoints - -When adding a new endpoint: - -1. Add it to the appropriate group in `src/api/endpoints.ts` -2. If it needs dynamic parameters, add a helper function to `endpoints` object -3. Use the new endpoint in your code -4. Update this guide if needed - -Example: -```typescript -// In endpoints.ts -export const V1_API_ENDPOINTS = { - // ... existing endpoints - NEW_ENDPOINT: `${API_BASE}/v1/api/new_endpoint`, -} as const; - -// If it needs parameters: -export const endpoints = { - // ... existing helpers - newEndpoint: (id: string, param: string): string => { - return `${V1_API_ENDPOINTS.NEW_ENDPOINT}/${id}?param=${param}`; - }, -} as const; -``` - From 76cee0221bffb4d0b0ce80d0e8be4d6a79a968b4 Mon Sep 17 00:00:00 2001 From: Akhil Bolla Date: Wed, 14 Jan 2026 12:08:17 -0500 Subject: [PATCH 11/14] sanitizer --- server/api/views/assistant/sanitizer.py | 26 +++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 server/api/views/assistant/sanitizer.py diff --git a/server/api/views/assistant/sanitizer.py b/server/api/views/assistant/sanitizer.py new file mode 100644 index 00000000..bdbbc77f --- /dev/null +++ b/server/api/views/assistant/sanitizer.py @@ -0,0 +1,26 @@ +import re +import logging +logger = logging.getLogger(__name__) +def sanitize_input(user_input:str) -> str: + """ + Sanitize user input to prevent injection attacks and remove unwanted characters. + Args: + user_input (str): The raw input string from the user. + Returns: + str: The sanitized input string. + """ + try: + # Remove any script tags + sanitized = re.sub(r'.*?', '', user_input, flags=re.IGNORECASE) + # Remove any HTML tags + sanitized = re.sub(r'<.*?>', '', sanitized) + # Escape special characters + sanitized = re.sub(r'["\'\\]', '', sanitized) + # Limit length to prevent buffer overflow attacks + max_length = 1000 + if len(sanitized) > max_length: + sanitized = sanitized[:max_length] + return sanitized.strip() + except Exception as e: + logger.error(f"Error sanitizing input: {e}") + return "" \ No newline at end of file From 1c458f0248e912b79e6bead1c1685d57c057d01b Mon Sep 17 00:00:00 2001 From: Christopher Tineo Date: Wed, 14 Jan 2026 22:47:57 -0500 Subject: [PATCH 12/14] ci: refactor pipelines for continuous deployment to sandbox --- .github/workflows/containers-publish.yml | 19 ++++++++++- .github/workflows/deploy-downstream.yml | 41 ++++++++++++++++++------ 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/.github/workflows/containers-publish.yml b/.github/workflows/containers-publish.yml index 64758fe9..834f0da9 100644 --- a/.github/workflows/containers-publish.yml +++ b/.github/workflows/containers-publish.yml @@ -3,6 +3,8 @@ name: "Containers: Publish" on: release: types: [published] + push: + branches: [develop] permissions: packages: write @@ -24,7 +26,13 @@ jobs: - name: Compute Docker container image addresses run: | DOCKER_REPOSITORY="ghcr.io/${GITHUB_REPOSITORY,,}" - DOCKER_TAG="${GITHUB_REF:11}" + + if [[ "${{ github.event_name }}" == "release" ]]; then + DOCKER_TAG="${GITHUB_REF:11}" + else + SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) + DOCKER_TAG="dev-${SHORT_SHA}" + fi echo "DOCKER_REPOSITORY=${DOCKER_REPOSITORY}" >> $GITHUB_ENV echo "DOCKER_TAG=${DOCKER_TAG}" >> $GITHUB_ENV @@ -51,3 +59,12 @@ jobs: - name: "Push Docker container image app:v*" run: docker push "${DOCKER_REPOSITORY}/app:${DOCKER_TAG}" + + - name: Save Docker Tag + run: echo "${DOCKER_TAG}" > docker_tag.txt + + - name: Upload Docker Tag + uses: actions/upload-artifact@v4 + with: + name: docker-tag + path: docker_tag.txt diff --git a/.github/workflows/deploy-downstream.yml b/.github/workflows/deploy-downstream.yml index 2557ff17..e13309e8 100644 --- a/.github/workflows/deploy-downstream.yml +++ b/.github/workflows/deploy-downstream.yml @@ -1,8 +1,10 @@ name: "Deploy: Downstream Clusters" on: - release: - types: [published] + workflow_run: + workflows: ["Containers: Publish"] + types: + - completed workflow_dispatch: inputs: tag: @@ -14,6 +16,7 @@ jobs: update-sandbox: name: Update Sandbox Cluster runs-on: ubuntu-latest + if: ${{ github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'develop') }} outputs: tag: ${{ steps.get_tag.outputs.TAG }} steps: @@ -26,8 +29,12 @@ jobs: if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then echo "TAG=${{ inputs.tag }}" >> $GITHUB_OUTPUT else - echo "TAG=${GITHUB_REF:11}" >> $GITHUB_OUTPUT + gh run download ${{ github.event.workflow_run.id }} -n docker-tag + TAG=$(cat docker_tag.txt) + echo "TAG=${TAG}" >> $GITHUB_OUTPUT fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Checkout Sandbox Cluster uses: actions/checkout@v4 @@ -57,9 +64,25 @@ jobs: update-live: name: Update Live Cluster - needs: update-sandbox runs-on: ubuntu-latest + if: ${{ github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'release') }} steps: + - name: Checkout App + uses: actions/checkout@v4 + + - name: Get Release Tag + id: get_tag + run: | + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + echo "TAG=${{ inputs.tag }}" >> $GITHUB_OUTPUT + else + gh run download ${{ github.event.workflow_run.id }} -n docker-tag + TAG=$(cat docker_tag.txt) + echo "TAG=${TAG}" >> $GITHUB_OUTPUT + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout Live Cluster uses: actions/checkout@v4 with: @@ -71,7 +94,7 @@ jobs: working-directory: live/balancer run: | curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash - ./kustomize edit set image ghcr.io/codeforphilly/balancer-main/app:${{ needs.update-sandbox.outputs.tag }} + ./kustomize edit set image ghcr.io/codeforphilly/balancer-main/app:${{ steps.get_tag.outputs.TAG }} rm kustomize - name: Create Live PR @@ -79,9 +102,9 @@ jobs: with: token: ${{ secrets.BOT_GITHUB_TOKEN }} path: live - commit-message: "Deploy balancer ${{ needs.update-sandbox.outputs.tag }} to live" - title: "Deploy balancer ${{ needs.update-sandbox.outputs.tag }}" - body: "Updates balancer image tag to ${{ needs.update-sandbox.outputs.tag }}" - branch: "deploy/balancer-${{ needs.update-sandbox.outputs.tag }}" + commit-message: "Deploy balancer ${{ steps.get_tag.outputs.TAG }} to live" + title: "Deploy balancer ${{ steps.get_tag.outputs.TAG }}" + body: "Updates balancer image tag to ${{ steps.get_tag.outputs.TAG }}" + branch: "deploy/balancer-${{ steps.get_tag.outputs.TAG }}" base: main delete-branch: true From 76a99a98be7711b8a2fde67d617df13cdae1a872 Mon Sep 17 00:00:00 2001 From: Christopher Tineo Date: Thu, 15 Jan 2026 09:09:15 -0500 Subject: [PATCH 13/14] build: use cpu-only torch to reduce image size and fix CI build --- Dockerfile.prod | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile.prod b/Dockerfile.prod index cd1f3604..f2fc5a20 100644 --- a/Dockerfile.prod +++ b/Dockerfile.prod @@ -32,9 +32,11 @@ ENV PYTHONUNBUFFERED=1 RUN apt-get update && apt-get install -y netcat && rm -rf /var/lib/apt/lists/* # Install Python dependencies -RUN pip install --upgrade pip +RUN pip install --upgrade pip --no-cache-dir COPY server/requirements.txt . -RUN pip install -r requirements.txt +# Install CPU-only torch to save space (avoids ~4GB of CUDA libs) +RUN pip install torch --index-url https://download.pytorch.org/whl/cpu --no-cache-dir +RUN pip install -r requirements.txt --no-cache-dir # Copy backend application code COPY server/ . From ca8c21f52b3a0d3ee5ab520b908626942b2f6784 Mon Sep 17 00:00:00 2001 From: Christopher Tineo Date: Thu, 15 Jan 2026 09:46:57 -0500 Subject: [PATCH 14/14] fix(ci): use actions/checkout@v4 instead of non-existent v5 --- .github/workflows/containers-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/containers-publish.yml b/.github/workflows/containers-publish.yml index 834f0da9..e7293376 100644 --- a/.github/workflows/containers-publish.yml +++ b/.github/workflows/containers-publish.yml @@ -14,7 +14,7 @@ jobs: name: Build and Push runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v4 - name: Login to ghcr.io Docker registry uses: docker/login-action@v3