From 095bd4db5fa6fbde98dea0073a9b6f769698725c Mon Sep 17 00:00:00 2001 From: Harshita Nagpal Date: Sat, 30 May 2026 01:01:26 +0530 Subject: [PATCH 1/5] feat(frontend): refactor state managers to centralize localStorage sync and add ErrorBoundary --- .github/pull_request_template.md | 47 ++++---- Frontend/src/admin/components/AdminHeader.jsx | 17 --- Frontend/src/components/ErrorBoundary.jsx | 110 ++++++++++++++++++ Frontend/src/main.jsx | 5 +- Frontend/src/store/adminStore.js | 2 + Frontend/src/store/authStore.js | 2 + Frontend/src/store/persistentStorage.js | 40 +++++++ Frontend/src/store/ticketStore.js | 2 + 8 files changed, 183 insertions(+), 42 deletions(-) create mode 100644 Frontend/src/components/ErrorBoundary.jsx create mode 100644 Frontend/src/store/persistentStorage.js diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 75f6956e..4b23b42d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,35 +1,34 @@ -# ๐Ÿ“Œ Pull Request Description +## Description -## ๐Ÿ”— Related Issue + -Closes # +## Related Issue -## ๐Ÿ“ Description + +Fixes # -Please include a summary of the changes and the related issue. +## Type of Change -## โœ… Type of Change +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Documentation update +- [ ] Configuration change -* [ ] Bug fix -* [ ] New feature -* [ ] Documentation update -* [x] Chore -* [ ] UI/UX improvement -* [ ] Other +## Checklist -## ๐Ÿงช Testing Done +- [ ] My code follows the project's coding style +- [ ] I have performed a self-review of my code +- [ ] I have commented my code where necessary +- [ ] I have updated the documentation accordingly +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or my feature works +- [ ] New and existing unit tests pass locally with my changes -* [ ] Tested locally -* [ ] Existing functionality verified -* [ ] No new warnings/errors introduced +## Screenshots (if applicable) -## ๐Ÿ“ธ Screenshots (if applicable) + -Add screenshots or proof here. +## Additional Notes -## โœ”๏ธ Checklist - -* [ ] My code follows the project guidelines -* [ ] I reviewed my own changes -* [ ] I linked the related issue -* [ ] This PR targets the `gssoc` branch + diff --git a/Frontend/src/admin/components/AdminHeader.jsx b/Frontend/src/admin/components/AdminHeader.jsx index b408f5f0..048243d3 100644 --- a/Frontend/src/admin/components/AdminHeader.jsx +++ b/Frontend/src/admin/components/AdminHeader.jsx @@ -13,28 +13,11 @@ import TicketSearchBar from '../../components/shared/TicketSearchBar'; */ const AdminHeader = ({ onMobileNavToggle, isSidebarCollapsed, onToggleSidebar }) => { const [isProfileOpen, setIsProfileOpen] = useState(false); - const [searchQuery, setSearchQuery] = useState(''); const dropdownRef = useRef(null); - const searchRef = useRef(null); const navigate = useNavigate(); const { logout, profile: adminProfile } = useAuthStore(); const initials = adminProfile?.full_name ? adminProfile.full_name.split(' ').map(n => n[0]).join('').toUpperCase() : 'AD'; - const handleSearchKeyDown = (e) => { - if (e.key === 'Enter' && searchQuery.trim()) { - navigate(`/admin/tickets?q=${encodeURIComponent(searchQuery.trim())}`); - searchRef.current?.blur(); - } else if (e.key === 'Escape') { - setSearchQuery(''); - searchRef.current?.blur(); - } - }; - - const handleSearchClear = () => { - setSearchQuery(''); - searchRef.current?.focus(); - }; - useEffect(() => { const handleClickOutside = (event) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { diff --git a/Frontend/src/components/ErrorBoundary.jsx b/Frontend/src/components/ErrorBoundary.jsx new file mode 100644 index 00000000..0831faa7 --- /dev/null +++ b/Frontend/src/components/ErrorBoundary.jsx @@ -0,0 +1,110 @@ +import React from 'react'; + +class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { hasError: false, error: null, errorInfo: null, copied: false }; + } + + static getDerivedStateFromError(error) { + return { hasError: true, error }; + } + + componentDidCatch(error, errorInfo) { + this.setState({ errorInfo }); + console.error("[Unhandled Error Caught by Boundary]:", error, errorInfo); + } + + handleCopyPayload = () => { + const payload = { + error: this.state.error?.toString() || "Unknown error", + stack: this.state.error?.stack || "No stack trace available", + componentStack: this.state.errorInfo?.componentStack || "No component stack available", + url: window.location.href, + timestamp: new Date().toISOString(), + userAgent: navigator.userAgent + }; + + navigator.clipboard.writeText(JSON.stringify(payload, null, 2)) + .then(() => { + this.setState({ copied: true }); + setTimeout(() => this.setState({ copied: false }), 2000); + }) + .catch(err => { + console.error("Failed to copy error payload:", err); + }); + }; + + handleReset = () => { + window.location.reload(); + }; + + render() { + if (this.state.hasError) { + return ( +
+ {/* Background Orbs */} +
+
+ +
+
+
+ + + +
+
+ +

