ciyidogan commited on
Commit
848cde1
·
verified ·
1 Parent(s): ad9b610

Update admin_routes.py

Browse files
Files changed (1) hide show
  1. admin_routes.py +111 -720
admin_routes.py CHANGED
@@ -21,6 +21,8 @@ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
21
  from pydantic import BaseModel, Field
22
 
23
  from utils import log
 
 
24
 
25
  # ===================== JWT Config =====================
26
  def get_jwt_config():
@@ -146,18 +148,6 @@ class APIUpdate(BaseModel):
146
  class TestRequest(BaseModel):
147
  test_type: str # "all", "ui", "backend", "integration", "spark"
148
 
149
- class EnvironmentUpdate(BaseModel):
150
- work_mode: str
151
- cloud_token: Optional[str] = None
152
- spark_endpoint: str
153
- internal_prompt: Optional[str] = None
154
- tts_engine: str = "no_tts"
155
- tts_engine_api_key: Optional[str] = None
156
- tts_settings: Optional[Dict[str, Any]] = None
157
- stt_engine: str = "no_stt"
158
- stt_engine_api_key: Optional[str] = None
159
- stt_settings: Optional[Dict[str, Any]] = None
160
-
161
  class TTSRequest(BaseModel):
162
  text: str
163
  voice_id: Optional[str] = None
@@ -186,6 +176,9 @@ def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security))
186
  raise HTTPException(status_code=401, detail="Token expired")
187
  except jwt.InvalidTokenError: # Bu genel JWT hatalarını yakalar
188
  raise HTTPException(status_code=401, detail="Invalid token")
 
 
 
189
 
190
  def hash_password(password: str, salt: str = None) -> tuple[str, str]:
