From d81f88e1d54ffa01fbc12a9729faf6edeff0d806 Mon Sep 17 00:00:00 2001 From: BrajamohanDas-afk Date: Mon, 2 Mar 2026 23:27:41 +0530 Subject: [PATCH 1/4] feat(eduaid_web): add API base fallback and backend connection status banner --- eduaid_web/src/App.css | 34 ++++++ eduaid_web/src/App.js | 62 ++++++++-- eduaid_web/src/utils/apiClient.js | 182 +++++++++++++++++++++--------- 3 files changed, 214 insertions(+), 64 deletions(-) diff --git a/eduaid_web/src/App.css b/eduaid_web/src/App.css index 74b5e053..977b3483 100644 --- a/eduaid_web/src/App.css +++ b/eduaid_web/src/App.css @@ -36,3 +36,37 @@ transform: rotate(360deg); } } + +.connection-banner { + padding: 8px 12px; + text-align: center; + font-size: 0.9rem; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; +} + +.connection-banner.down { + background: #b91c1c; + color: white; +} + +.connection-banner.error { + background: #facc15; + color: #111827; +} + +.connection-banner-retry { + background: #ffffff; + color: #b91c1c; + border: none; + padding: 4px 8px; + cursor: pointer; + border-radius: 4px; + font-size: 0.85rem; +} + +.connection-banner-retry:hover { + filter: brightness(0.95); +} diff --git a/eduaid_web/src/App.js b/eduaid_web/src/App.js index e9eef0a0..f3a8fefc 100644 --- a/eduaid_web/src/App.js +++ b/eduaid_web/src/App.js @@ -1,4 +1,5 @@ import "./App.css"; +import { useEffect, useState } from "react"; import { Routes, Route, HashRouter } from "react-router-dom"; import Home from "./pages/Home"; import Question_Type from "./pages/Question_Type"; @@ -6,19 +7,60 @@ import Text_Input from "./pages/Text_Input"; import Output from "./pages/Output"; import Previous from "./pages/Previous"; import NotFound from "./pages/PageNotFound"; +import apiClient from "./utils/apiClient"; + +const initialConnectionState = { status: "unknown", detail: "" }; + +function ConnectionBanner({ connection }) { + if (connection.status === "down") { + return ( +
+ + Backend disconnected.{" "} + {connection.detail || "Please check backend server and API URL."} + + +
+ ); + } + + if (connection.status === "error") { + return ( +
+ Backend responded with an error.{" "} + {connection.detail || "Please try again shortly."} +
+ ); + } + + return null; +} function App() { + const [connection, setConnection] = useState(initialConnectionState); + + useEffect(() => apiClient.subscribeConnectionStatus(setConnection), []); + return ( - - - } /> - } /> - } /> - } /> - } /> - } /> - - + <> + + + + } /> + } /> + } /> + } /> + } /> + } /> + + + ); } diff --git a/eduaid_web/src/utils/apiClient.js b/eduaid_web/src/utils/apiClient.js index 0b9c2576..7e087aae 100644 --- a/eduaid_web/src/utils/apiClient.js +++ b/eduaid_web/src/utils/apiClient.js @@ -1,89 +1,163 @@ // API client that works both in web and Electron environments class ApiClient { constructor() { - this.isElectron = typeof window !== 'undefined' && window.electronAPI; - this.baseUrl = this.isElectron - ? window.electronAPI.getApiConfig().baseUrl - : process.env.REACT_APP_BASE_URL || 'http://localhost:5000'; + this.isElectron = typeof window !== "undefined" && window.electronAPI; + this.baseUrl = this.isElectron + ? window.electronAPI.getApiConfig().baseUrl + : process.env.REACT_APP_BASE_URL || "http://localhost:5000"; + + this.fallbackBaseUrls = this.getFallbackBaseUrls(); + + this.listeners = new Set(); + this.currentStatus = "unknown"; // unknown | up | down | error + this.currentDetail = ""; + } + + subscribeConnectionStatus(listener) { + this.listeners.add(listener); + return () => this.listeners.delete(listener); + } + + notifyConnectionStatus(status, detail = "") { + this.listeners.forEach((listener) => { + try { + listener({ status, detail }); + } catch (error) { + console.error("Connection listener failed:", error); + } + }); + } + + setConnectionStatus(status, detail = "") { + if (this.currentStatus === status && this.currentDetail === detail) return; + this.currentStatus = status; + this.currentDetail = detail; + this.notifyConnectionStatus(status, detail); + } + + getFallbackBaseUrls() { + if (this.isElectron || process.env.REACT_APP_BASE_URL) return []; + if (typeof window === "undefined") return []; + + const host = window.location.hostname; + if (host !== "localhost" && host !== "127.0.0.1") return []; + + if (this.baseUrl.includes(":5000")) return ["http://localhost:5001"]; + if (this.baseUrl.includes(":5001")) return ["http://localhost:5000"]; + return []; + } + + async parseJsonResponse(response) { + if (!response.ok) { + const text = await response.text().catch(() => ""); + throw new Error(`HTTP ${response.status}: ${text || "Request Failed"}`); + } + return response.json(); + } + + async fetchJson(url, options = {}) { + const response = await fetch(url, options); + return this.parseJsonResponse(response); + } + + async requestWithFallback(endpoint, options = {}) { + const primaryUrl = `${this.baseUrl}${endpoint}`; + + try { + const data = await this.fetchJson(primaryUrl, options); + this.setConnectionStatus("up"); + return data; + } catch (error) { + const isNetworkError = error instanceof TypeError; + if (!isNetworkError) { + this.setConnectionStatus("error", error.message); + throw error; + } + + for (const fallbackBaseUrl of this.fallbackBaseUrls) { + try { + const data = await this.fetchJson(`${fallbackBaseUrl}${endpoint}`, options); + this.baseUrl = fallbackBaseUrl; + this.setConnectionStatus("up"); + return data; + } catch (fallbackError) { + if (!(fallbackError instanceof TypeError)) { + this.setConnectionStatus("error", fallbackError.message); + throw fallbackError; + } + } + } + + this.setConnectionStatus( + "down", + `Cannot reach backend at ${this.baseUrl}. Check server and REACT_APP_BASE_URL.` + ); + throw error; + } } async makeRequest(endpoint, options = {}) { if (this.isElectron) { - // Use Electron's IPC for API requests try { const response = await window.electronAPI.makeApiRequest(endpoint, options); - if (response.ok) { - return response.data; - } else { - throw new Error(`API request failed with status ${response.status}`); + if (!response.ok) { + const err = new Error(`API request failed with status ${response.status}`); + this.setConnectionStatus("error", err.message); + throw err; } + this.setConnectionStatus("up"); + return response.data; } catch (error) { - console.error('Electron API request failed:', error); + this.setConnectionStatus("down", error.message); throw error; } - } else { - // Use regular fetch for web environment - const url = `${this.baseUrl}${endpoint}`; - const response = await fetch(url, options); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - return await response.json(); } + + return this.requestWithFallback(endpoint, options); } // Convenience methods for common HTTP verbs async get(endpoint, options = {}) { - return this.makeRequest(endpoint, { ...options, method: 'GET' }); + return this.makeRequest(endpoint, { ...options, method: "GET" }); } async post(endpoint, data, options = {}) { return this.makeRequest(endpoint, { ...options, - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', - ...options.headers + "Content-Type": "application/json", + ...options.headers, }, - body: JSON.stringify(data) + body: JSON.stringify(data), }); } async postFormData(endpoint, formData, options = {}) { if (this.isElectron) { - // For Electron, we need to handle file uploads differently - // Since we can't easily pass files through IPC, we'll fall back to fetch - const url = `${this.baseUrl}${endpoint}`; - const response = await fetch(url, { - ...options, - method: 'POST', - body: formData - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - return await response.json(); - } else { - // For web, use FormData directly - const url = `${this.baseUrl}${endpoint}`; - const response = await fetch(url, { - ...options, - method: 'POST', - body: formData - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + try { + const response = await fetch(`${this.baseUrl}${endpoint}`, { + ...options, + method: "POST", + body: formData, + }); + const data = await this.parseJsonResponse(response); + this.setConnectionStatus("up"); + return data; + } catch (error) { + const status = error instanceof TypeError ? "down" : "error"; + this.setConnectionStatus(status, error.message); + throw error; } - - return await response.json(); } + + return this.requestWithFallback(endpoint, { + ...options, + method: "POST", + body: formData, + }); } } -// Export a singleton instance -export default new ApiClient(); +const apiClient = new ApiClient(); +export default apiClient; From bb69539d5dac35ba150ab0e18dd60f77ffa0ab6a Mon Sep 17 00:00:00 2001 From: BrajamohanDas-afk Date: Tue, 3 Mar 2026 00:35:05 +0530 Subject: [PATCH 2/4] preserve electron error status and recompute fallback targets --- eduaid_web/src/utils/apiClient.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/eduaid_web/src/utils/apiClient.js b/eduaid_web/src/utils/apiClient.js index 7e087aae..cf053c8e 100644 --- a/eduaid_web/src/utils/apiClient.js +++ b/eduaid_web/src/utils/apiClient.js @@ -6,8 +6,6 @@ class ApiClient { ? window.electronAPI.getApiConfig().baseUrl : process.env.REACT_APP_BASE_URL || "http://localhost:5000"; - this.fallbackBaseUrls = this.getFallbackBaseUrls(); - this.listeners = new Set(); this.currentStatus = "unknown"; // unknown | up | down | error this.currentDetail = ""; @@ -74,7 +72,11 @@ class ApiClient { throw error; } - for (const fallbackBaseUrl of this.fallbackBaseUrls) { + const fallbackBaseUrls = this.getFallbackBaseUrls().filter( + (url) => url !== this.baseUrl + ); + + for (const fallbackBaseUrl of fallbackBaseUrls) { try { const data = await this.fetchJson(`${fallbackBaseUrl}${endpoint}`, options); this.baseUrl = fallbackBaseUrl; @@ -102,13 +104,17 @@ class ApiClient { const response = await window.electronAPI.makeApiRequest(endpoint, options); if (!response.ok) { const err = new Error(`API request failed with status ${response.status}`); + err.isHttpError = true; this.setConnectionStatus("error", err.message); throw err; } this.setConnectionStatus("up"); return response.data; } catch (error) { - this.setConnectionStatus("down", error.message); + if (error?.isHttpError) { + throw error; + } + this.setConnectionStatus("down", error?.message || "API request failed"); throw error; } } From 36ef6e9e09765f8b0279d0a712c3724f4a37bced Mon Sep 17 00:00:00 2001 From: BrajamohanDas-afk Date: Tue, 3 Mar 2026 01:05:11 +0530 Subject: [PATCH 3/4] fixed the code suggested by code rabbit --- eduaid_web/src/utils/apiClient.js | 1 + 1 file changed, 1 insertion(+) diff --git a/eduaid_web/src/utils/apiClient.js b/eduaid_web/src/utils/apiClient.js index cf053c8e..7ccf3af1 100644 --- a/eduaid_web/src/utils/apiClient.js +++ b/eduaid_web/src/utils/apiClient.js @@ -13,6 +13,7 @@ class ApiClient { subscribeConnectionStatus(listener) { this.listeners.add(listener); + listener({ status: this.currentStatus, detail: this.currentDetail }); return () => this.listeners.delete(listener); } From 4bc43f69d83ce1b643552754495a69e54816ca0b Mon Sep 17 00:00:00 2001 From: BrajamohanDas-afk Date: Tue, 3 Mar 2026 17:09:13 +0530 Subject: [PATCH 4/4] fix(eduaid_web): address CodeRabbit feedback in apiClient --- eduaid_web/src/utils/apiClient.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/eduaid_web/src/utils/apiClient.js b/eduaid_web/src/utils/apiClient.js index 7ccf3af1..edff0599 100644 --- a/eduaid_web/src/utils/apiClient.js +++ b/eduaid_web/src/utils/apiClient.js @@ -13,7 +13,11 @@ class ApiClient { subscribeConnectionStatus(listener) { this.listeners.add(listener); - listener({ status: this.currentStatus, detail: this.currentDetail }); + try { + listener({ status: this.currentStatus, detail: this.currentDetail }); + } catch (error) { + console.error("Connection listener failed:", error); + } return () => this.listeners.delete(listener); } @@ -51,6 +55,11 @@ class ApiClient { const text = await response.text().catch(() => ""); throw new Error(`HTTP ${response.status}: ${text || "Request Failed"}`); } + + if (response.status === 204 || response.status === 205) return null; + + const contentLength = response.headers.get("Content-Length"); + if (contentLength === "0") return null; return response.json(); } @@ -153,7 +162,7 @@ class ApiClient { return data; } catch (error) { const status = error instanceof TypeError ? "down" : "error"; - this.setConnectionStatus(status, error.message); + this.setConnectionStatus(status, error?.message || "API request failed"); throw error; } }