System Interrupted

+

Our Neural Engine encountered an unhandled execution exception. The diagnostic log has been secured.

+ +
+

Error: {this.state.error?.message || this.state.error?.toString()}

+

{this.state.error?.stack || "No stack trace secured."}

+ {this.state.errorInfo?.componentStack && ( +

{this.state.errorInfo.componentStack}

+ )} +
+ +
+ + +
+
+
+ ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/Frontend/src/main.jsx b/Frontend/src/main.jsx index b9a1a6de..4798affc 100644 --- a/Frontend/src/main.jsx +++ b/Frontend/src/main.jsx @@ -2,9 +2,12 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.jsx' +import ErrorBoundary from './components/ErrorBoundary.jsx' createRoot(document.getElementById('root')).render( - + + + , ) diff --git a/Frontend/src/store/adminStore.js b/Frontend/src/store/adminStore.js index bc74631a..8bd84b98 100644 --- a/Frontend/src/store/adminStore.js +++ b/Frontend/src/store/adminStore.js @@ -1,5 +1,6 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; +import { safePersistStorage } from './persistentStorage'; const useAdminStore = create( persist( @@ -19,6 +20,7 @@ const useAdminStore = create( }), { name: 'admin-storage', + storage: safePersistStorage, } ) ); diff --git a/Frontend/src/store/authStore.js b/Frontend/src/store/authStore.js index 78685f5e..2edff540 100644 --- a/Frontend/src/store/authStore.js +++ b/Frontend/src/store/authStore.js @@ -3,6 +3,7 @@ import { persist } from 'zustand/middleware'; import { supabase } from '../lib/supabaseClient'; import { API_CONFIG } from '../config'; import useTicketStore from './ticketStore'; +import { safePersistStorage } from './persistentStorage'; const BACKEND_URL = API_CONFIG.BACKEND_URL; @@ -395,6 +396,7 @@ const useAuthStore = create( }), { name: 'auth-storage', + storage: safePersistStorage, partialize: (state) => ({ // Cache display-only profile fields. Role/status must come from the DB. profile: getProfileCache(state.profile) diff --git a/Frontend/src/store/persistentStorage.js b/Frontend/src/store/persistentStorage.js new file mode 100644 index 00000000..6eb0c435 --- /dev/null +++ b/Frontend/src/store/persistentStorage.js @@ -0,0 +1,40 @@ +import { createJSONStorage } from 'zustand/middleware'; + +// In-memory fallback cache when localStorage is unavailable (e.g. private mode, quota limits) +const inMemoryCache = {}; + +export const robustLocalStorage = { + getItem: (name) => { + try { + return localStorage.getItem(name); + } catch (error) { + console.warn(`[PersistentStorage Read Error] Failed to read key "${name}" from localStorage. Falling back to memory.`, error); + return inMemoryCache[name] || null; + } + }, + setItem: (name, value) => { + try { + localStorage.setItem(name, value); + } catch (error) { + console.error(`[PersistentStorage Write Error] Failed to write key "${name}" to localStorage. Falling back to memory.`, error); + inMemoryCache[name] = value; + + // Dispatch custom event to notify components or loggers about storage failure (e.g. QuotaExceededError) + const isQuotaExceeded = error.name === 'QuotaExceededError' || error.code === 22; + window.dispatchEvent(new CustomEvent('persistent-storage-error', { + detail: { name, error, isQuotaExceeded } + })); + } + }, + removeItem: (name) => { + try { + localStorage.removeItem(name); + delete inMemoryCache[name]; + } catch (error) { + console.warn(`[PersistentStorage Remove Error] Failed to remove key "${name}" from localStorage.`, error); + delete inMemoryCache[name]; + } + } +}; + +export const safePersistStorage = createJSONStorage(() => robustLocalStorage); diff --git a/Frontend/src/store/ticketStore.js b/Frontend/src/store/ticketStore.js index fb070883..e68345dc 100644 --- a/Frontend/src/store/ticketStore.js +++ b/Frontend/src/store/ticketStore.js @@ -1,5 +1,6 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; +import { safePersistStorage } from './persistentStorage'; const useTicketStore = create( persist( @@ -134,6 +135,7 @@ const useTicketStore = create( }), { name: 'ticket-storage', // unique name for localStorage key + storage: safePersistStorage, } ) ); From 0c00f95aeab579d02736883005ab1cbfe951afe4 Mon Sep 17 00:00:00 2001 From: Harshita Nagpal Date: Sat, 30 May 2026 01:12:54 +0530 Subject: [PATCH 2/5] fix(backend): resolve smoke test absolute imports and FastAPI parameter assertion errors --- backend/routes/estimator.py | 4 ++-- backend/routes/translation.py | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/backend/routes/estimator.py b/backend/routes/estimator.py index 9fd00c46..3886dead 100644 --- a/backend/routes/estimator.py +++ b/backend/routes/estimator.py @@ -6,7 +6,7 @@ from pydantic import BaseModel, Field from typing import Optional -from services.response_time_estimator import ( +from backend.services.response_time_estimator import ( estimate_response_time, generate_estimation_summary, ) @@ -49,6 +49,6 @@ async def estimate(request: EstimateRequest): @router.get("/sla-targets") async def get_sla_targets(): """Get SLA targets for all priority levels.""" - from services.response_time_estimator import SLA_TARGETS + from backend.services.response_time_estimator import SLA_TARGETS return {"success": True, "data": SLA_TARGETS} diff --git a/backend/routes/translation.py b/backend/routes/translation.py index 993f9ee1..ee20718c 100644 --- a/backend/routes/translation.py +++ b/backend/routes/translation.py @@ -6,7 +6,7 @@ from pydantic import BaseModel, Field from typing import Optional -from services.translation_service import ( +from backend.services.translation_service import ( translate_text, translate_ticket, detect_language, @@ -29,6 +29,11 @@ class TranslateTicketRequest(BaseModel): target_lang: str = Field(default="en", max_length=5) +class DetectLanguageRequest(BaseModel): + text: str = Field(..., min_length=1) + + + @router.post("/translate") async def translate(request: TranslateTextRequest): """Translate text to target language with auto-detection.""" @@ -62,9 +67,9 @@ async def translate_ticket_endpoint(request: TranslateTicketRequest): @router.post("/detect") -async def detect(text: str = Field(..., min_length=1)): +async def detect(request: DetectLanguageRequest): """Detect the language of the given text.""" - lang = detect_language(text) + lang = detect_language(request.text) if not lang: raise HTTPException(status_code=400, detail="Could not detect language") languages = get_supported_languages() From 5d2a8306bb7dd09c847881800269ee63e8fbde20 Mon Sep 17 00:00:00 2001 From: Harshita Nagpal Date: Sat, 30 May 2026 01:25:28 +0530 Subject: [PATCH 3/5] fix(gssoc): resolve upstream merge regressions, missing imports, and syntax/ESLint errors --- Frontend/src/admin/pages/AdminSettings.jsx | 7 ++-- Frontend/src/docs/pages/DocsPortal.jsx | 2 +- Frontend/src/pages/AdminSignup.jsx | 5 +-- Frontend/src/pages/Login.jsx | 40 ++-------------------- Frontend/src/pages/Signup.jsx | 12 ++----- Frontend/src/user/pages/AIProcessing.jsx | 5 ++- backend/main.py | 1 + 7 files changed, 14 insertions(+), 58 deletions(-) diff --git a/Frontend/src/admin/pages/AdminSettings.jsx b/Frontend/src/admin/pages/AdminSettings.jsx index 761ff1b6..617cfc5d 100644 --- a/Frontend/src/admin/pages/AdminSettings.jsx +++ b/Frontend/src/admin/pages/AdminSettings.jsx @@ -23,8 +23,6 @@ import { } from '../../utils/adminSettingsPersistence'; import { Select } from "../../components/ui/select"; -import useAuthStore from '../../store/authStore'; -import { supabase } from '../../lib/supabaseClient'; /** * AdminSettings Page @@ -115,6 +113,11 @@ const AdminSettings = () => { setStatusMessage('Saving changes...'); }; + const digestEnabled = settings.digestEnabled || false; + const handleDigestToggle = () => { + handleChange('digestEnabled', !digestEnabled); + }; + const handleSaveSettings = useCallback(() => { saveCompanySettings(settings); }, [saveCompanySettings, settings]); diff --git a/Frontend/src/docs/pages/DocsPortal.jsx b/Frontend/src/docs/pages/DocsPortal.jsx index c22fb331..15764f4a 100644 --- a/Frontend/src/docs/pages/DocsPortal.jsx +++ b/Frontend/src/docs/pages/DocsPortal.jsx @@ -73,7 +73,7 @@ const DocsPortal = () => { "Routed based on neural network rule matching" ] }, null, 2)); - } catch (e) { + } catch { setSandboxOutput(JSON.stringify({ status: "error", message: "Invalid JSON format in Request Payload." diff --git a/Frontend/src/pages/AdminSignup.jsx b/Frontend/src/pages/AdminSignup.jsx index d03cdaf4..fb6bc6b1 100644 --- a/Frontend/src/pages/AdminSignup.jsx +++ b/Frontend/src/pages/AdminSignup.jsx @@ -448,15 +448,12 @@ function AdminSignup() { {showPassword ? : } - {/* Strength Meter */} {formData.password && (
Strength: {getStrengthText()} {passwordStrength}%
- )} - {formData.password && (
-
+ )}
-

