yashgori20 commited on
Commit
0ce5100
Β·
1 Parent(s): 365a20a
Files changed (1) hide show
  1. app.py +441 -1
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"""