From 1893c4f38b7571a88e8e2502f2d4e2bbaea0122f Mon Sep 17 00:00:00 2001 From: Daniel Bedeleanu Date: Wed, 15 Apr 2026 15:20:45 +0300 Subject: [PATCH] modificari mari --- ...modern-css-and-responsive-design-expert.md | 6 + .claude/settings.local.json | 3 +- backend/ai_vision.py | 14 +- backend/config_manager.py | 90 ++ backend/main.py | 3 +- backend/routers/admin_db.py | 123 ++ frontend/app/admin/page.tsx | 1272 +++++++++-------- frontend/app/inventory/page.tsx | 30 +- frontend/app/logs/page.tsx | 306 ++-- frontend/components/BottomNav.tsx | 4 +- frontend/components/IdentityCheckOverlay.tsx | 114 +- frontend/components/Scanner.tsx | 60 +- frontend/components/StatCard.tsx | 8 +- frontend/lib/api.ts | 16 + inventory.env | 3 + 15 files changed, 1154 insertions(+), 898 deletions(-) create mode 100644 .agents/rules/modern-css-and-responsive-design-expert.md create mode 100644 backend/config_manager.py diff --git a/.agents/rules/modern-css-and-responsive-design-expert.md b/.agents/rules/modern-css-and-responsive-design-expert.md new file mode 100644 index 00000000..513a1d3a --- /dev/null +++ b/.agents/rules/modern-css-and-responsive-design-expert.md @@ -0,0 +1,6 @@ +--- +trigger: always_on +glob: +description: +--- + diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 5079f9f5..4b68f1e3 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -33,7 +33,8 @@ "Bash(bash *)", "Bash(npm run *)", "Bash(npm test *)", - "Bash(python3 *)" + "Bash(python3 *)", + "Bash(ls -lh aInventory-PROD-v*.zip)" ] } } diff --git a/backend/ai_vision.py b/backend/ai_vision.py index 2822d78d..c24f547c 100644 --- a/backend/ai_vision.py +++ b/backend/ai_vision.py @@ -73,8 +73,18 @@ def extract_label_info(image_bytes: bytes, mode: str = "item"): # 3. Final fallback to hardcoded default prompt = "Extract technical specs. Return JSON with name, category, description, connector, size, color, part_number, ocr_text, quantity." - # 1. Try Gemini - result = gemini.extract(image_bytes, prompt) + # 0. Get Active Provider from DB + provider_setting = db.query(models.SystemSetting).filter(models.SystemSetting.key == "ai_provider").first() + active_provider = provider_setting.value if provider_setting else "gemini" + + # 1. Execute extraction based on selection + result = None + if active_provider == "claude": + print(f"📡 Using Anthropic Claude for extraction...") + result = claude.extract(image_bytes, prompt) + else: + print(f"📡 Using Google Gemini for extraction...") + result = gemini.extract(image_bytes, prompt) if result: # Check if AI returned a list of items or a singular object diff --git a/backend/config_manager.py b/backend/config_manager.py new file mode 100644 index 00000000..aef2b0de --- /dev/null +++ b/backend/config_manager.py @@ -0,0 +1,90 @@ +import os +from dotenv import load_dotenv + +class ConfigManager: + """Safely manages multi-line .env files without corrupting other content.""" + + @staticmethod + def get_root_env_path(): + # backend/config_manager.py -> backend/ -> / + base_dir = os.path.dirname(os.path.abspath(__file__)) + project_root = os.path.dirname(base_dir) + return os.path.join(project_root, "inventory.env") + + @staticmethod + def update_keys(updates: dict): + """ + Updates specific keys in inventory.env. + Preserves comments and order where possible. + Appends new keys at the end if not found. + """ + env_path = ConfigManager.get_root_env_path() + + if not os.path.exists(env_path): + # Create a basic file if it doesn't exist (unlikely in this project) + with open(env_path, 'w', encoding='utf-8') as f: + f.write("# TFM aInventory — Generated Configuration\n") + + with open(env_path, 'r', encoding='utf-8') as f: + lines = f.readlines() + + new_lines = [] + keys_to_process = set(updates.keys()) + processed_keys = set() + + for line in lines: + trimmed = line.strip() + # Skip empty lines or comments when matching + if not trimmed or trimmed.startswith('#'): + new_lines.append(line) + continue + + # Check if this line is a key assignment we want to update + found_match = False + for key in keys_to_process: + if trimmed.startswith(f"{key}="): + new_lines.append(f"{key}={updates[key]}\n") + processed_keys.add(key) + found_match = True + break + + if not found_match: + new_lines.append(line) + + # Append keys that weren't found in the file + missing_keys = keys_to_process - processed_keys + if missing_keys: + if new_lines and not new_lines[-1].endswith('\n'): + new_lines.append('\n') + if new_lines and not new_lines[-1].strip() == '': + new_lines.append('\n') + + new_lines.append("# --- Automatically Added Keys ---\n") + for key in missing_keys: + new_lines.append(f"{key}={updates[key]}\n") + + with open(env_path, 'w', encoding='utf-8') as f: + f.writelines(new_lines) + + # Force reload environment variables for the current process + load_dotenv(env_path, override=True) + return True + + @staticmethod + def get_masked_key(key_name: str): + """Returns a masked version of the environment variable.""" + val = os.environ.get(key_name) + if not val: + return None + + # Determine prefix based on key type + prefix = "" + if key_name == "GEMINI_API_KEY": + prefix = "G-" + elif key_name == "CLAUDE_API_KEY": + prefix = "sk-" + + if len(val) <= 8: + return f"{prefix}****" + + return f"{prefix}****{val[-4:]}" diff --git a/backend/main.py b/backend/main.py index 9429d69a..ce27e27a 100644 --- a/backend/main.py +++ b/backend/main.py @@ -145,7 +145,8 @@ def startup_event(): defaults = { "backup_retention_count": "10", "backup_schedule_hour": "3", - "backup_schedule_freq_days": "1" + "backup_schedule_freq_days": "1", + "ai_provider": "gemini" } for key, val in defaults.items(): if not db.query(models.SystemSetting).filter(models.SystemSetting.key == key).first(): diff --git a/backend/routers/admin_db.py b/backend/routers/admin_db.py index b510ffa8..56405379 100644 --- a/backend/routers/admin_db.py +++ b/backend/routers/admin_db.py @@ -189,3 +189,126 @@ def update_ai_prompt( db.commit() return {"status": "success", "file_updated": os.path.exists(PROMPT_FILE_PATH)} + +from ..config_manager import ConfigManager + +@router.get("/settings/ai") +def get_ai_config( + db: Session = Depends(get_db), + current_admin: auth.TokenData = Depends(auth.get_current_admin) +): + """Check AI provider status and active provider.""" + gemini_key = os.environ.get("GEMINI_API_KEY") + claude_key = os.environ.get("CLAUDE_API_KEY") + + # Get active provider from DB + provider_setting = db.query(models.SystemSetting).filter(models.SystemSetting.key == "ai_provider").first() + active_provider = provider_setting.value if provider_setting else "gemini" + + return { + "active_provider": active_provider, + "providers": [ + { + "id": "gemini", + "name": "Google Gemini 2.0", + "configured": bool(gemini_key), + "active": active_provider == "gemini", + "masked_key": ConfigManager.get_masked_key("GEMINI_API_KEY") + }, + { + "id": "claude", + "name": "Anthropic Claude 3.5", + "configured": bool(claude_key), + "active": active_provider == "claude", + "masked_key": ConfigManager.get_masked_key("CLAUDE_API_KEY") + } + ] + } + +@router.post("/settings/ai-keys") +def update_ai_keys( + payload: dict, + current_admin: auth.TokenData = Depends(auth.get_current_admin) +): + """Update AI API keys in inventory.env.""" + gemini_key = payload.get("gemini_api_key") + claude_key = payload.get("claude_api_key") + + updates = {} + if gemini_key: + updates["GEMINI_API_KEY"] = gemini_key + if claude_key: + updates["CLAUDE_API_KEY"] = claude_key + + if not updates: + return {"status": "no_change"} + + ConfigManager.update_keys(updates) + return { + "status": "success", + "gemini_configured": bool(os.environ.get("GEMINI_API_KEY")), + "claude_configured": bool(os.environ.get("CLAUDE_API_KEY")) + } + +@router.post("/settings/test-ai-key") +def test_ai_key( + payload: dict, + current_admin: auth.TokenData = Depends(auth.get_current_admin) +): + """Test AI API key connectivity.""" + provider = payload.get("provider") + key = payload.get("key") + + if not provider or provider not in ["gemini", "claude"]: + raise HTTPException(status_code=400, detail="Invalid provider") + + # If key is masked or empty, use the one from environment + if not key or "****" in key: + key = os.environ.get("GEMINI_API_KEY" if provider == "gemini" else "CLAUDE_API_KEY") + + if not key: + raise HTTPException(status_code=400, detail="No API key provided or configured") + + try: + if provider == "gemini": + from google import genai + client = genai.Client(api_key=key, http_options={'api_version': 'v1beta'}) + # Listing models is the standard way to check key validity + client.models.list() + return {"status": "success", "message": "Google Gemini API connection verified!"} + + elif provider == "claude": + import anthropic + client = anthropic.Anthropic(api_key=key) + # Claude also supports model listing for key verification + client.models.list(limit=1) + return {"status": "success", "message": "Anthropic Claude API connection verified!"} + + except Exception as e: + error_msg = str(e) + if "API_KEY_INVALID" in error_msg or "invalid_api_key" in error_msg: + error_msg = "The API key provided is invalid." + elif "PERMISSION_DENIED" in error_msg: + error_msg = "Permission denied. Check if the key has correct permissions." + + raise HTTPException(status_code=400, detail=f"{provider.capitalize()} Test Failed: {error_msg}") + +@router.post("/settings/ai") +def update_ai_provider( + payload: dict, + db: Session = Depends(get_db), + current_admin: auth.TokenData = Depends(auth.get_current_admin) +): + """Update the active AI provider.""" + provider = payload.get("provider") + if provider not in ["gemini", "claude"]: + raise HTTPException(status_code=400, detail="Invalid provider. Must be 'gemini' or 'claude'.") + + existing = db.query(models.SystemSetting).filter(models.SystemSetting.key == "ai_provider").first() + if existing: + existing.value = provider + else: + db.add(models.SystemSetting(key="ai_provider", value=provider)) + + db.commit() + return {"status": "success", "active_provider": provider} diff --git a/frontend/app/admin/page.tsx b/frontend/app/admin/page.tsx index 8428541a..c3114f7e 100644 --- a/frontend/app/admin/page.tsx +++ b/frontend/app/admin/page.tsx @@ -30,7 +30,11 @@ import { Database, Layers, X, - ChevronDown + ChevronDown, + Power, + Lock, + Cpu, + Zap } from 'lucide-react'; import { toast } from 'react-hot-toast'; import { cn } from '@/lib/utils'; @@ -43,10 +47,10 @@ export default function AdminPage() { // LDAP Config State const [ldapConfig, setLdapConfig] = useState({ ldap_enabled: false, - server_uri: 'ldap://192.168.84.107:3890', - base_dn: 'dc=example,dc=com', - user_template: 'cn={username},ou=people,dc=example,dc=com', - groups_dn: 'ou=groups', + server_uri: '', + base_dn: '', + user_template: '', + groups_dn: '', use_tls: false, role_mappings: [] }); @@ -66,9 +70,13 @@ export default function AdminPage() { const [isBackingUp, setIsBackingUp] = useState(false); const [isImporting, setIsImporting] = useState(false); - // AI Prompt State + // AI Prompt & Config State const [aiPrompt, setAiPrompt] = useState(""); + const [aiConfig, setAiConfig] = useState(null); + const [aiKeys, setAiKeys] = useState({ gemini: '', claude: '' }); const [isSavingPrompt, setIsSavingPrompt] = useState(false); + const [isSavingKeys, setIsSavingKeys] = useState(false); + const [isTestingKeys, setIsTestingKeys] = useState<{gemini: boolean, claude: boolean}>({gemini: false, claude: false}); useEffect(() => { loadData(); @@ -92,8 +100,12 @@ export default function AdminPage() { setDbStats(s); setDbSettings(st); - const p = await inventoryApi.getAiPrompt(); + const [p, ai] = await Promise.all([ + inventoryApi.getAiPrompt(), + inventoryApi.getAiConfig() + ]); setAiPrompt(p.value); + setAiConfig(ai); } catch (err: any) { console.error(err); toast.error("Failed to load admin data"); @@ -130,6 +142,53 @@ export default function AdminPage() { } }; + const handleUpdateAiProvider = async (provider: string) => { + try { + await inventoryApi.updateAiProvider(provider); + toast.success(`AI provider switched to ${provider}`); + loadData(); + } catch (err: any) { + toast.error("Failed to switch AI provider"); + } + }; + + const handleSaveAiKeys = async () => { + if (!aiKeys.gemini && !aiKeys.claude) { + toast.error("Enter at least one API key to save"); + return; + } + + setIsSavingKeys(true); + try { + await inventoryApi.updateAiKeys({ + gemini_api_key: aiKeys.gemini || undefined, + claude_api_key: aiKeys.claude || undefined + }); + toast.success("AI Configuration keys updated successfully"); + setAiKeys({ gemini: '', claude: '' }); + loadData(); + } catch (err: any) { + toast.error("Failed to update AI keys"); + } finally { + setIsSavingKeys(false); + } + }; + + const handleTestAiKey = async (provider: 'gemini' | 'claude') => { + const keyToTest = provider === 'gemini' ? aiKeys.gemini : aiKeys.claude; + + setIsTestingKeys(prev => ({ ...prev, [provider]: true })); + try { + const res = await inventoryApi.testAiKey(provider, keyToTest); + toast.success(res.message || `${provider.toUpperCase()} connection successful!`); + } catch (err: any) { + const msg = err.response?.data?.detail || `Failed to test ${provider} key`; + toast.error(msg); + } finally { + setIsTestingKeys(prev => ({ ...prev, [provider]: false })); + } + }; + const handleAddCategory = async () => { const name = prompt("New category name:"); if (!name) return; @@ -315,717 +374,666 @@ export default function AdminPage() { }; return ( - -
-
-
- + +
+
+
+
-

