Initial MASON implementation - Data editing workspace

- Flask API for data review and completion
- Edit items before forwarding to FELDMAN
- Receives data from CLARA and incidencias from ALFRED/JARED
- Docker deployment on port 5053
This commit is contained in:
ARCHITECT
2025-12-24 10:32:39 +00:00
commit edf37b7d97
7 changed files with 379 additions and 0 deletions

280
app.py Normal file
View File

@@ -0,0 +1,280 @@
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/<id>': {'method': 'GET', 'auth': True, 'desc': 'Get item details'},
'/item/<id>': {'method': 'PUT', 'auth': True, 'desc': 'Edit item data'},
'/item/<id>/resolver': {'method': 'POST', 'auth': True, 'desc': 'Mark as resolved and forward to FELDMAN'},
'/item/<id>/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/<int:item_id>', 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/<int:item_id>', 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/<int:item_id>/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/<int:item_id>/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)