191
  """Hash password with bcrypt.
@@ -214,64 +207,17 @@ def verify_password(password: str, hashed: str, salt: str = None) -> bool:
214
  log(f"Password verification error: {e}")
215
  return False
216
 
217
- def load_config():
218
- """Load service_config.jsonc"""
219
- config_path = Path("service_config.jsonc")
220
- if not config_path.exists():
221
- return {"config": {}, "projects": [], "apis": []}
222
-
223
- with open(config_path, 'r', encoding='utf-8') as f:
224
- content = f.read()
225
- # Remove comments for JSON parsing
226
- lines = []
227
- for line in content.split('\n'):
228
- stripped = line.strip()
229
- if not stripped.startswith('//'):
230
- lines.append(line)
231
- clean_content = '\n'.join(lines)
232
- return json.loads(clean_content)
233
-
234
- def save_config(config: dict):
235
- """Save config back to service_config.jsonc"""
236
- with open("service_config.jsonc", 'w', encoding='utf-8') as f:
237
- json.dump(config, f, indent=2, ensure_ascii=False)
238
-
239
  def get_timestamp():
240
  """Get current timestamp in ISO format with milliseconds"""
241
  return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
242
 
243
- def add_activity_log(config: dict, username: str, action: str,
244
- entity_type: str, entity_id: Any, entity_name: str,
245
- details: str = ""):
246
- """Add activity log entry"""
247
- if "activity_log" not in config:
248
- config["activity_log"] = []
249
-
250
- # Get next ID
251
- log_id = max([log.get("id", 0) for log in config["activity_log"]], default=0) + 1
252
-
253
- config["activity_log"].append({
254
- "id": log_id,
255
- "timestamp": get_timestamp(),
256
- "user": username,
257
- "action": action,
258
- "entity_type": entity_type,
259
- "entity_id": entity_id,
260
- "entity_name": entity_name,
261
- "details": details
262
- })
263
-
264
- # Keep only last 1000 entries
265
- if len(config["activity_log"]) > 1000:
266
- config["activity_log"] = config["activity_log"][-1000:]
267
-
268
  async def _spark_project_control(action: str, project_name: str, username: str):
269
  """Common function for Spark project control"""
270
  if not project_name:
271
  raise HTTPException(status_code=400, detail="project_name is required")
272
 
273
- config = load_config()
274
- spark_endpoint = config.get("config", {}).get("spark_endpoint", "").rstrip("/")
275
  spark_token = _get_spark_token()
276
 
277
  if not spark_endpoint:
@@ -306,8 +252,8 @@ async def _spark_project_control(action: str, project_name: str, username: str):
306
 
307
  def _get_spark_token() -> Optional[str]:
308
  """Get Spark token based on work_mode"""
309
- config = load_config()
310
- work_mode = config.get("config", {}).get("work_mode", "on-premise")
311
 
312
  if work_mode in ("hfcloud", "cloud"):
313
  # Cloud mode - use HuggingFace Secrets
@@ -339,7 +285,6 @@ async def notify_spark_manual(project: dict, version: dict, global_config: dict)
339
 
340
  # Decrypt token if needed
341
  if cloud_token and cloud_token.startswith("enc:"):
342
- from encryption_utils import decrypt
343
  cloud_token = decrypt(cloud_token)
344
 
345
  payload = {
@@ -371,16 +316,16 @@ async def notify_spark_manual(project: dict, version: dict, global_config: dict)
371
  @router.post("/login", response_model=LoginResponse)
372
  async def login(request: LoginRequest):
373
  """Authenticate user and return JWT token"""
374
- config = load_config()
375
- users = config.get("config", {}).get("users", [])
376
 
377
  # Find user
378
- user = next((u for u in users if u["username"] == request.username), None)
379
  if not user:
380
  raise HTTPException(status_code=401, detail="Invalid credentials")
381
 
382
  # Verify password
383
- if not verify_password(request.password, user["password_hash"], user.get("salt")):
384
  raise HTTPException(status_code=401, detail="Invalid credentials")
385
 
386
  # Generate JWT token
@@ -402,25 +347,23 @@ async def change_password(
402
  username: str = Depends(verify_token)
403
  ):
404
  """Change user password"""
405
- config = load_config()
406
- users = config.get("config", {}).get("users", [])
407
 
408
  # Find user
409
- user = next((u for u in users if u["username"] == username), None)
410
  if not user:
411
  raise HTTPException(status_code=404, detail="User not found")
412
 
413
  # Verify current password
414
- if not verify_password(request.current_password, user["password_hash"], user.get("salt")):
415
  raise HTTPException(status_code=401, detail="Current password is incorrect")
416
 
417
  # Hash new password
418
  new_hash, new_salt = hash_password(request.new_password)
419
- user["password_hash"] = new_hash
420
- user["salt"] = new_salt
421
 
422
- # Save config
423
- save_config(config)
424
 
425
  log(f"✅ Password changed for user '{username}'")
426
  return {"success": True}
@@ -457,32 +400,21 @@ async def get_locale_details(
457
  @router.get("/environment")
458
  async def get_environment(username: str = Depends(verify_token)):
459
  """Get environment configuration"""
460
- config = load_config()
461
- env_config = config.get("config", {})
462
 
463
  return {
464
- "work_mode": env_config.get("work_mode", "on-premise"),
465
- "cloud_token": env_config.get("cloud_token", ""),
466
- "spark_endpoint": env_config.get("spark_endpoint", "http://localhost:7861"),
467
- "internal_prompt": env_config.get("internal_prompt", ""),
468
- "tts_engine": env_config.get("tts_engine", "no_tts"),
469
- "tts_engine_api_key": env_config.get("tts_engine_api_key", ""),
470
- "tts_settings": env_config.get("tts_settings", {
471
- "use_ssml": False
472
- }),
473
- "stt_engine": env_config.get("stt_engine", "no_stt"),
474
- "stt_engine_api_key": env_config.get("stt_engine_api_key", ""),
475
- "stt_settings": env_config.get("stt_settings", {
476
- "speech_timeout_ms": 2000,
477
- "noise_reduction_level": 2,
478
- "vad_sensitivity": 0.5,
479
- "language": "tr-TR",
480
- "model": "latest_long",
481
- "use_enhanced": True,
482
- "enable_punctuation": True,
483
- "interim_results": True
484
- }),
485
- "parameter_collection_config": env_config.get("parameter_collection_config", {})
486
  }
487
 
488
  @router.put("/environment")
@@ -491,7 +423,7 @@ async def update_environment(
491
  username: str = Depends(verify_token)
492
  ):
493
  """Update environment configuration"""
494
- config = load_config()
495
 
496
  # Token validation based on mode
497
  if update.work_mode in ("gpt4o", "gpt4o-mini"):
@@ -503,10 +435,6 @@ async def update_environment(
503
  if not update.cloud_token:
504
  raise HTTPException(status_code=400, detail="Cloud token is required for cloud modes")
505
 
506
- # Debug log - gelen değerleri kontrol et
507
- log(f"📥 Received TTS engine: {update.tts_engine}")
508
- log(f"📥 Received TTS key: {'***' + update.tts_engine_api_key[-4:] if update.tts_engine_api_key else 'None'}")
509
-
510
  # TTS/STT validation
511
  if update.tts_engine not in ("no_tts", "elevenlabs", "blaze"):
512
  raise HTTPException(status_code=400, detail="Invalid TTS engine")
@@ -524,52 +452,9 @@ async def update_environment(
524
  if update.work_mode not in ("gpt4o", "gpt4o-mini") and not update.spark_endpoint:
525
  raise HTTPException(status_code=400, detail="Spark endpoint is required for non-GPT modes")
526
 
527
- # Encrypt API keys if needed
528
- from encryption_utils import encrypt
529
 
530
- # TTS key encryption
531
- if update.tts_engine_api_key:
532
- encrypted_tts_key = encrypt(update.tts_engine_api_key)
533
- log(f"🔐 Encrypted TTS key: {encrypted_tts_key[:20]}...")
534
- else:
535
- encrypted_tts_key = ""
536
-
537
- # STT key encryption
538
- if update.stt_engine_api_key:
539
- encrypted_stt_key = encrypt(update.stt_engine_api_key)
540
- else:
541
- encrypted_stt_key = ""
542
-
543
- # Update config
544
- config["config"]["work_mode"] = update.work_mode
545
- config["config"]["cloud_token"] = encrypt(update.cloud_token) if update.cloud_token else ""
546
- config["config"]["spark_endpoint"] = update.spark_endpoint
547
- config["config"]["internal_prompt"] = update.internal_prompt or ""
548
- config["config"]["tts_engine"] = update.tts_engine
549
- config["config"]["tts_engine_api_key"] = encrypted_tts_key
550
- config["config"]["stt_engine"] = update.stt_engine
551
- config["config"]["stt_engine_api_key"] = encrypted_stt_key
552
- config["config"]["last_update_date"] = get_timestamp()
553
- config["config"]["last_update_user"] = username
554
- config["config"]["parameter_collection_config"] = update.parameter_collection_config
555
-
556
- if update.tts_settings:
557
- config["config"]["tts_settings"] = update.tts_settings
558
-
559
- if update.stt_settings:
560
- config["config"]["stt_settings"] = update.stt_settings
561
-
562
- # Add activity log
563
- add_activity_log(config, username, "UPDATE_ENVIRONMENT", "config", None,
564
- "environment", f"Changed to {update.work_mode}, TTS: {update.tts_engine}, STT: {update.stt_engine}")
565
-
566
- # Save config
567
- save_config(config)
568
-
569
- # Reload ConfigProvider to reflect changes
570
- from config_provider import ConfigProvider
571
- ConfigProvider.reload()
572
-
573
  log(f"✅ Environment updated to {update.work_mode} with TTS: {update.tts_engine}, STT: {update.stt_engine} by {username}")
574
  return {"success": True}
575
 
@@ -577,9 +462,8 @@ async def update_environment(
577
  @router.get("/projects/names")
578
  def list_enabled_projects():
579
  """Get list of enabled project names for chat"""
580
- cfg = load_config()
581
- projects = cfg.get("projects", [])
582
- return [p["name"] for p in projects if p.get("enabled", False) and not p.get("deleted", False)]
583
 
584
  @router.get("/projects")
585
  async def list_projects(
@@ -587,14 +471,14 @@ async def list_projects(
587
  username: str = Depends(verify_token)
588
  ):
589
  """List all projects"""
590
- config = load_config()
591
- projects = config.get("projects", [])
592
 
593
  # Filter deleted if needed
594
  if not include_deleted:
595
- projects = [p for p in projects if not p.get("deleted", False)]
596
 
597
- return projects
598
 
599
  @router.get("/projects/{project_id}")
600
  async def get_project(
@@ -602,21 +486,11 @@ async def get_project(
602
  username: str = Depends(verify_token)
603
  ):
604
  """Get single project by ID"""
605
- try:
606
- config = load_config()
607
- projects = config.get("projects", [])
608
-
609
- project = next((p for p in projects if p.get("id") == project_id), None)
610
- if not project or project.get("deleted", False):
611
- raise HTTPException(status_code=404, detail="Project not found")
612
-
613
- return project
614
-
615
- except HTTPException:
616
- raise
617
- except Exception as e:
618
- log(f"Failed to get project: {e}")
619
- raise HTTPException(status_code=500, detail=str(e))
620
 
621
  @router.post("/projects")
622
  async def create_project(
@@ -624,13 +498,6 @@ async def create_project(
624
  username: str = Depends(verify_token)
625
  ):
626
  """Create new project with initial version"""
627
- config = load_config()
628
-
629
- # Validate project name
630
- existing_names = [p["name"].lower() for p in config.get("projects", []) if not p.get("deleted", False)]
631
- if project.name.lower() in existing_names:
632
- raise HTTPException(status_code=400, detail="Project name already exists")
633
-
634
  # Validate supported languages
635
  from locale_manager import LocaleManager
636
 
@@ -644,77 +511,17 @@ async def create_project(
644
  )
645
 
646
  # Check if default language is in supported languages
647
- # NOT: default_language bir name (Türkçe), supported_languages ise code'lar (tr-TR)
648
- # Bu yüzden karşılaştırma yapamayız, sadece supported_languages'ın boş olmamasını kontrol edelim
649
  if not project.supported_languages:
650
  raise HTTPException(
651
  status_code=400,
652
  detail="At least one supported language must be selected"
653
  )
654
 
655
- # Get new project ID
656
- project_id = config.get("project_id_counter", 1)
657
- config["project_id_counter"] = project_id + 1
658
-
659
- # Create project with version
660
- new_project = {
661
- "id": project_id,
662
- "name": project.name,
663
- "caption": project.caption,
664
- "icon": project.icon,
665
- "description": project.description,
666
- "default_language": project.default_language,
667
- "supported_languages": project.supported_languages,
668
- "timezone": project.timezone,
669
- "region": project.region,
670
- "enabled": True,
671
- "deleted": False,
672
- "version_id_counter": 2, # Start from 2 since we create version 1
673
- "last_version_number": 1,
674
- "created_date": get_timestamp(),
675
- "created_by": username,
676
- "last_update_date": get_timestamp(),
677
- "last_update_user": username,
678
- "versions": [
679
- {
680
- "id": 1,
681
- "no": 1,
682
- "caption": "Version 1",
683
- "published": False,
684
- "created_date": get_timestamp(),
685
- "created_by": username,
686
- "last_update_date": get_timestamp(),
687
- "last_update_user": username,
688
- "general_prompt": "",
689
- "llm": {
690
- "repo_id": "Qwen/Qwen2.5-72B-Instruct",
691
- "generation_config": {
692
- "temperature": 0.5,
693
- "max_tokens": 2048,
694
- "top_p": 0.7,
695
- "repetition_penalty": 1.1
696
- },
697
- "use_fine_tune": False,
698
- "fine_tune_zip": ""
699
- },
700
- "intents": []
701
- }
702
- ]
703
- }
704
-
705
- # Add to config
706
- if "projects" not in config:
707
- config["projects"] = []
708
- config["projects"].append(new_project)
709
-
710
- # Add activity log
711
- add_activity_log(config, username, "CREATE_PROJECT", "project", project_id, project.name)
712
-
713
- # Save
714
- save_config(config)
715
 
716
  log(f"✅ Project '{project.name}' created by {username}")
717
- return new_project
718
 
719
  @router.put("/projects/{project_id}")
720
  async def update_project(
@@ -723,85 +530,27 @@ async def update_project(
723
  username: str = Depends(verify_token)
724
  ):
725
  """Update project"""
726
- config = load_config()
727
-
728
- # Find project
729
- project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
730
- if not project:
731
- raise HTTPException(status_code=404, detail="Project not found")
732
 
733
- # Check race condition
734
- if project.get("last_update_date") != update.last_update_date:
735
- raise HTTPException(status_code=409, detail="Project was modified by another user")
736
-
737
- # Update - yeni alanlarla
738
- project["caption"] = update.caption
739
- project["icon"] = update.icon
740
- project["description"] = update.description
741
- project["default_language"] = update.default_language
742
- project["supported_languages"] = update.supported_languages
743
- project["timezone"] = update.timezone
744
- project["region"] = update.region
745
- project["last_update_date"] = get_timestamp()
746
- project["last_update_user"] = username
747
-
748
- # Add activity log
749
- add_activity_log(config, username, "UPDATE_PROJECT", "project", project_id, project["name"])
750
-
751
- # Save
752
- save_config(config)
753
-
754
- log(f"✅ Project '{project['name']}' updated by {username}")
755
- return project
756
 
757
  @router.delete("/projects/{project_id}")
758
  async def delete_project(project_id: int, username: str = Depends(verify_token)):
759
  """Delete project (soft delete)"""
760
- config = load_config()
761
-
762
- # Find project
763
- project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
764
- if not project:
765
- raise HTTPException(status_code=404, detail="Project not found")
766
 
767
- # Soft delete
768
- project["deleted"] = True
769
- project["last_update_date"] = get_timestamp()
770
- project["last_update_user"] = username
771
-
772
- # Add activity log
773
- add_activity_log(config, username, "DELETE_PROJECT", "project", project_id, project["name"])
774
-
775
- # Save
776
- save_config(config)
777
-
778
- log(f"✅ Project '{project['name']}' deleted by {username}")
779
  return {"success": True}
780
 
781
  @router.patch("/projects/{project_id}/toggle")
782
  async def toggle_project(project_id: int, username: str = Depends(verify_token)):
783
  """Toggle project enabled status"""
784
- config = load_config()
785
-
786
- # Find project
787
- project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
788
- if not project:
789
- raise HTTPException(status_code=404, detail="Project not found")
790
-
791
- # Toggle
792
- project["enabled"] = not project.get("enabled", False)
793
- project["last_update_date"] = get_timestamp()
794
- project["last_update_user"] = username
795
-
796
- # Add activity log
797
- action = "ENABLE_PROJECT" if project["enabled"] else "DISABLE_PROJECT"
798
- add_activity_log(config, username, action, "project", project_id, project["name"])
799
 
800
- # Save
801
- save_config(config)
802
-
803
- log(f"✅ Project '{project['name']}' {'enabled' if project['enabled'] else 'disabled'} by {username}")
804
- return {"enabled": project["enabled"]}
805
 
806
  # ===================== Version Endpoints =====================
807
  @router.get("/projects/{project_id}/versions")
@@ -811,20 +560,17 @@ async def list_versions(
811
  username: str = Depends(verify_token)
812
  ):
813
  """List project versions"""
814
- config = load_config()
815
-
816
- # Find project
817
- project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
818
  if not project:
819
  raise HTTPException(status_code=404, detail="Project not found")
820
 
821
- versions = project.get("versions", [])
822
 
823
  # Filter deleted if needed
824
  if not include_deleted:
825
- versions = [v for v in versions if not v.get("deleted", False)]
826
 
827
- return versions
828
 
829
  @router.post("/projects/{project_id}/versions")
830
  async def create_version(
@@ -833,89 +579,10 @@ async def create_version(
833
  username: str = Depends(verify_token)
834
  ):
835
  """Create new version"""
836
- config = load_config()
837
-
838
- # Find project
839
- project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
840
- if not project:
841
- raise HTTPException(status_code=404, detail="Project not found")
842
-
843
- # Get next version ID
844
- version_id = project.get("version_id_counter", 0) + 1
845
- project["version_id_counter"] = version_id
846
-
847
- # Get next version number
848
- existing_versions = [v for v in project.get("versions", []) if not v.get("deleted", False)]
849
- version_no = max([v.get("no", 0) for v in existing_versions], default=0) + 1
850
-
851
- # Create base version
852
- new_version = {
853
- "id": version_id,
854
- "no": version_no,
855
- "caption": version_data.caption,
856
- "description": f"Version {version_no}",
857
- "published": False,
858
- "deleted": False,
859
- "created_date": get_timestamp(),
860
- "created_by": username,
861
- "last_update_date": get_timestamp(),
862
- "last_update_user": username,
863
- "publish_date": None,
864
- "published_by": None
865
- }
866
 
867
- # Copy from source version if specified
868
- if version_data.source_version_id:
869
- source_version = next(
870
- (v for v in project.get("versions", []) if v["id"] == version_data.source_version_id),
871
- None
872
- )
873
- if source_version:
874
- # Copy configuration from source
875
- new_version.update({
876
- "general_prompt": source_version.get("general_prompt", ""),
877
- "llm": source_version.get("llm", {}).copy(),
878
- "intents": [intent.copy() for intent in source_version.get("intents", [])],
879
- "parameters": [param.copy() for param in source_version.get("parameters", [])]
880
- })
881
- else:
882
- # Empty template
883
- new_version.update({
884
- "general_prompt": "",
885
- "llm": {
886
- "repo_id": "",
887
- "generation_config": {
888
- "max_new_tokens": 512,
889
- "temperature": 0.7,
890
- "top_p": 0.95,
891
- "top_k": 50,
892
- "repetition_penalty": 1.1
893
- },
894
- "use_fine_tune": False,
895
- "fine_tune_zip": ""
896
- },
897
- "intents": [],
898
- "parameters": []
899
- })
900
-
901
- # Add to project
902
- if "versions" not in project:
903
- project["versions"] = []
904
- project["versions"].append(new_version)
905
-
906
- # Update project timestamp
907
- project["last_update_date"] = get_timestamp()
908
- project["last_update_user"] = username
909
-
910
- # Add activity log
911
- add_activity_log(config, username, "CREATE_VERSION", "version", version_id,
912
- f"{project['name']} v{version_no}")
913
-
914
- # Save
915
- save_config(config)
916
-
917
- log(f"✅ Version {version_no} created for project '{project['name']}' by {username}")
918
- return new_version
919
 
920
  @router.put("/projects/{project_id}/versions/{version_id}")
921
  async def update_version(
@@ -925,46 +592,10 @@ async def update_version(
925
  username: str = Depends(verify_token)
926
  ):
927
  """Update version"""
928
- config = load_config()
929
-
930
- # Find project and version
931
- project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
932
- if not project:
933
- raise HTTPException(status_code=404, detail="Project not found")
934
-
935
- version = next((v for v in project.get("versions", []) if v["id"] == version_id), None)
936
- if not version:
937
- raise HTTPException(status_code=404, detail="Version not found")
938
-
939
- # Check race condition
940
- if version.get("last_update_date") != update.last_update_date:
941
- raise HTTPException(status_code=409, detail="Version was modified by another user")
942
 
943
- # Cannot update published version
944
- if version.get("published", False):
945
- raise HTTPException(status_code=400, detail="Cannot modify published version")
946
-
947
- # Update version
948
- version["caption"] = update.caption
949
- version["general_prompt"] = update.general_prompt
950
- version["llm"] = update.llm
951
- version["intents"] = [intent.dict() for intent in update.intents]
952
- version["last_update_date"] = get_timestamp()
953
- version["last_update_user"] = username
954
-
955
- # Update project timestamp
956
- project["last_update_date"] = get_timestamp()
957
- project["last_update_user"] = username
958
-
959
- # Add activity log
960
- add_activity_log(config, username, "UPDATE_VERSION", "version", version_id,
961
- f"{project['name']} v{version['no']}")
962
-
963
- # Save
964
- save_config(config)
965
-
966
- log(f"✅ Version {version['no']} updated for project '{project['name']}' by {username}")
967
- return version
968
 
969
  @router.post("/projects/{project_id}/versions/{version_id}/publish")
970
  async def publish_version(
@@ -973,46 +604,19 @@ async def publish_version(
973
  username: str = Depends(verify_token)
974
  ):
975
  """Publish version"""
976
- config = load_config()
977
-
978
- # Find project and version
979
- project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
980
- if not project:
981
- raise HTTPException(status_code=404, detail="Project not found")
982
-
983
- version = next((v for v in project.get("versions", []) if v["id"] == version_id), None)
984
- if not version:
985
- raise HTTPException(status_code=404, detail="Version not found")
986
-
987
- # Unpublish all other versions
988
- for v in project.get("versions", []):
989
- if v["id"] != version_id:
990
- v["published"] = False
991
-
992
- # Publish this version
993
- version["published"] = True
994
- version["publish_date"] = get_timestamp()
995
- version["published_by"] = username
996
- version["last_update_date"] = get_timestamp()
997
- version["last_update_user"] = username
998
-
999
- # Update project timestamp
1000
- project["last_update_date"] = get_timestamp()
1001
- project["last_update_user"] = username
1002
-
1003
- # Add activity log
1004
- add_activity_log(config, username, "PUBLISH_VERSION", "version", version_id,
1005
- f"{project['name']} v{version['no']}")
1006
-
1007
- # Save
1008
- save_config(config)
1009
 
1010
- log(f"✅ Version {version_id} published for project '{project['name']}' by {username}")
1011
 
1012
  # Notify Spark if project is enabled
1013
- if project.get("enabled", False):
1014
  try:
1015
- await notify_spark_manual(project, version, config.get("config", {}))
 
 
 
 
 
1016
  except Exception as e:
1017
  log(f"⚠️ Failed to notify Spark: {e}")
1018
  # Don't fail the publish
@@ -1026,37 +630,9 @@ async def delete_version(
1026
  username: str = Depends(verify_token)
1027
  ):
1028
  """Delete version (soft delete)"""
1029
- config = load_config()
1030
-
1031
- # Find project and version
1032
- project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
1033
- if not project:
1034
- raise HTTPException(status_code=404, detail="Project not found")
1035
-
1036
- version = next((v for v in project.get("versions", []) if v["id"] == version_id), None)
1037
- if not version:
1038
- raise HTTPException(status_code=404, detail="Version not found")
1039
-
1040
- # Cannot delete published version
1041
- if version.get("published", False):
1042
- raise HTTPException(status_code=400, detail="Cannot delete published version")
1043
 
1044
- # Soft delete
1045
- version["deleted"] = True
1046
- version["last_update_date"] = get_timestamp()
1047
- version["last_update_user"] = username
1048
-
1049
- project["last_update_date"] = get_timestamp()
1050
- project["last_update_user"] = username
1051
-
1052
- # Add activity log
1053
- add_activity_log(config, username, "DELETE_VERSION", "version", version_id,
1054
- f"{project['name']} v{version_id}")
1055
-
1056
- # Save
1057
- save_config(config)
1058
-
1059
- log(f"✅ Version {version_id} deleted for project '{project['name']}' by {username}")
1060
  return {"success": True}
1061
 
1062
  @router.post("/validate/regex")
@@ -1095,45 +671,22 @@ async def list_apis(
1095
  username: str = Depends(verify_token)
1096
  ):
1097
  """List all APIs"""
1098
- config = load_config()
1099
- apis = config.get("apis", [])
1100
 
1101
  # Filter deleted if needed
1102
  if not include_deleted:
1103
- apis = [a for a in apis if not a.get("deleted", False)]
1104
 
1105
- return apis
1106
 
1107
  @router.post("/apis")
1108
  async def create_api(api: APICreate, username: str = Depends(verify_token)):
1109
  """Create new API"""
1110
- config = load_config()
1111
-
1112
- # Check duplicate name
1113
- existing = [a for a in config.get("apis", []) if a["name"] == api.name]
1114
- if existing:
1115
- raise HTTPException(status_code=400, detail="API name already exists")
1116
-
1117
- # Create API
1118
- new_api = api.dict()
1119
- new_api["deleted"] = False
1120
- new_api["created_date"] = get_timestamp()
1121
- new_api["created_by"] = username
1122
- new_api["last_update_date"] = get_timestamp()
1123
- new_api["last_update_user"] = username
1124
-
1125
- if "apis" not in config:
1126
- config["apis"] = []
1127
- config["apis"].append(new_api)
1128
-
1129
- # Add activity log
1130
- add_activity_log(config, username, "CREATE_API", "api", api.name, api.name)
1131
-
1132
- # Save
1133
- save_config(config)
1134
 
1135
  log(f"✅ API '{api.name}' created by {username}")
1136
- return new_api
1137
 
1138
  @router.put("/apis/{api_name}")
1139
  async def update_api(
@@ -1142,78 +695,15 @@ async def update_api(
1142
  username: str = Depends(verify_token)
1143
  ):
1144
  """Update API"""
1145
- config = load_config()
1146
-
1147
- # Find API
1148
- api = next((a for a in config.get("apis", []) if a["name"] == api_name), None)
1149
- if not api:
1150
- raise HTTPException(status_code=404, detail="API not found")
1151
-
1152
- # Check race condition
1153
- if api.get("last_update_date") != update.last_update_date:
1154
- raise HTTPException(status_code=409, detail="API was modified by another user")
1155
-
1156
- # Check if API is in use
1157
- for project in config.get("projects", []):
1158
- for version in project.get("versions", []):
1159
- for intent in version.get("intents", []):
1160
- if intent.get("action") == api_name and version.get("published", False):
1161
- raise HTTPException(status_code=400,
1162
- detail=f"API is used in published version of project '{project['name']}'")
1163
-
1164
- # Update
1165
- update_dict = update.dict()
1166
- del update_dict["last_update_date"]
1167
- api.update(update_dict)
1168
- api["last_update_date"] = get_timestamp()
1169
- api["last_update_user"] = username
1170
-
1171
- # Add activity log
1172
- add_activity_log(config, username, "UPDATE_API", "api", api_name, api_name)
1173
-
1174
- # Save
1175
- save_config(config)
1176
 
1177
  log(f"✅ API '{api_name}' updated by {username}")
1178
- return api
1179
 
1180
  @router.delete("/apis/{api_name}")
1181
  async def delete_api(api_name: str, username: str = Depends(verify_token)):
1182
  """Delete API (soft delete)"""
1183
- config = load_config()
1184
-
1185
- # Find API
1186
- api = next((a for a in config.get("apis", []) if a["name"] == api_name), None)
1187
- if not api:
1188
- raise HTTPException(status_code=404, detail="API not found")
1189
-
1190
- # Check if API is in use
1191
- for project in config.get("projects", []):
1192
- # Skip deleted projects
1193
- if project.get("deleted", False):
1194
- continue
1195
-
1196
- for version in project.get("versions", []):
1197
- # Skip deleted versions
1198
- if version.get("deleted", False):
1199
- continue
1200
-
1201
- # Check in intents
1202
- for intent in version.get("intents", []):
1203
- if intent.get("action") == api_name:
1204
- raise HTTPException(status_code=400,
1205
- detail=f"API is used in intent '{intent.get('name', 'unknown')}' in project '{project['name']}' version {version.get('version_number', version.get('id'))}")
1206
-
1207
- # Soft delete
1208
- api["deleted"] = True
1209
- api["last_update_date"] = get_timestamp()
1210
- api["last_update_user"] = username
1211
-
1212
- # Add activity log
1213
- add_activity_log(config, username, "DELETE_API", "api", api_name, api_name)
1214
-
1215
- # Save
1216
- save_config(config)
1217
 
1218
  log(f"✅ API '{api_name}' deleted by {username}")
1219
  return {"success": True}
@@ -1226,21 +716,23 @@ async def spark_startup(request: dict = Body(...), username: str = Depends(verif
1226
  if not project_name:
1227
  raise HTTPException(status_code=400, detail="project_name is required")
1228
 
1229
- config = load_config()
1230
-
1231
- # Find project
1232
- project = next((p for p in config.get("projects", []) if p["name"] == project_name), None)
1233
  if not project:
1234
  raise HTTPException(status_code=404, detail=f"Project not found: {project_name}")
1235
 
1236
  # Find published version
1237
- version = next((v for v in project.get("versions", []) if v.get("published", False)), None)
1238
  if not version:
1239
  raise HTTPException(status_code=400, detail=f"No published version found for project: {project_name}")
1240
 
1241
  # Notify Spark
1242
  try:
1243
- result = await notify_spark_manual(project, version, config.get("config", {}))
 
 
 
 
 
1244
  return {"message": result.get("message", "Spark startup initiated")}
1245
  except Exception as e:
1246
  log(f"❌ Spark startup failed: {e}")
@@ -1249,8 +741,8 @@ async def spark_startup(request: dict = Body(...), username: str = Depends(verif
1249
  @router.get("/spark/projects")
1250
  async def spark_get_projects(username: str = Depends(verify_token)):
1251
  """Get Spark project list"""
1252
- config = load_config()
1253
- spark_endpoint = config.get("config", {}).get("spark_endpoint", "").rstrip("/")
1254
  spark_token = _get_spark_token()
1255
 
1256
  if not spark_endpoint:
@@ -1470,75 +962,10 @@ async def import_project(
1470
  username: str = Depends(verify_token)
1471
  ):
1472
  """Import project from JSON"""
1473
- config = load_config()
1474
-
1475
- # Validate structure
1476
- if "name" not in project_data:
1477
- raise HTTPException(status_code=400, detail="Invalid project data")
1478
-
1479
- # Check duplicate name
1480
- existing = [p for p in config.get("projects", [])
1481
- if p["name"] == project_data["name"] and not p.get("deleted", False)]
1482
- if existing:
1483
- raise HTTPException(status_code=400, detail="Project name already exists")
1484
-
1485
- # Get new project ID
1486
- project_id = config["config"].get("project_id_counter", 0) + 1
1487
- config["config"]["project_id_counter"] = project_id
1488
-
1489
- # Create new project
1490
- new_project = {
1491
- "id": project_id,
1492
- "name": project_data["name"],
1493
- "caption": project_data.get("caption", ""),
1494
- "icon": project_data.get("icon", "folder"),
1495
- "description": project_data.get("description", ""),
1496
- "enabled": False,
1497
- "deleted": False,
1498
- "created_date": get_timestamp(),
1499
- "created_by": username,
1500
- "last_update_date": get_timestamp(),
1501
- "last_update_user": username,
1502
- "version_id_counter": 1,
1503
- "versions": []
1504
- }
1505
-
1506
- # Import versions
1507
- for idx, version_data in enumerate(project_data.get("versions", [])):
1508
- new_version = {
1509
- "id": idx + 1,
1510
- "no": idx + 1,
1511
- "caption": version_data.get("caption", f"Version {idx + 1}"),
1512
- "description": version_data.get("description", ""),
1513
- "published": False,
1514
- "deleted": False,
1515
- "created_date": get_timestamp(),
1516
- "created_by": username,
1517
- "last_update_date": get_timestamp(),
1518
- "last_update_user": username,
1519
- "publish_date": None,
1520
- "published_by": None,
1521
- "general_prompt": version_data.get("general_prompt", ""),
1522
- "llm": version_data.get("llm", {}),
1523
- "intents": version_data.get("intents", []),
1524
- "parameters": version_data.get("parameters", [])
1525
- }
1526
- new_project["versions"].append(new_version)
1527
- new_project["version_id_counter"] = idx + 1
1528
 
1529
- # Add to config
1530
- if "projects" not in config:
1531
- config["projects"] = []
1532
- config["projects"].append(new_project)
1533
-
1534
- # Add activity log
1535
- add_activity_log(config, username, "IMPORT_PROJECT", "project", project_id, new_project["name"])
1536
-
1537
- # Save
1538
- save_config(config)
1539
-
1540
- log(f"✅ Project '{new_project['name']}' imported by {username}")
1541
- return new_project
1542
 
1543
  @router.get("/projects/{project_id}/export")
1544
  async def export_project(
@@ -1546,40 +973,9 @@ async def export_project(
1546
  username: str = Depends(verify_token)
1547
  ):
1548
  """Export project as JSON"""
1549
- config = load_config()
1550
-
1551
- # Find project
1552
- project = next((p for p in config.get("projects", []) if p["id"] == project_id), None)
1553
- if not project:
1554
- raise HTTPException(status_code=404, detail="Project not found")
1555
-
1556
- # Create export data
1557
- export_data = {
1558
- "name": project["name"],
1559
- "caption": project.get("caption", ""),
1560
- "icon": project.get("icon", "folder"),
1561
- "description": project.get("description", ""),
1562
- "versions": []
1563
- }
1564
-
1565
- # Export versions
1566
- for version in project.get("versions", []):
1567
- if not version.get("deleted", False):
1568
- export_version = {
1569
- "caption": version.get("caption", ""),
1570
- "description": version.get("description", ""),
1571
- "general_prompt": version.get("general_prompt", ""),
1572
- "llm": version.get("llm", {}),
1573
- "intents": version.get("intents", []),
1574
- "parameters": version.get("parameters", [])
1575
- }
1576
- export_data["versions"].append(export_version)
1577
-
1578
- # Add activity log
1579
- add_activity_log(config, username, "EXPORT_PROJECT", "project", project_id, project["name"])
1580
- save_config(config)
1581
 
1582
- log(f"✅ Project '{project['name']}' exported by {username}")
1583
  return export_data
1584
 
1585
  # ===================== TTS Endpoints =====================
@@ -1590,9 +986,8 @@ async def generate_tts(
1590
  ):
1591
  """Generate TTS audio from text"""
1592
  try:
1593
- # ConfigProvider'dan reload ederek güncel config'i alalım
1594
- from config_provider import ConfigProvider
1595
- cfg = ConfigProvider.reload()
1596
 
1597
  tts_engine = cfg.global_config.tts_engine
1598
  log(f"🔧 TTS Engine: {tts_engine}")
@@ -1654,23 +1049,19 @@ async def get_activity_log(
1654
  username: str = Depends(verify_token)
1655
  ):
1656
  """Get activity log"""
1657
- config = load_config()
1658
- logs = config.get("activity_log", [])
1659
 
1660
- # Return latest entries (format as paginated response if needed)
1661
- return logs[-limit:]
1662
 
1663
  # ===================== Cleanup Task =====================
1664
  def cleanup_old_logs():
1665
  """Cleanup old activity logs (runs in background)"""
1666
  while True:
1667
  try:
1668
- config = load_config()
1669
- if "activity_log" in config and len(config["activity_log"]) > 5000:
1670
- # Keep only last 1000 entries
1671
- config["activity_log"] = config["activity_log"][-1000:]
1672
- save_config(config)
1673
- log("🧹 Cleaned up old activity logs")
1674
  except Exception as e:
1675
  log(f"Error in cleanup task: {e}")
1676
 
 
21
  from pydantic import BaseModel, Field
22
 
23
  from utils import log
24
+ from config_provider import ConfigProvider
25
+ from encryption_utils import encrypt, decrypt
26
 
27
  # ===================== JWT Config =====================
28
  def get_jwt_config():
 
148
  class TestRequest(BaseModel):
149
  test_type: str # "all", "ui", "backend", "integration", "spark"
150
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  class TTSRequest(BaseModel):
152
  text: str
153
  voice_id: Optional[str] = None
 
176
  raise HTTPException(status_code=401, detail="Token expired")
177
  except jwt.InvalidTokenError: # Bu genel JWT hatalarını yakalar
178
  raise HTTPException(status_code=401, detail="Invalid token")
179
+
180
+ # Utility function to get username for Depends
181
+ get_username = verify_token
182
 
183
  def hash_password(password: str, salt: str = None) -> tuple[str, str]:
184
  """Hash password with bcrypt.
 