System Admin

-

Enterprise Control Center

+

Admin Control

+

System Configuration & Security

+
- {/* User Management Section */} -
-
-
-
- -
-
-

User Accounts

-

Manage local and network synchronized identities

-
-
- -
- -
- {/* Local Users Group */} -
-
-
-

Local Users

-
-
- {loading ? ( -
Loading...
- ) : ( - users.filter(u => (u.origin || 'local') === 'local').map(u => ( -
-
-
- {u.role === 'admin' ? : } -
-
-

{u.username}

- {u.role} -
-
- -
- - {u.username !== 'Admin' && ( - - )} -
-
- )) - )} -
-
- - {/* enterprise Users Group */} - {users.some(u => u.origin === 'ldap') && ( -
-
-
-

Enterprise Users (LDAP)

+
+ {/* Main Controls - System Integrity */} +
+
+
+
+
-
- {users.filter(u => u.origin === 'ldap').map(u => ( -
-
-
- -
-
-

{u.username}

-
- Ldap Profile - {u.role} -
-
-
-
-
- Cached Profile -
- -
-
- ))} +

System Integrity

+
+ +
+
+

Database Health

+
+
+ Operational +
+
+
+

Last Backup

+

{dbStats.backup_count > 0 ? 'Verified' : 'Pending...'}

+
+
+
- )} -
-
+
- {/* Enterprise Integration (LDAP) Section */} -
-
-
-
- + {/* Identity Management */} +
+
+
+
+ +
+

