commit 5e135466ec9a3ae7c81ab6153f97759ac388c5c4 Author: ARCHITECT Date: Wed Dec 24 10:32:40 2025 +0000 Initial FELDMAN implementation - Completed flows registry - Flask API for storing completed flows - Receives OK flows from ALFRED/JARED - Receives corrected data from MASON - Docker deployment on port 5054 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ff09ddb --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +H_INSTANCIA=your_h_instancia_here +DB_HOST=172.17.0.1 +DB_PORT=5432 +DB_NAME=corp +DB_USER=corp +DB_PASSWORD=your_password_here +PORT=5054 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ae0b5d3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.11-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY app.py . +EXPOSE 5054 +CMD ["gunicorn", "--bind", "0.0.0.0:5054", "--workers", "2", "app:app"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..5c6e283 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# FELDMAN - Completed Flows Registry + +Registro de flujos completados exitosamente en el sistema TZZR. + +## Descripcion + +FELDMAN recibe y almacena: +- Flujos OK directamente desde ALFRED/JARED +- Datos corregidos desde MASON + +## Endpoints + +| Endpoint | Metodo | Auth | Descripcion | +|----------|--------|------|-------------| +| `/health` | GET | No | Health check | +| `/s-contract` | GET | No | Contrato del servicio | +| `/recibir` | POST | Si | Recibir flujo completado | +| `/completados` | GET | Si | Listar completados | +| `/stats` | GET | Si | Estadisticas | + +## Puerto + +5054 diff --git a/app.py b/app.py new file mode 100644 index 0000000..a4f0ebc --- /dev/null +++ b/app.py @@ -0,0 +1,124 @@ +from flask import Flask, request, jsonify +from functools import wraps +import psycopg2 +from psycopg2.extras import RealDictCursor +import os +import hashlib +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', 5054)) + +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': 'feldman', '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': 'feldman', + 'version': '1.0.0', + 'contract_version': 'S-CONTRACT v2.1', + 'description': 'Receives routed OK flows from ALFRED/JARED', + 'endpoints': { + '/health': {'method': 'GET', 'auth': False}, + '/recibir': {'method': 'POST', 'auth': True, 'desc': 'Receive completed flow'}, + '/completados': {'method': 'GET', 'auth': True, 'desc': 'List completed flows'}, + '/stats': {'method': 'GET', 'auth': True, 'desc': 'Statistics'} + } + }) + +@app.route('/recibir', methods=['POST']) +@require_auth +def recibir(): + data = request.get_json() or {} + h_completado = generate_hash(str(data)) + + conn = get_db() + cur = conn.cursor() + cur.execute(''' + INSERT INTO completados + (h_completado, h_instancia_origen, h_ejecucion, flujo_nombre, datos, notas) + VALUES (%s, %s, %s, %s, %s, %s) + RETURNING id, h_completado, created_at + ''', ( + h_completado, + data.get('h_instancia_origen', 'unknown'), + data.get('h_ejecucion', ''), + data.get('flujo_nombre', ''), + psycopg2.extras.Json(data), + data.get('notas', '') + )) + result = cur.fetchone() + conn.commit() + cur.close() + conn.close() + + return jsonify({'success': True, 'registro': dict(result), 'service': 'feldman'}) + +@app.route('/completados', methods=['GET']) +@require_auth +def completados(): + limit = request.args.get('limit', 50, type=int) + conn = get_db() + cur = conn.cursor() + cur.execute('SELECT * FROM completados ORDER BY created_at DESC LIMIT %s', (limit,)) + records = cur.fetchall() + cur.close() + conn.close() + return jsonify({'completados': [dict(r) for r in records], 'count': len(records)}) + +@app.route('/stats', methods=['GET']) +@require_auth +def stats(): + conn = get_db() + cur = conn.cursor() + cur.execute('SELECT COUNT(*) as total FROM completados') + total = cur.fetchone()['total'] + cur.execute(''' + SELECT DATE(created_at) as fecha, COUNT(*) as count + FROM completados + GROUP BY DATE(created_at) + ORDER BY fecha DESC LIMIT 7 + ''') + por_dia = {str(r['fecha']): r['count'] for r in cur.fetchall()} + cur.close() + conn.close() + return jsonify({'total': total, 'por_dia': por_dia}) + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=PORT, debug=False) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6154397 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +version: "3.8" +services: + feldman: + build: . + container_name: feldman-service + restart: unless-stopped + ports: + - "5054:5054" + environment: + - H_INSTANCIA=${H_INSTANCIA} + - DB_HOST=${DB_HOST} + - DB_PORT=${DB_PORT} + - DB_NAME=${DB_NAME} + - DB_USER=${DB_USER} + - DB_PASSWORD=${DB_PASSWORD} + - PORT=5054 + extra_hosts: + - "host.docker.internal:host-gateway" + networks: + - tzzr-network +networks: + tzzr-network: + external: true diff --git a/init.sql b/init.sql new file mode 100644 index 0000000..b2fd52b --- /dev/null +++ b/init.sql @@ -0,0 +1,14 @@ +-- FELDMAN tables + +CREATE TABLE IF NOT EXISTS completados ( + id BIGSERIAL PRIMARY KEY, + h_completado VARCHAR(64) NOT NULL UNIQUE, + h_instancia_origen VARCHAR(64) NOT NULL, + h_ejecucion VARCHAR(64), + flujo_nombre VARCHAR(100), + datos JSONB DEFAULT '{}', + notas TEXT, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_completados_origen ON completados(h_instancia_origen); diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..efddf1d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +flask==3.0.0 +psycopg2-binary==2.9.9 +gunicorn==21.2.0