207
  log(f"Password verification error: {e}")
208
  return False
209
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  def get_timestamp():
211
  """Get current timestamp in ISO format with milliseconds"""
212
  return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
213
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  async def _spark_project_control(action: str, project_name: str, username: str):
215
  """Common function for Spark project control"""
216
  if not project_name:
217
  raise HTTPException(status_code=400, detail="project_name is required")
218
 
219
+ cfg = ConfigProvider.get()
220
+ spark_endpoint = str(cfg.global_config.spark_endpoint).rstrip("/")
221
  spark_token = _get_spark_token()
222
 
223
  if not spark_endpoint:
 
252
 
253
  def _get_spark_token() -> Optional[str]:
254
  """Get Spark token based on work_mode"""
255
+ cfg = ConfigProvider.get()
256
+ work_mode = cfg.global_config.work_mode
257
 
258
  if work_mode in ("hfcloud", "cloud"):
259
  # Cloud mode - use HuggingFace Secrets
 
285
 
286
  # Decrypt token if needed
287
  if cloud_token and cloud_token.startswith("enc:"):
 
288
  cloud_token = decrypt(cloud_token)
289
 
290
  payload = {
 
316
  @router.post("/login", response_model=LoginResponse)
317
  async def login(request: LoginRequest):
318
  """Authenticate user and return JWT token"""
319
+ cfg = ConfigProvider.get()
320
+ users = cfg.global_config.users
321
 
322
  # Find user
323
+ user = next((u for u in users if u.username == request.username), None)
324
  if not user:
325
  raise HTTPException(status_code=401, detail="Invalid credentials")
326
 
327
  # Verify password
328
+ if not verify_password(request.password, user.password_hash, user.salt):
329
  raise HTTPException(status_code=401, detail="Invalid credentials")
330
 
331
  # Generate JWT token
 
347
  username: str = Depends(verify_token)
348
  ):
