Ctrl K
ComunidadGuías…

routes/report_issue.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""
Issue reporting — user-submitted bug/feedback reports.

POST /api/report-issue   — save an issue report locally + forward to feedback service
GET  /api/report-issues  — list recent local reports

For public installs (no Clerk auth configured), reports are also sent to
https://feedback.openvoiceui.com/api/report so the dev team can see them.
This is fire-and-forget — local save always succeeds regardless.
"""

import json
import logging
import os
import threading
from datetime import datetime
from pathlib import Path

import requests as http_requests
from flask import Blueprint, jsonify, request

from services.paths import RUNTIME_DIR

logger = logging.getLogger(__name__)

report_issue_bp = Blueprint('report_issue', __name__)

REPORTS_DIR = RUNTIME_DIR / 'issue-reports'

FEEDBACK_URL = 'https://feedback.openvoiceui.com/api/report'


def _is_public_install() -> bool:
    """True if this is a public/Pinokio install (no Clerk auth configured)."""
    return not os.environ.get('CLERK_PUBLISHABLE_KEY', '').strip()


def _forward_to_feedback_service(report: dict):
    """Fire-and-forget POST to the public feedback service."""
    try:
        resp = http_requests.post(
            FEEDBACK_URL,
            json=report,
            timeout=10,
            headers={'Content-Type': 'application/json'},
        )
        if resp.ok:
            logger.info('Feedback forwarded to %s', FEEDBACK_URL)
        else:
            logger.warning('Feedback forward failed: %s %s', resp.status_code, resp.text[:200])
    except Exception as e:
        logger.warning('Feedback forward error: %s', e)


@report_issue_bp.route('/api/report-issue', methods=['POST'])
def submit_issue():
    data = request.get_json(force=True, silent=True) or {}

    issue_type = (data.get('type') or 'other').strip()[:50]
    description = (data.get('description') or '').strip()[:2000]
    context = data.get('context') or {}

    if not description:
        return jsonify({'error': 'Description required'}), 400

    now = datetime.now()
    report = {
        'ts': now.isoformat(),
        'type': issue_type,
        'description': description,
        'context': context,
        'ua': request.headers.get('User-Agent', ''),
    }

    # Always save locally. Never let a filesystem error bubble up as an HTML 500
    # page — the frontend does res.json() and an HTML body yields the cryptic
    # "Unexpected token '<', "<!doctype "" error. Return clean JSON instead, and
    # still forward to the feedback service so the report isn't lost.
    filename = None
    save_error = None
    try:
        REPORTS_DIR.mkdir(parents=True, exist_ok=True)

        date_str = now.strftime('%Y-%m-%d')
        time_str = now.strftime('%H-%M-%S')
        filename = f'{date_str}_{time_str}_{issue_type}.json'
        filepath = REPORTS_DIR / filename

        if filepath.exists():
            filename = f'{date_str}_{time_str}_{issue_type}_2.json'
            filepath = REPORTS_DIR / filename

        filepath.write_text(json.dumps(report, indent=2))
    except OSError as e:
        save_error = str(e)
        logger.error('Failed to save issue report locally: %s', e)

    # Forward to public feedback service (fire-and-forget, non-blocking)
    if _is_public_install():
        threading.Thread(
            target=_forward_to_feedback_service,
            args=(report,),
            daemon=True,
        ).start()

    if filename is not None:
        return jsonify({'ok': True, 'saved': filename})
    # Local save failed but we still accepted the report (forwarded if public).
    # Report it as accepted so the user isn't blocked, with a soft warning.
    return jsonify({'ok': True, 'saved': None, 'warning': 'stored remotely only', 'detail': save_error})


@report_issue_bp.route('/api/report-issues', methods=['GET'])
def list_issues():
    """Return last N issue reports, newest first."""
    REPORTS_DIR.mkdir(parents=True, exist_ok=True)
    files = sorted(REPORTS_DIR.glob('*.json'), reverse=True)[:50]
    reports = []
    for f in files:
        try:
            reports.append(json.loads(f.read_text()))
        except Exception:
            pass
    return jsonify(reports)