Spaces:
Sleeping
Sleeping
Commit Β·
0ce5100
1
Parent(s): 365a20a
ueifu
Browse files
app.py
CHANGED
|
@@ -14,7 +14,8 @@ from flask import Flask, request, jsonify, redirect, url_for, render_template_st
|
|
| 14 |
from pathlib import Path
|
| 15 |
import requests
|
| 16 |
from typing import List, Dict
|
| 17 |
-
|
|
|
|
| 18 |
# Import RAG utilities
|
| 19 |
from rag_utils import get_comprehensive_context, format_context_for_prompt
|
| 20 |
|
|
@@ -98,6 +99,8 @@ def init_db():
|
|
| 98 |
"""Initialize database tables - runs once when app starts"""
|
| 99 |
con = sql.connect("swift_check.db")
|
| 100 |
cur = con.cursor()
|
|
|
|
|
|
|
| 101 |
cur.execute("""
|
| 102 |
CREATE TABLE IF NOT EXISTS qc_requests (
|
| 103 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -142,11 +145,161 @@ def init_db():
|
|
| 142 |
FOREIGN KEY (request_id) REFERENCES qc_requests(id)
|
| 143 |
)""")
|
| 144 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
con.commit()
|
| 146 |
con.close()
|
| 147 |
|
| 148 |
init_db()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
def extract_top_level_json_array(text):
|
| 151 |
"""
|
| 152 |
function to extract JSON array from text, handling both raw JSON and code blocks
|
|
@@ -1698,6 +1851,7 @@ def index():
|
|
| 1698 |
</html>
|
| 1699 |
"""# UPDATED: refine endpoint to respect user requirements
|
| 1700 |
@app.route("/refine", methods=["POST"])
|
|
|
|
| 1701 |
def refine_parameters():
|
| 1702 |
"""refine endpoint that respects user intent and requirements"""
|
| 1703 |
global global_parameters
|
|
@@ -1899,6 +2053,7 @@ def refine_parameters():
|
|
| 1899 |
return jsonify({"error": str(e)}), 500
|
| 1900 |
|
| 1901 |
@app.route("/edit", methods=["POST"])
|
|
|
|
| 1902 |
def edit_parameters():
|
| 1903 |
"""Edit endpoint that modifies existing templates precisely as commanded"""
|
| 1904 |
global global_parameters
|
|
@@ -2307,6 +2462,7 @@ def edit_parameters():
|
|
| 2307 |
|
| 2308 |
|
| 2309 |
@app.route("/validate", methods=["POST"])
|
|
|
|
| 2310 |
def validate_template():
|
| 2311 |
"""Validate an existing template and get suggestions"""
|
| 2312 |
try:
|
|
@@ -2352,6 +2508,7 @@ def validate_template():
|
|
| 2352 |
return jsonify({"error": str(e)}), 500
|
| 2353 |
|
| 2354 |
@app.route("/digitize", methods=["POST"])
|
|
|
|
| 2355 |
def digitize_checklist():
|
| 2356 |
"""digitization that preserves original document structure without adding extras"""
|
| 2357 |
print(">> /digitize route called <<")
|
|
@@ -2685,7 +2842,290 @@ def get_template_json(request_id):
|
|
| 2685 |
except Exception as e:
|
| 2686 |
print(f"β Error in /template/{request_id}: {str(e)}")
|
| 2687 |
return jsonify({"error": str(e)}), 500
|
|
|
|
| 2688 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2689 |
@app.route("/preview/<int:request_id>", methods=["GET"])
|
| 2690 |
def preview_page(request_id):
|
| 2691 |
"""preview with better formatting and metadata"""
|
|
|
|
| 14 |
from pathlib import Path
|
| 15 |
import requests
|
| 16 |
from typing import List, Dict
|
| 17 |
+
import time
|
| 18 |
+
from functools import wraps
|
| 19 |
# Import RAG utilities
|
| 20 |
from rag_utils import get_comprehensive_context, format_context_for_prompt
|
| 21 |
|
|
|
|
| 99 |
"""Initialize database tables - runs once when app starts"""
|
| 100 |
con = sql.connect("swift_check.db")
|
| 101 |
cur = con.cursor()
|
| 102 |
+
|
| 103 |
+
# Existing tables
|
| 104 |
cur.execute("""
|
| 105 |
CREATE TABLE IF NOT EXISTS qc_requests (
|
| 106 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
| 145 |
FOREIGN KEY (request_id) REFERENCES qc_requests(id)
|
| 146 |
)""")
|
| 147 |
|
| 148 |
+
# NEW: API Logs table
|
| 149 |
+
cur.execute("""
|
| 150 |
+
CREATE TABLE IF NOT EXISTS api_logs (
|
| 151 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 152 |
+
request_id INTEGER,
|
| 153 |
+
endpoint TEXT NOT NULL,
|
| 154 |
+
method TEXT NOT NULL,
|
| 155 |
+
client_ip TEXT,
|
| 156 |
+
user_agent TEXT,
|
| 157 |
+
request_data TEXT,
|
| 158 |
+
response_data TEXT,
|
| 159 |
+
file_info TEXT,
|
| 160 |
+
processing_time_ms INTEGER,
|
| 161 |
+
status_code INTEGER,
|
| 162 |
+
error_message TEXT,
|
| 163 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 164 |
+
FOREIGN KEY (request_id) REFERENCES qc_requests(id)
|
| 165 |
+
)""")
|
| 166 |
+
|
| 167 |
con.commit()
|
| 168 |
con.close()
|
| 169 |
|
| 170 |
init_db()
|
| 171 |
+
def log_api_request(endpoint, method, request_id=None, file_info=None, processing_time=None,
|
| 172 |
+
status_code=200, error_message=None, request_data=None, response_data=None):
|
| 173 |
+
"""Log API request details to database"""
|
| 174 |
+
try:
|
| 175 |
+
con = sql.connect("swift_check.db")
|
| 176 |
+
cur = con.cursor()
|
| 177 |
+
|
| 178 |
+
# Get client info
|
| 179 |
+
client_ip = request.remote_addr or 'unknown'
|
| 180 |
+
user_agent = request.headers.get('User-Agent', 'unknown')[:500] # Limit length
|
| 181 |
+
|
| 182 |
+
# Truncate large data
|
| 183 |
+
request_data_str = str(request_data)[:2000] if request_data else None
|
| 184 |
+
response_data_str = str(response_data)[:1000] if response_data else None
|
| 185 |
+
file_info_str = str(file_info)[:500] if file_info else None
|
| 186 |
+
|
| 187 |
+
cur.execute("""
|
| 188 |
+
INSERT INTO api_logs
|
| 189 |
+
(request_id, endpoint, method, client_ip, user_agent, request_data,
|
| 190 |
+
response_data, file_info, processing_time_ms, status_code, error_message)
|
| 191 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 192 |
+
""", (request_id, endpoint, method, client_ip, user_agent, request_data_str,
|
| 193 |
+
response_data_str, file_info_str, processing_time, status_code, error_message))
|
| 194 |
+
|
| 195 |
+
con.commit()
|
| 196 |
+
con.close()
|
| 197 |
+
|
| 198 |
+
except Exception as e:
|
| 199 |
+
print(f"β Failed to log API request: {str(e)}")
|
| 200 |
+
|
| 201 |
+
def api_logger(endpoint_name):
|
| 202 |
+
"""Decorator to automatically log API requests"""
|
| 203 |
+
def decorator(func):
|
| 204 |
+
@wraps(func)
|
| 205 |
+
def wrapper(*args, **kwargs):
|
| 206 |
+
start_time = time.time()
|
| 207 |
+
|
| 208 |
+
try:
|
| 209 |
+
# Execute the original function
|
| 210 |
+
result = func(*args, **kwargs)
|
| 211 |
+
|
| 212 |
+
# Calculate processing time
|
| 213 |
+
processing_time = int((time.time() - start_time) * 1000)
|
| 214 |
+
|
| 215 |
+
# Extract request_id from result if it's a JSON response
|
| 216 |
+
request_id = None
|
| 217 |
+
if hasattr(result, 'get_json') and result.get_json():
|
| 218 |
+
request_id = result.get_json().get('request_id')
|
| 219 |
+
elif isinstance(result, tuple) and len(result) > 0:
|
| 220 |
+
if hasattr(result[0], 'get_json') and result[0].get_json():
|
| 221 |
+
request_id = result[0].get_json().get('request_id')
|
| 222 |
+
|
| 223 |
+
# Get file info if present
|
| 224 |
+
file_info = None
|
| 225 |
+
if hasattr(request, 'files') and request.files:
|
| 226 |
+
uploaded_files = []
|
| 227 |
+
for key, file in request.files.items():
|
| 228 |
+
if file and file.filename:
|
| 229 |
+
uploaded_files.append(f"{key}:{file.filename}")
|
| 230 |
+
if uploaded_files:
|
| 231 |
+
file_info = ", ".join(uploaded_files)
|
| 232 |
+
|
| 233 |
+
# Log successful request
|
| 234 |
+
log_api_request(
|
| 235 |
+
endpoint=endpoint_name,
|
| 236 |
+
method=request.method,
|
| 237 |
+
request_id=request_id,
|
| 238 |
+
file_info=file_info,
|
| 239 |
+
processing_time=processing_time,
|
| 240 |
+
status_code=200,
|
| 241 |
+
request_data=_get_safe_request_data(),
|
| 242 |
+
response_data=_get_safe_response_data(result)
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
return result
|
| 246 |
+
|
| 247 |
+
except Exception as e:
|
| 248 |
+
# Calculate processing time for errors too
|
| 249 |
+
processing_time = int((time.time() - start_time) * 1000)
|
| 250 |
+
|
| 251 |
+
# Log error
|
| 252 |
+
log_api_request(
|
| 253 |
+
endpoint=endpoint_name,
|
| 254 |
+
method=request.method,
|
| 255 |
+
processing_time=processing_time,
|
| 256 |
+
status_code=500,
|
| 257 |
+
error_message=str(e),
|
| 258 |
+
request_data=_get_safe_request_data()
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
# Re-raise the exception
|
| 262 |
+
raise e
|
| 263 |
+
|
| 264 |
+
return wrapper
|
| 265 |
+
return decorator
|
| 266 |
|
| 267 |
+
def _get_safe_request_data():
|
| 268 |
+
"""Safely extract request data for logging"""
|
| 269 |
+
try:
|
| 270 |
+
if request.content_type and 'application/json' in request.content_type:
|
| 271 |
+
data = request.get_json()
|
| 272 |
+
# Remove sensitive data
|
| 273 |
+
if isinstance(data, dict):
|
| 274 |
+
safe_data = {k: v for k, v in data.items() if k not in ['password', 'token', 'api_key']}
|
| 275 |
+
return safe_data
|
| 276 |
+
elif request.content_type and 'multipart/form-data' in request.content_type:
|
| 277 |
+
# Get form data but not file contents
|
| 278 |
+
safe_data = {}
|
| 279 |
+
for key, value in request.form.items():
|
| 280 |
+
safe_data[key] = value[:100] if len(str(value)) > 100 else value
|
| 281 |
+
return safe_data
|
| 282 |
+
return None
|
| 283 |
+
except:
|
| 284 |
+
return None
|
| 285 |
+
|
| 286 |
+
def _get_safe_response_data(result):
|
| 287 |
+
"""Safely extract response data for logging"""
|
| 288 |
+
try:
|
| 289 |
+
if hasattr(result, 'get_json'):
|
| 290 |
+
data = result.get_json()
|
| 291 |
+
if isinstance(data, dict):
|
| 292 |
+
# Keep only essential response fields
|
| 293 |
+
safe_data = {
|
| 294 |
+
'success': data.get('success'),
|
| 295 |
+
'request_id': data.get('request_id'),
|
| 296 |
+
'parameters_count': data.get('parameters_count'),
|
| 297 |
+
'message': data.get('message', '')[:200] # Truncate message
|
| 298 |
+
}
|
| 299 |
+
return safe_data
|
| 300 |
+
return None
|
| 301 |
+
except:
|
| 302 |
+
return None
|
| 303 |
def extract_top_level_json_array(text):
|
| 304 |
"""
|
| 305 |
function to extract JSON array from text, handling both raw JSON and code blocks
|
|
|
|
| 1851 |
</html>
|
| 1852 |
"""# UPDATED: refine endpoint to respect user requirements
|
| 1853 |
@app.route("/refine", methods=["POST"])
|
| 1854 |
+
@api_logger("/refine")
|
| 1855 |
def refine_parameters():
|
| 1856 |
"""refine endpoint that respects user intent and requirements"""
|
| 1857 |
global global_parameters
|
|
|
|
| 2053 |
return jsonify({"error": str(e)}), 500
|
| 2054 |
|
| 2055 |
@app.route("/edit", methods=["POST"])
|
| 2056 |
+
@api_logger("/edit")
|
| 2057 |
def edit_parameters():
|
| 2058 |
"""Edit endpoint that modifies existing templates precisely as commanded"""
|
| 2059 |
global global_parameters
|
|
|
|
| 2462 |
|
| 2463 |
|
| 2464 |
@app.route("/validate", methods=["POST"])
|
| 2465 |
+
@api_logger("/validate")
|
| 2466 |
def validate_template():
|
| 2467 |
"""Validate an existing template and get suggestions"""
|
| 2468 |
try:
|
|
|
|
| 2508 |
return jsonify({"error": str(e)}), 500
|
| 2509 |
|
| 2510 |
@app.route("/digitize", methods=["POST"])
|
| 2511 |
+
@api_logger("/digitize")
|
| 2512 |
def digitize_checklist():
|
| 2513 |
"""digitization that preserves original document structure without adding extras"""
|
| 2514 |
print(">> /digitize route called <<")
|
|
|
|
| 2842 |
except Exception as e:
|
| 2843 |
print(f"β Error in /template/{request_id}: {str(e)}")
|
| 2844 |
return jsonify({"error": str(e)}), 500
|
| 2845 |
+
# ADD THIS NEW ENDPOINT after the existing endpoints
|
| 2846 |
|
| 2847 |
+
@app.route("/logs", methods=["GET"])
|
| 2848 |
+
def view_logs():
|
| 2849 |
+
"""View API logs with filtering options"""
|
| 2850 |
+
|
| 2851 |
+
# Get query parameters for filtering
|
| 2852 |
+
request_id = request.args.get('request_id')
|
| 2853 |
+
endpoint = request.args.get('endpoint')
|
| 2854 |
+
limit = int(request.args.get('limit', 50))
|
| 2855 |
+
|
| 2856 |
+
if request.headers.get('Accept') == 'application/json' or request.args.get('format') == 'json':
|
| 2857 |
+
try:
|
| 2858 |
+
con = sql.connect("swift_check.db")
|
| 2859 |
+
cur = con.cursor()
|
| 2860 |
+
|
| 2861 |
+
# Build query with filters
|
| 2862 |
+
query = """
|
| 2863 |
+
SELECT
|
| 2864 |
+
l.id, l.request_id, l.endpoint, l.method, l.client_ip,
|
| 2865 |
+
l.file_info, l.processing_time_ms, l.status_code,
|
| 2866 |
+
l.error_message, l.created_at,
|
| 2867 |
+
r.product_name, r.supplier_name, r.doc_type
|
| 2868 |
+
FROM api_logs l
|
| 2869 |
+
LEFT JOIN qc_requests r ON l.request_id = r.id
|
| 2870 |
+
WHERE 1=1
|
| 2871 |
+
"""
|
| 2872 |
+
params = []
|
| 2873 |
+
|
| 2874 |
+
if request_id:
|
| 2875 |
+
query += " AND l.request_id = ?"
|
| 2876 |
+
params.append(request_id)
|
| 2877 |
+
|
| 2878 |
+
if endpoint:
|
| 2879 |
+
query += " AND l.endpoint LIKE ?"
|
| 2880 |
+
params.append(f"%{endpoint}%")
|
| 2881 |
+
|
| 2882 |
+
query += " ORDER BY l.created_at DESC LIMIT ?"
|
| 2883 |
+
params.append(limit)
|
| 2884 |
+
|
| 2885 |
+
cur.execute(query, params)
|
| 2886 |
+
rows = cur.fetchall()
|
| 2887 |
+
con.close()
|
| 2888 |
+
|
| 2889 |
+
logs = []
|
| 2890 |
+
for row in rows:
|
| 2891 |
+
logs.append({
|
| 2892 |
+
"log_id": row[0],
|
| 2893 |
+
"request_id": row[1],
|
| 2894 |
+
"endpoint": row[2],
|
| 2895 |
+
"method": row[3],
|
| 2896 |
+
"client_ip": row[4],
|
| 2897 |
+
"file_info": row[5],
|
| 2898 |
+
"processing_time_ms": row[6],
|
| 2899 |
+
"status_code": row[7],
|
| 2900 |
+
"error_message": row[8],
|
| 2901 |
+
"created_at": row[9],
|
| 2902 |
+
"product_name": row[10],
|
| 2903 |
+
"supplier_name": row[11],
|
| 2904 |
+
"doc_type": row[12]
|
| 2905 |
+
})
|
| 2906 |
+
|
| 2907 |
+
return jsonify({
|
| 2908 |
+
"success": True,
|
| 2909 |
+
"logs": logs,
|
| 2910 |
+
"total_logs": len(logs),
|
| 2911 |
+
"filters_applied": {
|
| 2912 |
+
"request_id": request_id,
|
| 2913 |
+
"endpoint": endpoint,
|
| 2914 |
+
"limit": limit
|
| 2915 |
+
}
|
| 2916 |
+
})
|
| 2917 |
+
|
| 2918 |
+
except Exception as e:
|
| 2919 |
+
return jsonify({"error": str(e)}), 500
|
| 2920 |
+
|
| 2921 |
+
# HTML view
|
| 2922 |
+
try:
|
| 2923 |
+
con = sql.connect("swift_check.db")
|
| 2924 |
+
cur = con.cursor()
|
| 2925 |
+
|
| 2926 |
+
# Get logs with request details
|
| 2927 |
+
cur.execute("""
|
| 2928 |
+
SELECT
|
| 2929 |
+
l.id, l.request_id, l.endpoint, l.method, l.client_ip,
|
| 2930 |
+
l.file_info, l.processing_time_ms, l.status_code,
|
| 2931 |
+
l.error_message, l.created_at,
|
| 2932 |
+
r.product_name, r.supplier_name, r.doc_type
|
| 2933 |
+
FROM api_logs l
|
| 2934 |
+
LEFT JOIN qc_requests r ON l.request_id = r.id
|
| 2935 |
+
ORDER BY l.created_at DESC
|
| 2936 |
+
LIMIT 100
|
| 2937 |
+
""")
|
| 2938 |
+
|
| 2939 |
+
rows = cur.fetchall()
|
| 2940 |
+
|
| 2941 |
+
# Get summary statistics
|
| 2942 |
+
cur.execute("""
|
| 2943 |
+
SELECT
|
| 2944 |
+
endpoint,
|
| 2945 |
+
COUNT(*) as total_requests,
|
| 2946 |
+
AVG(processing_time_ms) as avg_time_ms,
|
| 2947 |
+
COUNT(CASE WHEN status_code >= 400 THEN 1 END) as error_count
|
| 2948 |
+
FROM api_logs
|
| 2949 |
+
GROUP BY endpoint
|
| 2950 |
+
ORDER BY total_requests DESC
|
| 2951 |
+
""")
|
| 2952 |
+
|
| 2953 |
+
stats = cur.fetchall()
|
| 2954 |
+
con.close()
|
| 2955 |
+
|
| 2956 |
+
html = """
|
| 2957 |
+
<html>
|
| 2958 |
+
<head>
|
| 2959 |
+
<title>API Logs - Swift Check</title>
|
| 2960 |
+
<style>
|
| 2961 |
+
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f8f9fa; }
|
| 2962 |
+
.container { max-width: 1600px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
| 2963 |
+
table { border-collapse: collapse; width: 100%; margin-top: 20px; font-size: 12px; }
|
| 2964 |
+
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
| 2965 |
+
th { background-color: #007bff; color: white; font-weight: bold; position: sticky; top: 0; }
|
| 2966 |
+
tr:nth-child(even) { background-color: #f2f2f2; }
|
| 2967 |
+
tr:hover { background-color: #e3f2fd; }
|
| 2968 |
+
.success { color: #28a745; font-weight: bold; }
|
| 2969 |
+
.error { color: #dc3545; font-weight: bold; }
|
| 2970 |
+
.endpoint { padding: 3px 8px; border-radius: 12px; font-size: 10px; color: white; }
|
| 2971 |
+
.refine { background: #28a745; }
|
| 2972 |
+
.edit { background: #ffc107; color: black; }
|
| 2973 |
+
.digitize { background: #17a2b8; }
|
| 2974 |
+
.validate { background: #6c757d; }
|
| 2975 |
+
.stats-section { margin: 20px 0; padding: 15px; background: #e8f4f8; border-radius: 8px; }
|
| 2976 |
+
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; }
|
| 2977 |
+
.stat-card { background: white; padding: 15px; border-radius: 8px; border-left: 4px solid #007bff; }
|
| 2978 |
+
h1 { color: #333; text-align: center; margin-bottom: 30px; }
|
| 2979 |
+
h2 { color: #007bff; }
|
| 2980 |
+
.filter-section { margin: 20px 0; padding: 15px; background: #f8f9fa; border-radius: 8px; }
|
| 2981 |
+
input, select { margin: 5px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
|
| 2982 |
+
button { background: #007bff; color: white; padding: 8px 15px; border: none; border-radius: 4px; cursor: pointer; }
|
| 2983 |
+
.file-info { font-style: italic; color: #6c757d; }
|
| 2984 |
+
.time-ms { color: #28a745; font-weight: bold; }
|
| 2985 |
+
.error-msg { color: #dc3545; font-size: 11px; max-width: 200px; overflow: hidden; text-overflow: ellipsis; }
|
| 2986 |
+
</style>
|
| 2987 |
+
<script>
|
| 2988 |
+
function filterLogs() {
|
| 2989 |
+
const endpoint = document.getElementById('endpointFilter').value;
|
| 2990 |
+
const status = document.getElementById('statusFilter').value;
|
| 2991 |
+
|
| 2992 |
+
const rows = document.querySelectorAll('.log-row');
|
| 2993 |
+
rows.forEach(row => {
|
| 2994 |
+
const rowEndpoint = row.getAttribute('data-endpoint');
|
| 2995 |
+
const rowStatus = row.getAttribute('data-status');
|
| 2996 |
+
|
| 2997 |
+
let show = true;
|
| 2998 |
+
if (endpoint && !rowEndpoint.includes(endpoint)) show = false;
|
| 2999 |
+
if (status && rowStatus !== status) show = false;
|
| 3000 |
+
|
| 3001 |
+
row.style.display = show ? '' : 'none';
|
| 3002 |
+
});
|
| 3003 |
+
}
|
| 3004 |
+
</script>
|
| 3005 |
+
</head>
|
| 3006 |
+
<body>
|
| 3007 |
+
<div class="container">
|
| 3008 |
+
<h1>π API Logs Dashboard</h1>
|
| 3009 |
+
|
| 3010 |
+
<div class="stats-section">
|
| 3011 |
+
<h2>π Endpoint Statistics</h2>
|
| 3012 |
+
<div class="stats-grid">
|
| 3013 |
+
"""
|
| 3014 |
+
|
| 3015 |
+
for stat in stats:
|
| 3016 |
+
endpoint, total, avg_time, errors = stat
|
| 3017 |
+
success_rate = ((total - errors) / total * 100) if total > 0 else 0
|
| 3018 |
+
html += f"""
|
| 3019 |
+
<div class="stat-card">
|
| 3020 |
+
<h3>{endpoint}</h3>
|
| 3021 |
+
<p><strong>Total Requests:</strong> {total}</p>
|
| 3022 |
+
<p><strong>Avg Time:</strong> {avg_time:.1f}ms</p>
|
| 3023 |
+
<p><strong>Success Rate:</strong> {success_rate:.1f}%</p>
|
| 3024 |
+
<p><strong>Errors:</strong> {errors}</p>
|
| 3025 |
+
</div>
|
| 3026 |
+
"""
|
| 3027 |
+
|
| 3028 |
+
html += """
|
| 3029 |
+
</div>
|
| 3030 |
+
</div>
|
| 3031 |
+
|
| 3032 |
+
<div class="filter-section">
|
| 3033 |
+
<h3>π Filter Logs</h3>
|
| 3034 |
+
<select id="endpointFilter" onchange="filterLogs()">
|
| 3035 |
+
<option value="">All Endpoints</option>
|
| 3036 |
+
<option value="refine">Refine</option>
|
| 3037 |
+
<option value="edit">Edit</option>
|
| 3038 |
+
<option value="digitize">Digitize</option>
|
| 3039 |
+
<option value="validate">Validate</option>
|
| 3040 |
+
</select>
|
| 3041 |
+
|
| 3042 |
+
<select id="statusFilter" onchange="filterLogs()">
|
| 3043 |
+
<option value="">All Status</option>
|
| 3044 |
+
<option value="200">Success (200)</option>
|
| 3045 |
+
<option value="500">Error (500)</option>
|
| 3046 |
+
</select>
|
| 3047 |
+
|
| 3048 |
+
<button onclick="window.location.reload()">π Refresh</button>
|
| 3049 |
+
<button onclick="window.location.href='/logs?format=json'">π JSON Export</button>
|
| 3050 |
+
</div>
|
| 3051 |
+
|
| 3052 |
+
<table>
|
| 3053 |
+
<tr>
|
| 3054 |
+
<th>Log ID</th>
|
| 3055 |
+
<th>Request ID</th>
|
| 3056 |
+
<th>Endpoint</th>
|
| 3057 |
+
<th>Product</th>
|
| 3058 |
+
<th>Supplier</th>
|
| 3059 |
+
<th>File Info</th>
|
| 3060 |
+
<th>Time (ms)</th>
|
| 3061 |
+
<th>Status</th>
|
| 3062 |
+
<th>Client IP</th>
|
| 3063 |
+
<th>Error</th>
|
| 3064 |
+
<th>Created At</th>
|
| 3065 |
+
<th>Actions</th>
|
| 3066 |
+
</tr>
|
| 3067 |
+
"""
|
| 3068 |
+
|
| 3069 |
+
for row in rows:
|
| 3070 |
+
log_id, request_id, endpoint, method, client_ip, file_info, processing_time, status_code, error_message, created_at, product_name, supplier_name, doc_type = row
|
| 3071 |
+
|
| 3072 |
+
# Style endpoint
|
| 3073 |
+
endpoint_class = endpoint.replace('/', '').lower()
|
| 3074 |
+
endpoint_badge = f'<span class="endpoint {endpoint_class}">{endpoint}</span>'
|
| 3075 |
+
|
| 3076 |
+
# Style status
|
| 3077 |
+
status_class = "success" if status_code == 200 else "error"
|
| 3078 |
+
status_text = f'<span class="{status_class}">{status_code}</span>'
|
| 3079 |
+
|
| 3080 |
+
# Format processing time
|
| 3081 |
+
time_class = "time-ms"
|
| 3082 |
+
if processing_time and processing_time > 5000:
|
| 3083 |
+
time_class += " error"
|
| 3084 |
+
elif processing_time and processing_time > 2000:
|
| 3085 |
+
time_class += " warning"
|
| 3086 |
+
|
| 3087 |
+
time_text = f'<span class="{time_class}">{processing_time or "N/A"}</span>'
|
| 3088 |
+
|
| 3089 |
+
# Format file info
|
| 3090 |
+
file_display = f'<span class="file-info">{file_info or "No file"}</span>'
|
| 3091 |
+
|
| 3092 |
+
# Format error message
|
| 3093 |
+
error_display = f'<span class="error-msg" title="{error_message or ""}">{(error_message or "")[:50]}{"..." if error_message and len(error_message) > 50 else ""}</span>'
|
| 3094 |
+
|
| 3095 |
+
html += f"""
|
| 3096 |
+
<tr class="log-row" data-endpoint="{endpoint}" data-status="{status_code}">
|
| 3097 |
+
<td>{log_id}</td>
|
| 3098 |
+
<td>{request_id or "N/A"}</td>
|
| 3099 |
+
<td>{endpoint_badge}</td>
|
| 3100 |
+
<td><strong>{product_name or "N/A"}</strong></td>
|
| 3101 |
+
<td>{supplier_name or "N/A"}</td>
|
| 3102 |
+
<td>{file_display}</td>
|
| 3103 |
+
<td>{time_text}</td>
|
| 3104 |
+
<td>{status_text}</td>
|
| 3105 |
+
<td>{client_ip}</td>
|
| 3106 |
+
<td>{error_display}</td>
|
| 3107 |
+
<td>{created_at}</td>
|
| 3108 |
+
<td>
|
| 3109 |
+
{f'<a href="/preview/{request_id}">Preview</a> <a href="/template/{request_id}">JSON</a>' if request_id else 'N/A'}
|
| 3110 |
+
</td>
|
| 3111 |
+
</tr>
|
| 3112 |
+
"""
|
| 3113 |
+
|
| 3114 |
+
html += """
|
| 3115 |
+
</table>
|
| 3116 |
+
|
| 3117 |
+
<div style="margin-top: 20px; text-align: center;">
|
| 3118 |
+
<button onclick="window.location.href='/history'">π View Request History</button>
|
| 3119 |
+
<button onclick="window.location.href='/'">π Home</button>
|
| 3120 |
+
</div>
|
| 3121 |
+
</div>
|
| 3122 |
+
</body>
|
| 3123 |
+
</html>
|
| 3124 |
+
"""
|
| 3125 |
+
return html
|
| 3126 |
+
|
| 3127 |
+
except Exception as e:
|
| 3128 |
+
return f"<h1>Error</h1><p>{str(e)}</p>", 500
|
| 3129 |
@app.route("/preview/<int:request_id>", methods=["GET"])
|
| 3130 |
def preview_page(request_id):
|
| 3131 |
"""preview with better formatting and metadata"""
|