from flask import Flask, request, jsonify from functools import wraps import psycopg2 from psycopg2.extras import RealDictCursor import os import hashlib import json from datetime import datetime app = Flask(__name__) H_INSTANCIA = os.environ.get('H_INSTANCIA') DB_HOST = os.environ.get('DB_HOST', '172.17.0.1') DB_PORT = os.environ.get('DB_PORT', '5432') DB_NAME = os.environ.get('DB_NAME', 'corp') DB_USER = os.environ.get('DB_USER', 'corp') DB_PASSWORD = os.environ.get('DB_PASSWORD', 'corp') PORT = int(os.environ.get('PORT', 5053)) def get_db(): return psycopg2.connect( host=DB_HOST, port=DB_PORT, database=DB_NAME, user=DB_USER, password=DB_PASSWORD, cursor_factory=RealDictCursor ) def require_auth(f): @wraps(f) def decorated(*args, **kwargs): auth_key = request.headers.get('X-Auth-Key') if not auth_key or auth_key != H_INSTANCIA: return jsonify({'error': 'Unauthorized'}), 401 return f(*args, **kwargs) return decorated def generate_hash(data): return hashlib.sha256(f"{data}{datetime.now().isoformat()}".encode()).hexdigest()[:64] @app.route('/health', methods=['GET']) def health(): try: conn = get_db() cur = conn.cursor() cur.execute('SELECT 1') cur.close() conn.close() return jsonify({'status': 'healthy', 'service': 'mason', 'version': '1.0.0'}) except Exception as e: return jsonify({'status': 'unhealthy', 'error': str(e)}), 500 @app.route('/s-contract', methods=['GET']) def s_contract(): return jsonify({ 'service': 'mason', 'version': '1.0.0', 'contract_version': 'S-CONTRACT v2.1', 'description': 'Editing workspace for data with incidencias - review and correct before forwarding', 'endpoints': { '/health': {'method': 'GET', 'auth': False}, '/recibir': {'method': 'POST', 'auth': True, 'desc': 'Receive data with incidencia'}, '/pendientes': {'method': 'GET', 'auth': True, 'desc': 'List pending items to review'}, '/item/': {'method': 'GET', 'auth': True, 'desc': 'Get item details'}, '/item/': {'method': 'PUT', 'auth': True, 'desc': 'Edit item data'}, '/item//resolver': {'method': 'POST', 'auth': True, 'desc': 'Mark as resolved and forward to FELDMAN'}, '/item//descartar': {'method': 'POST', 'auth': True, 'desc': 'Discard item'}, '/historial': {'method': 'GET', 'auth': True, 'desc': 'Resolved/discarded items'}, '/stats': {'method': 'GET', 'auth': True, 'desc': 'Statistics'} } }) # Receive data with incidencia (from ALFRED/JARED routing) @app.route('/recibir', methods=['POST']) @require_auth def recibir(): data = request.get_json() or {} h_registro = generate_hash(str(data)) conn = get_db() cur = conn.cursor() cur.execute(''' INSERT INTO incidencias (h_incidencia, h_instancia_origen, h_ejecucion, tipo, descripcion, datos, estado) VALUES (%s, %s, %s, %s, %s, %s, %s) RETURNING id, h_incidencia, created_at ''', ( h_registro, data.get('h_instancia_origen', 'unknown'), data.get('h_ejecucion', ''), data.get('tipo', 'revision'), data.get('descripcion', 'Pendiente de revision'), psycopg2.extras.Json(data), 'pendiente' )) result = cur.fetchone() conn.commit() cur.close() conn.close() return jsonify({ 'success': True, 'mensaje': 'Recibido para revision', 'registro': dict(result) }) # List pending items (need review/editing) @app.route('/pendientes', methods=['GET']) @require_auth def pendientes(): conn = get_db() cur = conn.cursor() cur.execute(''' SELECT id, h_incidencia, h_instancia_origen, tipo, descripcion, datos, created_at FROM incidencias WHERE estado = 'pendiente' ORDER BY created_at ASC ''') items = cur.fetchall() cur.close() conn.close() return jsonify({'pendientes': [dict(i) for i in items], 'count': len(items)}) # Get item details @app.route('/item/', methods=['GET']) @require_auth def get_item(item_id): conn = get_db() cur = conn.cursor() cur.execute('SELECT * FROM incidencias WHERE id = %s', (item_id,)) item = cur.fetchone() cur.close() conn.close() if not item: return jsonify({'error': 'Item not found'}), 404 return jsonify({'item': dict(item)}) # Edit item data @app.route('/item/', methods=['PUT']) @require_auth def edit_item(item_id): data = request.get_json() or {} conn = get_db() cur = conn.cursor() # Get current item cur.execute('SELECT * FROM incidencias WHERE id = %s AND estado = %s', (item_id, 'pendiente')) item = cur.fetchone() if not item: cur.close() conn.close() return jsonify({'error': 'Item not found or already processed'}), 404 # Merge edited data current_datos = item['datos'] or {} if 'datos' in data: current_datos.update(data['datos']) cur.execute(''' UPDATE incidencias SET datos = %s, descripcion = %s, updated_at = NOW() WHERE id = %s RETURNING id, datos, updated_at ''', ( psycopg2.extras.Json(current_datos), data.get('descripcion', item['descripcion']), item_id )) result = cur.fetchone() conn.commit() cur.close() conn.close() return jsonify({'success': True, 'mensaje': 'Item actualizado', 'item': dict(result)}) # Resolve and forward to FELDMAN @app.route('/item//resolver', methods=['POST']) @require_auth def resolver_item(item_id): data = request.get_json() or {} conn = get_db() cur = conn.cursor() cur.execute('SELECT * FROM incidencias WHERE id = %s AND estado = %s', (item_id, 'pendiente')) item = cur.fetchone() if not item: cur.close() conn.close() return jsonify({'error': 'Item not found or already processed'}), 404 # Mark as resolved cur.execute(''' UPDATE incidencias SET estado = 'resuelto', resolucion = %s, resolved_at = NOW(), updated_at = NOW() WHERE id = %s RETURNING id, h_incidencia, estado, resolved_at ''', (data.get('resolucion', 'Corregido manualmente'), item_id)) result = cur.fetchone() # Insert into completados (FELDMAN table) with corrected data h_completado = generate_hash(str(item['datos'])) cur.execute(''' INSERT INTO completados (h_completado, h_instancia_origen, h_ejecucion, flujo_nombre, datos, notas) VALUES (%s, %s, %s, %s, %s, %s) ''', ( h_completado, item['h_instancia_origen'], item['h_ejecucion'], item['datos'].get('flujo_nombre', ''), psycopg2.extras.Json(item['datos']), f"Corregido en MASON: {data.get('resolucion', '')}" )) conn.commit() cur.close() conn.close() return jsonify({ 'success': True, 'mensaje': 'Item resuelto y enviado a FELDMAN', 'item': dict(result) }) # Discard item @app.route('/item//descartar', methods=['POST']) @require_auth def descartar_item(item_id): data = request.get_json() or {} conn = get_db() cur = conn.cursor() cur.execute(''' UPDATE incidencias SET estado = 'descartado', resolucion = %s, resolved_at = NOW(), updated_at = NOW() WHERE id = %s AND estado = 'pendiente' RETURNING id, h_incidencia, estado ''', (data.get('motivo', 'Descartado'), item_id)) result = cur.fetchone() conn.commit() cur.close() conn.close() if not result: return jsonify({'error': 'Item not found or already processed'}), 404 return jsonify({'success': True, 'mensaje': 'Item descartado', 'item': dict(result)}) # History of resolved/discarded @app.route('/historial', methods=['GET']) @require_auth def historial(): limit = request.args.get('limit', 50, type=int) conn = get_db() cur = conn.cursor() cur.execute(''' SELECT * FROM incidencias WHERE estado IN ('resuelto', 'descartado') ORDER BY resolved_at DESC LIMIT %s ''', (limit,)) items = cur.fetchall() cur.close() conn.close() return jsonify({'historial': [dict(i) for i in items], 'count': len(items)}) # Stats @app.route('/stats', methods=['GET']) @require_auth def stats(): conn = get_db() cur = conn.cursor() cur.execute('SELECT estado, COUNT(*) as count FROM incidencias GROUP BY estado') por_estado = {r['estado']: r['count'] for r in cur.fetchall()} cur.execute('SELECT COUNT(*) as total FROM incidencias WHERE estado = %s', ('pendiente',)) pendientes = cur.fetchone()['total'] cur.close() conn.close() return jsonify({'pendientes': pendientes, 'por_estado': por_estado}) if __name__ == '__main__': app.run(host='0.0.0.0', port=PORT, debug=False)