349
  """Change user password"""
350
+ cfg = ConfigProvider.get()
351
+ users = cfg.global_config.users
352
 
353
  # Find user
354
+ user = next((u for u in users if u.username == username), None)
355
  if not user:
356
  raise HTTPException(status_code=404, detail="User not found")
357
 
358
  # Verify current password
359
+ if not verify_password(request.current_password, user.password_hash, user.salt):
360
  raise HTTPException(status_code=401, detail="Current password is incorrect")
361
 
362
  # Hash new password
363
  new_hash, new_salt = hash_password(request.new_password)
 
 
364
 
365
+ # Update user via ConfigProvider
366
+ ConfigProvider.update_user_password(username, new_hash, new_salt)
367
 
368
  log(f"✅ Password changed for user '{username}'")
369
  return {"success": True}
 
400
  @router.get("/environment")
401
  async def get_environment(username: str = Depends(verify_token)):
402
  """Get environment configuration"""
403
+ cfg = ConfigProvider.get()
404
+ env_config = cfg.global_config
405
 
406
  return {
407
+ "work_mode": env_config.work_mode,
408
+ "cloud_token": env_config.cloud_token or "",
409
+ "spark_endpoint": str(env_config.spark_endpoint),
410
+ "internal_prompt": env_config.internal_prompt or "",
411
+ "tts_engine": env_config.tts_engine,
412
+ "tts_engine_api_key": env_config.tts_engine_api_key or "",
413
+ "tts_settings": env_config.get_tts_settings(),
414
+ "stt_engine": env_config.stt_engine,
415
+ "stt_engine_api_key": env_config.stt_engine_api_key or "",
416
+ "stt_settings": env_config.get_stt_settings(),
417
+ "parameter_collection_config": env_config.parameter_collection_config.model_dump()
 
 
 
 
 
 
 
 
 
 
 
418
  }