Identity Management

+
+
-
-

Enterprise Integration

-

Configure directory services and single sign-on synchronization

+ +
+ {users.map((user) => ( +
+
+
+ {user.role === 'admin' ? : } +
+
+

{user.username}

+

{user.role}

+
+
+
+ + {user.username !== 'Admin' && ( + + )} +
+
+ ))}
-
- Offline Only - - LDAP Active -
-
+
+ + {/* Right Column - LDAP & AI */} +
+
+
+
+
+ +
+

Enterprise LDAP

+
+
+ +
+
+
+ +
+
+

LDAP Authorization

+

+ {ldapConfig.ldap_enabled ? "External Directory Active" : "Internal Authentication Only"} +

+
+
+ +
-
-
-
- -
- - setLdapConfig({ ...ldapConfig, server_uri: e.target.value })} - className="w-full bg-slate-950 border border-slate-800 rounded-2xl py-3 pl-10 pr-4 text-sm text-white outline-none focus:border-indigo-500/30 transition-all font-mono" - /> -
-
-
- +
+ +
+ setLdapConfig({...ldapConfig, server_uri: e.target.value})} + className="w-full bg-slate-950/80 border border-slate-800 rounded-2xl py-2.5 pl-10 pr-4 text-xs font-bold text-white outline-none focus:border-indigo-500/50 transition-all font-mono" + /> +
+
+
+ +
+ + setLdapConfig({ ...ldapConfig, base_dn: e.target.value })} - className="w-full bg-slate-950 border border-slate-800 rounded-2xl py-3 px-4 text-sm text-white outline-none focus:border-indigo-500/30 transition-all font-mono" + onChange={(e) => setLdapConfig({...ldapConfig, base_dn: e.target.value})} + className="w-full bg-slate-950/80 border border-slate-800 rounded-2xl py-2.5 pl-10 pr-4 text-xs font-bold text-white outline-none focus:border-indigo-500/50 transition-all font-mono" /> -
-
- -
- - setLdapConfig({ ...ldapConfig, user_template: e.target.value })} - className="w-full bg-slate-950 border border-slate-800 rounded-2xl py-3 px-4 text-sm text-white outline-none focus:border-indigo-500/30 transition-all font-mono" - /> -
- -
- - setLdapConfig({ ...ldapConfig, groups_dn: e.target.value })} - className="w-full bg-slate-950 border border-slate-800 rounded-2xl py-3 px-4 text-sm text-white outline-none focus:border-indigo-500/30 transition-all font-mono" - /> +
+
-
- +
+ +
+ m.role === 'admin')?.group || ''} - onChange={(e) => { - const newMappings = [...(ldapConfig.role_mappings || [])]; - const idx = newMappings.findIndex((m: any) => m.role === 'admin'); - if (idx >= 0) newMappings[idx] = { ...newMappings[idx], group: e.target.value }; - else newMappings.push({ role: 'admin', group: e.target.value }); - setLdapConfig({ ...ldapConfig, role_mappings: newMappings }); - }} - className="w-full bg-slate-950 border border-slate-800 rounded-2xl py-3 px-4 text-sm text-white outline-none focus:border-indigo-500/30 transition-all font-mono" + type="text" + placeholder="uid={username},ou=people..." + value={ldapConfig.user_template} + onChange={(e) => setLdapConfig({...ldapConfig, user_template: e.target.value})} + className="w-full bg-slate-950/80 border border-slate-800 rounded-2xl py-2.5 pl-10 pr-4 text-xs font-bold text-white outline-none focus:border-indigo-500/50 transition-all font-mono" /> -
-
- +
+
+
+ +
+ m.role === 'user')?.group || ''} - onChange={(e) => { - const newMappings = [...(ldapConfig.role_mappings || [])]; - const idx = newMappings.findIndex((m: any) => m.role === 'user'); - if (idx >= 0) newMappings[idx] = { ...newMappings[idx], group: e.target.value }; - else newMappings.push({ role: 'user', group: e.target.value }); - setLdapConfig({ ...ldapConfig, role_mappings: newMappings }); - }} - className="w-full bg-slate-950 border border-slate-800 rounded-2xl py-3 px-4 text-sm text-white outline-none focus:border-indigo-500/30 transition-all font-mono" + type="text" + placeholder="ou=groups" + value={ldapConfig.groups_dn} + onChange={(e) => setLdapConfig({...ldapConfig, groups_dn: e.target.value})} + className="w-full bg-slate-950/80 border border-slate-800 rounded-2xl py-2.5 pl-10 pr-4 text-xs font-bold text-white outline-none focus:border-indigo-500/50 transition-all font-mono" /> -
+
+
-
-
-
-
- -

