From fe357668b48ed8309f7929de8534efe3c595f73a Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 16 May 2026 09:18:39 +0000 Subject: [PATCH] feat(admin): system config and global executions history API Co-Authored-By: Claude Sonnet 4.6 --- backend/app/api/admin.py | 75 +++++++++++++++++++++++++++++++++ backend/tests/test_admin_api.py | 67 +++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 backend/tests/test_admin_api.py diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index e69de29b..1455e589 100644 --- a/backend/app/api/admin.py +++ b/backend/app/api/admin.py @@ -0,0 +1,75 @@ +"""Admin API: configuració sistema i historial d'execucions.""" +from flask import request, jsonify +from sqlalchemy import select, desc, func +from . import admin_bp +from .. import require_admin +from ..db import get_session +from ..models.db_models import SystemConfigModel, SimulationModel, ProjectModel, UserModel + + +@admin_bp.route('/config', methods=['GET']) +@require_admin +def get_config(): + with get_session() as db: + entries = db.execute(select(SystemConfigModel)).scalars().all() + result = [] + for e in entries: + result.append({ + 'key': e.key, + 'value': '●●●●' if e.is_secret else e.value, + 'value_type': e.value_type, + 'group': e.group, + 'label': e.label, + 'description': e.description, + 'is_secret': e.is_secret, + }) + return jsonify({'success': True, 'data': result}) + + +@admin_bp.route('/config', methods=['PATCH']) +@require_admin +def patch_config(): + data = request.get_json(silent=True) or {} + with get_session() as db: + for key, value in data.items(): + entry = db.get(SystemConfigModel, key) + if entry: + entry.value = str(value) + db.commit() + return jsonify({'success': True}) + + +@admin_bp.route('/executions', methods=['GET']) +@require_admin +def list_executions(): + page = request.args.get('page', 1, type=int) + page_size = request.args.get('pageSize', 20, type=int) + filter_user_id = request.args.get('user_id') + offset = (page - 1) * page_size + + with get_session() as db: + stmt = ( + select(SimulationModel, ProjectModel, UserModel) + .join(ProjectModel, SimulationModel.project_id == ProjectModel.id) + .outerjoin(UserModel, ProjectModel.user_id == UserModel.id) + .order_by(desc(SimulationModel.created_at)) + ) + if filter_user_id: + stmt = stmt.where(ProjectModel.user_id == filter_user_id) + + total = db.execute(select(func.count()).select_from(stmt.subquery())).scalar() + rows = db.execute(stmt.offset(offset).limit(page_size)).all() + result = [] + for sim, proj, user in rows: + result.append({ + 'simulation_id': sim.id, + 'project_id': proj.id, + 'project_name': proj.name, + 'user_email': user.email if user else None, + 'status': sim.status, + 'platform': sim.platform, + 'rounds_total': sim.rounds_total, + 'rounds_completed': sim.rounds_completed, + 'created_at': sim.created_at.isoformat(), + }) + return jsonify({'success': True, 'data': result, 'total': total, 'page': page, 'pageSize': page_size}) diff --git a/backend/tests/test_admin_api.py b/backend/tests/test_admin_api.py new file mode 100644 index 00000000..e44e2b3b --- /dev/null +++ b/backend/tests/test_admin_api.py @@ -0,0 +1,67 @@ +"""Tests per a l'API d'administració.""" +import pytest +from unittest.mock import MagicMock, patch + + +@pytest.fixture +def app(in_memory_db): + import backend.app.db as db_module + saved_engine = db_module._engine + saved_session = db_module._SessionLocal + + def _noop(url): + db_module._engine = saved_engine + db_module._SessionLocal = saved_session + + with patch('backend.app.db.init_db', side_effect=_noop): + from backend.app import create_app + application = create_app() + + application.config['TESTING'] = True + application.extensions['storage'] = MagicMock() + db_module._engine = saved_engine + db_module._SessionLocal = saved_session + return application + + +@pytest.fixture +def client(app): + with app.test_client() as c: + yield c + + +def test_get_config_empty(client, in_memory_db): + res = client.get('/api/admin/config') + assert res.status_code == 200 + data = res.get_json() + assert data['success'] is True + assert isinstance(data['data'], list) + + +def test_patch_config(client, in_memory_db): + from backend.app.models.db_models import SystemConfigModel + from backend.app.db import get_session + with get_session() as db: + db.add(SystemConfigModel( + key='llm.model_name', value='qwen-plus', + value_type='string', group='llm', + label='Model LLM', description='Nom del model LLM principal', + is_secret=False + )) + db.commit() + + res = client.patch('/api/admin/config', json={'llm.model_name': 'gpt-4o'}) + assert res.status_code == 200 + + res2 = client.get('/api/admin/config') + entries = res2.get_json()['data'] + entry = next(e for e in entries if e['key'] == 'llm.model_name') + assert entry['value'] == 'gpt-4o' + + +def test_get_executions_empty(client, in_memory_db): + res = client.get('/api/admin/executions') + assert res.status_code == 200 + data = res.get_json() + assert data['success'] is True + assert data['data'] == []