419
 
420
  @router.put("/environment")
 
423
  username: str = Depends(verify_token)
424
  ):
425
  """Update environment configuration"""
426
+ log(f"📝 Updating environment config by {username}")
427
 
428
  # Token validation based on mode
429
  if update.work_mode in ("gpt4o", "gpt4o-mini"):
 
435
  if not update.cloud_token:
436
  raise HTTPException(status_code=400, detail="Cloud token is required for cloud modes")
437
 
 
 
 
 
438
  # TTS/STT validation
439
  if update.tts_engine not in ("no_tts", "elevenlabs", "blaze"):
440
  raise HTTPException(status_code=400, detail="Invalid TTS engine")
 
452
  if update.work_mode not in ("gpt4o", "gpt4o-mini") and not update.spark_endpoint:
453
  raise HTTPException(status_code=400, detail="Spark endpoint is required for non-GPT modes")
454
 
455
+ # Update via ConfigProvider
456
+ ConfigProvider.update_environment(update.model_dump(), username)
457
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
458
  log(f"✅ Environment updated to {update.work_mode} with TTS: {update.tts_engine}, STT: {update.stt_engine} by {username}")
459
  return {"success": True}
460
 
 
462
  @router.get("/projects/names")
463
  def list_enabled_projects():
464
  """Get list of enabled project names for chat"""