-
- -
- Back to Home - + {/* Header */}
diff --git a/Frontend/src/pages/Signup.jsx b/Frontend/src/pages/Signup.jsx index ca6cb3f5..71861b57 100644 --- a/Frontend/src/pages/Signup.jsx +++ b/Frontend/src/pages/Signup.jsx @@ -27,7 +27,7 @@ function Signup() { const dropdownRef = useRef(null); const navigate = useNavigate(); - const { signup, user, profile } = useAuthStore(); + const { signup, loginWithGoogle, loading, user, profile } = useAuthStore(); const passwordRules = { minLength: 6 }; const passwordChecks = getPasswordValidation(password, passwordRules); const passwordWarning = getPasswordValidationMessage(passwordChecks, passwordRules); @@ -108,15 +108,7 @@ function Signup() { e.preventDefault(); setError(""); - // Password complexity validator โ€” mirrors Supabase's policy - const validatePassword = (pw) => { - if (pw.length < 8) return 'Password must be at least 8 characters long.'; - if (!/[a-z]/.test(pw)) return 'Password must contain at least one lowercase letter (a-z).'; - if (!/[A-Z]/.test(pw)) return 'Password must contain at least one uppercase letter (A-Z).'; - if (!/[0-9]/.test(pw)) return 'Password must contain at least one number (0-9).'; - if (!/[^A-Za-z0-9]/.test(pw)) return 'Password must contain at least one special character.'; - return null; - }; + if (!email || !password || !confirmPassword || !fullName) { setError("All fields are required."); diff --git a/Frontend/src/user/pages/AIProcessing.jsx b/Frontend/src/user/pages/AIProcessing.jsx index da1b7064..05c83d79 100644 --- a/Frontend/src/user/pages/AIProcessing.jsx +++ b/Frontend/src/user/pages/AIProcessing.jsx @@ -24,7 +24,7 @@ const steps = [ const AIProcessing = () => { const navigate = useNavigate(); const location = useLocation(); - const { text, image_text, image_base64, template_id, template_used, user_modified, ticket_title, original_text, original_language, source } = location.state || {}; + const { text, image_text, image_base64, template_id, template_used, user_modified, ticket_title, original_text, original_language } = location.state || {}; const setAITicket = useTicketStore((state) => state.setAITicket); const { settings } = useAdminStore(); const { user, profile } = useAuthStore(); @@ -43,7 +43,7 @@ const AIProcessing = () => { hasCalledAPI.current = true; const analyzeTicket = async () => { - console.log("[AIProcessing] Starting analysis for:", text); + let uploadedImageUrl = null; try { @@ -52,7 +52,6 @@ const AIProcessing = () => { // โ”€โ”€ Upload Image if present โ”€โ”€ - let uploadedImageUrl = null; if (image_base64) { diff --git a/backend/main.py b/backend/main.py index 01e50b1e..39341b10 100644 --- a/backend/main.py +++ b/backend/main.py @@ -77,6 +77,7 @@ def _startup_fatal(message: str) -> None: from backend.services.onnx_service import onnx_classifier from backend.services.ner_service import NERService from backend.services.duplicate_service import DuplicateService +from backend.services.incident_service import IncidentService from backend.services.semantic_duplicate_service import SemanticDuplicateService from backend.services.rag_service import RagService from backend.services.spam_service import SpamService From 27ff30d976d1b2408a366cf9d86da5b13c458019 Mon Sep 17 00:00:00 2001 From: Harshita Nagpal Date: Sat, 30 May 2026 01:32:22 +0530 Subject: [PATCH 4/5] fix(backend): gracefully handle optional sentence_transformers dependency in incident service --- backend/services/incident_service.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/services/incident_service.py b/backend/services/incident_service.py index e5bbce89..1ef197ae 100644 --- a/backend/services/incident_service.py +++ b/backend/services/incident_service.py @@ -12,7 +12,12 @@ import os import time import uuid -from sentence_transformers import util +try: + from sentence_transformers import util + _HAS_SENTENCE = True +except ImportError: + util = None + _HAS_SENTENCE = False # Defaults are tunable via env without code changes. From a8ad41944ea2e9f386054d72fe8bd0e38eb51724 Mon Sep 17 00:00:00 2001 From: Harshita Nagpal Date: Sat, 30 May 2026 23:06:48 +0530 Subject: [PATCH 5/5] fix(tests): resolve duplicate_service vectorized test suite mismatch and test pollution --- backend/tests/test_duplicate_service.py | 68 ++++++++++++++----------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/backend/tests/test_duplicate_service.py b/backend/tests/test_duplicate_service.py index c1f91e4c..30fa984d 100644 --- a/backend/tests/test_duplicate_service.py +++ b/backend/tests/test_duplicate_service.py @@ -29,8 +29,14 @@ _mock_st.util = MagicMock() sys.modules.setdefault("sentence_transformers", _mock_st) -# Ensure the backend package is importable -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..")) +# Remove the stub installed by conftest.py to force importing the real module with our mocks +if "backend.services.duplicate_service" in sys.modules: + del sys.modules["backend.services.duplicate_service"] + +# Mock torch globally for checking duplicate vectorized operations +_mock_torch = MagicMock() +_mock_torch.max.side_effect = lambda sim, dim: (sim, MagicMock(item=MagicMock(return_value=0))) +sys.modules["torch"] = _mock_torch from backend.services import duplicate_service as dup_mod from backend.services.duplicate_service import DuplicateService, SIMILARITY_THRESHOLD @@ -204,6 +210,11 @@ def test_custom_threshold_matching(self): assert result["is_duplicate"] is True assert result["duplicate_ticket_id"] == "T-4" + def teardown_method(self): + """Prevent test pollution by resetting common shared mocks.""" + _mock_st.util.cos_sim.side_effect = None + _mock_st.util.cos_sim.return_value = MagicMock() + def test_picks_best_match_among_multiple_tickets(self): """Should return the ticket with the highest similarity score.""" mock_tensor = MagicMock() @@ -216,22 +227,23 @@ def test_picks_best_match_among_multiple_tickets(self): ) svc.model.encode.return_value = mock_tensor - # Return different scores for each comparison - scores = [0.50, 0.95, 0.72] - call_count = {"n": 0} + # Vectorized call: cos_sim returns a single matrix of scores + cosine_result = MagicMock() + _mock_st.util.cos_sim.return_value = cosine_result - def fake_cos_sim(a, b): - result = MagicMock() - result.item.return_value = scores[call_count["n"]] - call_count["n"] += 1 - return result + # Mock torch.max specifically for this test to return score 0.95 at index 1 (T-20) + mock_torch = MagicMock() + mock_score_tensor = MagicMock() + mock_score_tensor.item.return_value = 0.95 + mock_index_tensor = MagicMock() + mock_index_tensor.item.return_value = 1 # Index 1 corresponds to T-20 + mock_torch.max.return_value = (mock_score_tensor, mock_index_tensor) - _mock_st.util.cos_sim.side_effect = fake_cos_sim - - result = svc.check_duplicate("test query") - assert result["is_duplicate"] is True - assert result["duplicate_ticket_id"] == "T-20" - assert result["similarity"] == 0.95 + with patch.dict(sys.modules, {"torch": mock_torch}): + result = svc.check_duplicate("test query") + assert result["is_duplicate"] is True + assert result["duplicate_ticket_id"] == "T-20" + assert result["similarity"] == 0.95 def test_threshold_boundary_exactly_at(self): """Score exactly equal to threshold should count as duplicate.""" @@ -266,19 +278,14 @@ def test_threshold_boundary_just_below(self): def test_check_duplicate_vectorized_matches_best(self): """Should correctly find duplicate when tickets exist in store.""" mock_torch = MagicMock() - sys.modules['torch'] = mock_torch - - self.service._loaded = True - self.service._load_failed = False - - self.service.model = MagicMock() - self.service.model.encode.return_value = MagicMock() - - self.service._tickets = [ - ("T-1", MagicMock(), "orthogonal"), - ("T-2", MagicMock(), "identical"), - ("T-3", MagicMock(), "close") - ] + svc = _make_service( + tickets=[ + ("T-1", MagicMock(), "orthogonal"), + ("T-2", MagicMock(), "identical"), + ("T-3", MagicMock(), "close") + ] + ) + svc.model.encode.return_value = MagicMock() mock_score_tensor = MagicMock() mock_score_tensor.item.return_value = 0.95 @@ -288,7 +295,8 @@ def test_check_duplicate_vectorized_matches_best(self): mock_torch.max.return_value = (mock_score_tensor, mock_index_tensor) - result = self.service.check_duplicate("identical text") + with patch.dict(sys.modules, {"torch": mock_torch}): + result = svc.check_duplicate("identical text") assert result["is_duplicate"] is True assert result["duplicate_ticket_id"] == "T-2"