Synchronization Shield

+
+
+
+
-

- Enabling LDAP will allow users from your network domain to log in using their enterprise credentials. - Local account passwords will still function as a fail-safe backup. -

-
-
- - LDAPS / TLS Encryption -
- +
+

Use TLS

+

+ {ldapConfig.use_tls ? "Encrypted Channel" : "Standard Link"} +

- - {ldapConfig.use_tls && ( -
-
- - Ignore Certificate Validation (Self-signed) -
- -
+
+
+ > +
+ +
-
- - -
+ className="flex-1 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl py-3 text-xs font-black shadow-xl shadow-indigo-600/10 transition-all active:scale-95 disabled:opacity-50 flex items-center justify-center gap-2 border border-indigo-500/30" + > + {testingLdap ? : } + Test Connection + + +
+
+ +
+
+ + {/* AI Intelligence Section - Full Width */} +
+
+
+
+ +
+

AI Intelligence

-
+ + {/* [NEW] AI Provider Selection */} +
+ {aiConfig?.providers?.map((p: any) => ( + + ))} +
- {/* AI Configuration Section */} -
-
-
-
- + {/* [NEW] AI Key Configuration */} +
+
+
+
+

Provider Access Keys

-
-

AI Vision Intelligence

-

Configure extraction prompts and heuristic models

+ +
+ +
+
+ +
+ setAiKeys({...aiKeys, gemini: e.target.value})} + placeholder={aiConfig?.providers?.find((p: any) => p.id === 'gemini')?.masked_key || "Enter Gemini Key..."} + className="flex-1 bg-slate-950/80 border border-slate-800 rounded-xl py-3 px-4 text-xs text-white outline-none focus:border-indigo-500/50 transition-all placeholder:text-slate-600" + /> + +
+
+
+ +
+ setAiKeys({...aiKeys, claude: e.target.value})} + placeholder={aiConfig?.providers?.find((p: any) => p.id === 'claude')?.masked_key || "Enter Claude Key..."} + className="flex-1 bg-slate-950/80 border border-slate-800 rounded-xl py-3 px-4 text-xs text-white outline-none focus:border-indigo-500/50 transition-all placeholder:text-slate-600" + /> + +
-
-
- - -
-