465
+ cfg = ConfigProvider.get()
466
+ return [p.name for p in cfg.projects if p.enabled and not getattr(p, 'deleted', False)]
 
467
 
468
  @router.get("/projects")
469
  async def list_projects(
 
471
  username: str = Depends(verify_token)
472
  ):
473
  """List all projects"""
474
+ cfg = ConfigProvider.get()
475
+ projects = cfg.projects
476
 
477
  # Filter deleted if needed
478
  if not include_deleted:
479
+ projects = [p for p in projects if not getattr(p, 'deleted', False)]
480
 
481
+ return [p.model_dump() for p in projects]
482
 
483
  @router.get("/projects/{project_id}")
484
  async def get_project(
 
486
  username: str = Depends(verify_token)
487
  ):
488
  """Get single project by ID"""
489
+ project = ConfigProvider.get_project(project_id)
490
+ if not project or getattr(project, 'deleted', False):
491
+ raise HTTPException(status_code=404, detail="Project not found")
492
+
493
+ return project.model_dump()
 
 
 
 
 
 
 
 
 
 
494
 
495
  @router.post("/projects")
496
  async def create_project(
 
498
  username: str = Depends(verify_token)
499
  ):
500
  """Create new project with initial version"""
 
 
 
 
 
 
 
501
  # Validate supported languages
