Add pagination to event queries, input validation, daily event purge
- get_active_events() now takes limit/offset (default 200) to cap unbounded queries - count_active_events() added to return total for pagination display - /api/events supports ?limit=, ?offset=, ?status= query params (max 1000) - /api/status includes total_active count alongside paginated events list - index() route passes total_active to template for server-side truncation notice - Show "Showing X of Y" notice in dashboard when events are truncated - Suppression POST validates: reason ≤500 chars, target_name/detail ≤255 chars - _purge_old_jobs_loop runs purge_old_resolved_events(90d) once per day Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
51
app.py
51
app.py
@@ -47,8 +47,11 @@ _diag_jobs: dict = {}
|
||||
_diag_lock = threading.Lock()
|
||||
|
||||
|
||||
_last_event_purge = [0.0] # mutable container so the thread can update it
|
||||
|
||||
|
||||
def _purge_old_jobs_loop():
|
||||
"""Background thread: remove jobs older than 10 minutes and mark stuck running jobs as errored."""
|
||||
"""Background thread: remove stale diag jobs and run daily event purge."""
|
||||
while True:
|
||||
time.sleep(120)
|
||||
cutoff = time.time() - 600
|
||||
@@ -63,6 +66,15 @@ def _purge_old_jobs_loop():
|
||||
j['result'] = {'status': 'error', 'error': 'Diagnostic timed out (thread crash)'}
|
||||
logger.error(f'Diagnostic job {jid} appeared stuck; marked as errored')
|
||||
|
||||
# Purge old resolved events once per day
|
||||
now = time.time()
|
||||
if now - _last_event_purge[0] > 86400:
|
||||
try:
|
||||
db.purge_old_resolved_events(days=90)
|
||||
except Exception as e:
|
||||
logger.error(f'Daily event purge failed: {e}')
|
||||
_last_event_purge[0] = now
|
||||
|
||||
|
||||
_purge_thread = threading.Thread(target=_purge_old_jobs_loop, daemon=True)
|
||||
_purge_thread.start()
|
||||
@@ -120,11 +132,15 @@ def require_auth(f):
|
||||
# Page routes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_PAGE_LIMIT = 200 # max events returned per request
|
||||
|
||||
|
||||
@app.route('/')
|
||||
@require_auth
|
||||
def index():
|
||||
user = _get_user()
|
||||
events = db.get_active_events()
|
||||
events = db.get_active_events(limit=_PAGE_LIMIT)
|
||||
total_active = db.count_active_events()
|
||||
summary = db.get_status_summary()
|
||||
snapshot_raw = db.get_state('network_snapshot')
|
||||
last_check = db.get_state('last_check', 'Never')
|
||||
@@ -135,6 +151,7 @@ def index():
|
||||
'index.html',
|
||||
user=user,
|
||||
events=events,
|
||||
total_active=total_active,
|
||||
summary=summary,
|
||||
snapshot=snapshot,
|
||||
last_check=last_check,
|
||||
@@ -181,10 +198,12 @@ def suppressions_page():
|
||||
@app.route('/api/status')
|
||||
@require_auth
|
||||
def api_status():
|
||||
active = db.get_active_events(limit=_PAGE_LIMIT)
|
||||
return jsonify({
|
||||
'summary': db.get_status_summary(),
|
||||
'last_check': db.get_state('last_check', 'Never'),
|
||||
'events': db.get_active_events(),
|
||||
'events': active,
|
||||
'total_active': db.count_active_events(),
|
||||
})
|
||||
|
||||
|
||||
@@ -215,10 +234,22 @@ def api_links():
|
||||
@app.route('/api/events')
|
||||
@require_auth
|
||||
def api_events():
|
||||
return jsonify({
|
||||
'active': db.get_active_events(),
|
||||
'resolved': db.get_recent_resolved(hours=24, limit=30),
|
||||
})
|
||||
try:
|
||||
limit = min(int(request.args.get('limit', _PAGE_LIMIT)), 1000)
|
||||
offset = max(int(request.args.get('offset', 0)), 0)
|
||||
except ValueError:
|
||||
return jsonify({'error': 'limit and offset must be integers'}), 400
|
||||
status_filter = request.args.get('status', 'active')
|
||||
if status_filter not in ('active', 'resolved', 'all'):
|
||||
return jsonify({'error': 'status must be active, resolved, or all'}), 400
|
||||
|
||||
result: dict = {}
|
||||
if status_filter in ('active', 'all'):
|
||||
result['active'] = db.get_active_events(limit=limit, offset=offset)
|
||||
result['total_active'] = db.count_active_events()
|
||||
if status_filter in ('resolved', 'all'):
|
||||
result['resolved'] = db.get_recent_resolved(hours=24, limit=30)
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@app.route('/api/suppressions', methods=['GET'])
|
||||
@@ -245,6 +276,12 @@ def api_create_suppression():
|
||||
return jsonify({'error': 'target_name required'}), 400
|
||||
if not reason:
|
||||
return jsonify({'error': 'reason required'}), 400
|
||||
if len(reason) > 500:
|
||||
return jsonify({'error': 'reason must be 500 characters or fewer'}), 400
|
||||
if len(target_name) > 255:
|
||||
return jsonify({'error': 'target_name must be 255 characters or fewer'}), 400
|
||||
if len(target_detail) > 255:
|
||||
return jsonify({'error': 'target_detail must be 255 characters or fewer'}), 400
|
||||
|
||||
sup_id = db.create_suppression(
|
||||
target_type=target_type,
|
||||
|
||||
Reference in New Issue
Block a user