502
  from locale_manager import LocaleManager
503
 
 
511
  )
512
 
513
  # Check if default language is in supported languages
 
 
514
  if not project.supported_languages:
515
  raise HTTPException(
516
  status_code=400,
517
  detail="At least one supported language must be selected"
518
  )
519
 
520
+ # Create project via ConfigProvider
521
+ new_project = ConfigProvider.create_project(project.model_dump(), username)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522
 
523
  log(f"✅ Project '{project.name}' created by {username}")
524
+ return new_project.model_dump()
525
 
526
  @router.put("/projects/{project_id}")
527
  async def update_project(
 
530
  username: str = Depends(verify_token)
531
  ):
532
  """Update project"""
533
+ # Update via ConfigProvider
534
+ updated_project = ConfigProvider.update_project(project_id, update.model_dump(), username)
 
 
 
 
535
 
536
+ log(f"✅ Project '{updated_project.name}' updated by {username}")
537
+ return updated_project.model_dump()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538
 
539
  @router.delete("/projects/{project_id}")
540
  async def delete_project(project_id: int, username: str = Depends(verify_token)):
541
  """Delete project (soft delete)"""
542
+ ConfigProvider.delete_project(project_id, username)
 
 
 
 
 
543
 
544
+ log(f"✅ Project deleted by {username}")
 
 
 
 
 
 
 
 
 
 
 
545
  return {"success": True}
546
 
547
  @router.patch("/projects/{project_id}/toggle")
548
  async def toggle_project(project_id: int, username: str = Depends(verify_token)):
549
  """Toggle project enabled status"""
550
+ enabled = ConfigProvider.toggle_project(project_id, username)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
 
552
+ log(f"✅ Project {'enabled' if enabled else 'disabled'} by {username}")
553
+ return {"enabled": enabled}
 
 
 
554
 
555
  # ===================== Version Endpoints =====================
556
  @router.get("/projects/{project_id}/versions")
 
560
  username: str = Depends(verify_token)
561
  ):
562
  """List project versions"""
563
+ project = ConfigProvider.get_project(project_id)
 
 
 
564
  if not project:
565
  raise HTTPException(status_code=404, detail="Project not found")
566
 
567
+ versions = project.versions
568
 
569
  # Filter deleted if needed
570
  if not include_deleted:
571
+ versions = [v for v in versions if not getattr(v, 'deleted', False)]
572
 
573
+ return [v.model_dump() for v in versions]
574
 
575
  @router.post("/projects/{project_id}/versions")
576
  async def create_version(
 
579
  username: str = Depends(verify_token)
580
  ):
581
  """Create new version"""
582
+ new_version = ConfigProvider.create_version(project_id, version_data.model_dump(), username)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
583
 
584
+ log(f"✅ Version created for project {project_id} by {username}")
585
+ return new_version.model_dump()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
586
 
587
  @router.put("/projects/{project_id}/versions/{version_id}")
588
  async def update_version(
 
592
  username: str = Depends(verify_token)
593
  ):
594
  """Update version"""
595
+ updated_version = ConfigProvider.update_version(project_id, version_id, update.model_dump(), username)
 
 
 
 
 
 
 
 
 
 
 
 
 
596
 
597
+ log(f"✅ Version {version_id} updated for project {project_id} by {username}")
598
+ return updated_version.model_dump()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
599
 
600
  @router.post("/projects/{project_id}/versions/{version_id}/publish")
601
  async def publish_version(
 
604
  username: str = Depends(verify_token)
605
  ):
606
  """Publish version"""
607
+ project, version = ConfigProvider.publish_version(project_id, version_id, username)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
608
 
609
+ log(f"✅ Version {version_id} published for project '{project.name}' by {username}")
610
 
611
  # Notify Spark if project is enabled
612
+ if project.enabled:
613
  try:
614
+ cfg = ConfigProvider.get()
615
+ await notify_spark_manual(
616
+ project.model_dump(),
617
+ version.model_dump(),
618
+ cfg.global_config.model_dump()
619
+ )
620
  except Exception as e:
621
  log(f"⚠️ Failed to notify Spark: {e}")
622
  # Don't fail the publish
 
630
  username: str = Depends(verify_token)
631
  ):
632
  """Delete version (soft delete)"""
633
+ ConfigProvider.delete_version(project_id, version_id, username)
 
 
 
 
 
 
 
 
 
 
 
 
 
634
 
635
+ log(f"✅ Version {version_id} deleted for project {project_id} by {username}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
636
  return {"success": True}
637
 
638
  @router.post("/validate/regex")
 
671
  username: str = Depends(verify_token)
672
  ):
673
  """List all APIs"""
674
+ cfg = ConfigProvider.get()
675
+ apis = cfg.apis
676
 
677
  # Filter deleted if needed
678
  if not include_deleted:
679
+ apis = [a for a in apis if not getattr(a, 'deleted', False)]
680
 
681
+ return [a.model_dump() for a in apis]
682
 
683
  @router.post("/apis")
684
  async def create_api(api: APICreate, username: str = Depends(verify_token)):
685
  """Create new API"""
686
+ new_api = ConfigProvider.create_api(api.model_dump(), username)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
687
 
688
  log(f"✅ API '{api.name}' created by {username}")
689
+ return new_api.model_dump()
690
 
691
  @router.put("/apis/{api_name}")
692
  async def update_api(
 
695
  username: str = Depends(verify_token)
696
  ):
697
  """Update API"""
698
+ updated_api = ConfigProvider.update_api(api_name, update.model_dump(), username)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
699
 
700
  log(f"✅ API '{api_name}' updated by {username}")
701
+ return updated_api.model_dump()
702
 
703
  @router.delete("/apis/{api_name}")
704
  async def delete_api(api_name: str, username: str = Depends(verify_token)):
705
  """Delete API (soft delete)"""
706
+ ConfigProvider.delete_api(api_name, username)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
707
 
708
  log(f"✅ API '{api_name}' deleted by {username}")
709
  return {"success": True}
 
716
  if not project_name:
717
  raise HTTPException(status_code=400, detail="project_name is required")
718
 
719
+ project = ConfigProvider.get_project_by_name(project_name)
 
 
 
720
  if not project:
721
  raise HTTPException(status_code=404, detail=f"Project not found: {project_name}")
722
 
723
  # Find published version
724
+ version = next((v for v in project.versions if v.published), None)
725
  if not version:
726
  raise HTTPException(status_code=400, detail=f"No published version found for project: {project_name}")
727
 
728
  # Notify Spark
729
  try:
730
+ cfg = ConfigProvider.get()
731
+ result = await notify_spark_manual(
732
+ project.model_dump(),
733
+ version.model_dump(),
734
+ cfg.global_config.model_dump()
735
+ )
736
  return {"message": result.get("message", "Spark startup initiated")}
737
  except Exception as e:
738
  log(f"❌ Spark startup failed: {e}")
 
741
  @router.get("/spark/projects")
742
  async def spark_get_projects(username: str = Depends(verify_token)):
743
  """Get Spark project list"""
744
+ cfg = ConfigProvider.get()
745
+ spark_endpoint = str(cfg.global_config.spark_endpoint).rstrip("/")
746
  spark_token = _get_spark_token()
747
 
748
  if not spark_endpoint:
 
962
  username: str = Depends(verify_token)
963
  ):
964
  """Import project from JSON"""
965
+ imported_project = ConfigProvider.import_project(project_data, username)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
966
 
967
+ log(f"✅ Project '{imported_project.name}' imported by {username}")
968
+ return imported_project.model_dump()
 
 
 
 
 
 
 
 
 
 
 
969
 
970
  @router.get("/projects/{project_id}/export")
971
  async def export_project(
 
973
  username: str = Depends(verify_token)
974
  ):
975
  """Export project as JSON"""
976
+ export_data = ConfigProvider.export_project(project_id, username)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
977
 
978
+ log(f"✅ Project exported by {username}")
979
  return export_data
980
 
981
  # ===================== TTS Endpoints =====================
 
986
  ):
987
  """Generate TTS audio from text"""
988
  try:
989
+ # ConfigProvider'dan güncel config'i al (reload yerine get kullan)
990
+ cfg = ConfigProvider.get()
 
991
 
992
  tts_engine = cfg.global_config.tts_engine
993
  log(f"🔧 TTS Engine: {tts_engine}")
 
1049
  username: str = Depends(verify_token)
1050
  ):
1051
  """Get activity log"""
1052
+ cfg = ConfigProvider.get()
1053
+ logs = cfg.activity_log
1054
 
1055
+ # Return latest entries
1056
+ return [log.model_dump() for log in logs[-limit:]]
1057
 
1058
  # ===================== Cleanup Task =====================
1059
  def cleanup_old_logs():
1060
  """Cleanup old activity logs (runs in background)"""
1061
  while True:
1062
  try:
1063
+ ConfigProvider.cleanup_activity_logs()
1064
+ log("🧹 Cleaned up old activity logs")
 
 
 
 
1065
  except Exception as e:
1066
  log(f"Error in cleanup task: {e}")
1067