refactor: remove debug panel entirely
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

Remove in-memory log viewer (DebugLogHandler, 5 routes, template,
menu links, endpoint aliases). Logs available via journalctl on server.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-11 04:32:12 +01:00
parent 954cd02975
commit d8ee8fe7e4
7 changed files with 1 additions and 585 deletions

28
app.py
View File

@ -22,7 +22,6 @@ import secrets
import re
import json
import time
from collections import deque
from pathlib import Path
from datetime import datetime, timedelta, date
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash, session, Response, send_file
@ -64,37 +63,11 @@ STAGING_TEST_FEATURES = {
},
}
# Configure logging with in-memory buffer for debug panel
class DebugLogHandler(logging.Handler):
"""Custom handler that stores logs in memory for real-time viewing"""
def __init__(self, max_logs=500):
super().__init__()
self.logs = deque(maxlen=max_logs)
def emit(self, record):
log_entry = {
'timestamp': datetime.now().isoformat(),
'level': record.levelname,
'logger': record.name,
'message': self.format(record),
'module': record.module,
'funcName': record.funcName,
'lineno': record.lineno
}
self.logs.append(log_entry)
# Create debug handler
debug_handler = DebugLogHandler(max_logs=500)
debug_handler.setFormatter(logging.Formatter('%(message)s'))
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# Add debug handler to root logger
logging.getLogger().addHandler(debug_handler)
logger = logging.getLogger(__name__)
# Security logger for fail2ban integration
@ -1011,7 +984,6 @@ def health_full():
('/admin/analytics', 'Admin: Analityka'),
('/admin/status', 'Admin: Status systemu'),
('/admin/health', 'Admin: Health dashboard'),
('/admin/debug', 'Admin: Debug'),
('/admin/ai-usage', 'Admin: AI Usage'),
('/admin/chat-analytics', 'Admin: Chat analytics'),
('/admin/users', 'Admin: Użytkownicy'),

View File

@ -326,10 +326,9 @@ def register_blueprints(app):
# SEO & Audits (Phase 6.2a)
'admin_seo': 'admin.admin_seo',
'admin_gbp_audit': 'admin.admin_gbp_audit',
# Status, Health, Debug (Phase 6.2c)
# Status, Health (Phase 6.2c)
'admin_status': 'admin.admin_status',
'admin_health': 'admin.admin_health',
'debug_panel': 'admin.debug_panel',
# Social Media (Phase 6.2e)
'admin_social_media': 'admin.admin_social_media',
'admin_social_audit': 'admin.admin_social_audit',

View File

@ -786,94 +786,3 @@ def api_admin_health():
'health_percent': round(100 * ok_count / len(results), 1)
}
})
# ============================================================
# DEBUG PANEL
# ============================================================
@bp.route('/debug')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def debug_panel():
"""Real-time debug panel for monitoring app activity"""
return render_template('admin/debug.html')
@bp.route('/api/logs')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_get_logs():
"""API: Get recent logs"""
# Import debug_handler from main app
from app import debug_handler
# Get optional filters
level = request.args.get('level', '') # DEBUG, INFO, WARNING, ERROR
since = request.args.get('since', '') # ISO timestamp
limit = min(int(request.args.get('limit', 100)), 500)
logs = list(debug_handler.logs)
# Filter by level
if level:
logs = [l for l in logs if l['level'] == level.upper()]
# Filter by timestamp
if since:
logs = [l for l in logs if l['timestamp'] > since]
# Return most recent
logs = logs[-limit:]
return jsonify({
'success': True,
'logs': logs,
'total': len(debug_handler.logs)
})
@bp.route('/api/logs/stream')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_logs_stream():
"""SSE endpoint for real-time log streaming"""
from app import debug_handler
import time
def generate():
last_count = 0
while True:
current_count = len(debug_handler.logs)
if current_count > last_count:
# Send new logs
new_logs = list(debug_handler.logs)[last_count:]
for log in new_logs:
yield f"data: {json.dumps(log)}\n\n"
last_count = current_count
time.sleep(0.5)
return Response(generate(), mimetype='text/event-stream')
@bp.route('/api/logs/clear', methods=['POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_clear_logs():
"""API: Clear log buffer"""
from app import debug_handler
debug_handler.logs.clear()
logger.info("Log buffer cleared by admin")
return jsonify({'success': True})
@bp.route('/api/test-log', methods=['POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def api_test_log():
"""API: Generate test log entries"""
logger.debug("Test DEBUG message")
logger.info("Test INFO message")
logger.warning("Test WARNING message")
logger.error("Test ERROR message")
return jsonify({'success': True, 'message': 'Test logs generated'})

View File

@ -1,448 +0,0 @@
{% extends "base.html" %}
{% block title %}Debug Panel - Norda Biznes Partner{% endblock %}
{% block extra_css %}
<style>
.debug-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-lg);
}
.debug-header h1 {
font-size: var(--font-size-2xl);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--error);
animation: pulse 2s infinite;
}
.status-indicator.connected {
background: var(--success);
}
.debug-controls {
display: flex;
gap: var(--spacing-md);
align-items: center;
}
.debug-controls select,
.debug-controls button {
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: var(--font-size-sm);
background: var(--surface);
cursor: pointer;
}
.debug-controls button:hover {
background: var(--background);
}
.btn-danger {
background: var(--error) !important;
color: white !important;
border-color: var(--error) !important;
}
.btn-danger:hover {
opacity: 0.9;
}
.log-container {
background: #1e1e1e;
border-radius: var(--radius-lg);
padding: var(--spacing-md);
height: calc(100vh - 300px);
min-height: 400px;
overflow-y: auto;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
line-height: 1.5;
}
.log-entry {
padding: 4px 8px;
border-radius: 4px;
margin-bottom: 2px;
display: flex;
gap: var(--spacing-md);
animation: fadeIn 0.2s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-5px); }
to { opacity: 1; transform: translateY(0); }
}
.log-entry.DEBUG { color: #9cdcfe; }
.log-entry.INFO { color: #4ec9b0; }
.log-entry.WARNING { color: #dcdcaa; background: rgba(220, 220, 170, 0.1); }
.log-entry.ERROR { color: #f48771; background: rgba(244, 135, 113, 0.1); }
.log-timestamp {
color: #6a9955;
flex-shrink: 0;
width: 85px;
}
.log-level {
flex-shrink: 0;
width: 60px;
font-weight: bold;
}
.log-logger {
color: #569cd6;
flex-shrink: 0;
width: 150px;
overflow: hidden;
text-overflow: ellipsis;
}
.log-message {
flex: 1;
word-break: break-word;
}
.log-location {
color: #808080;
font-size: 11px;
flex-shrink: 0;
}
.stats-bar {
display: flex;
gap: var(--spacing-xl);
margin-bottom: var(--spacing-lg);
padding: var(--spacing-md);
background: var(--surface);
border-radius: var(--radius);
box-shadow: var(--shadow-sm);
}
.stat-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.stat-item .count {
font-weight: 700;
font-size: var(--font-size-lg);
}
.stat-item.info .count { color: var(--primary); }
.stat-item.warning .count { color: var(--warning); }
.stat-item.error .count { color: var(--error); }
.empty-logs {
color: #6a9955;
text-align: center;
padding: var(--spacing-2xl);
}
.auto-scroll-label {
display: flex;
align-items: center;
gap: var(--spacing-xs);
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
</style>
{% endblock %}
{% block content %}
<div class="debug-header">
<h1>
<span class="status-indicator" id="statusIndicator"></span>
Debug Panel
</h1>
<div class="debug-controls">
<label class="auto-scroll-label">
<input type="checkbox" id="autoScroll" checked>
Auto-scroll
</label>
<select id="levelFilter">
<option value="">Wszystkie poziomy</option>
<option value="DEBUG">DEBUG</option>
<option value="INFO">INFO</option>
<option value="WARNING">WARNING</option>
<option value="ERROR">ERROR</option>
</select>
<button onclick="testLogs()">Test Logs</button>
<button onclick="clearLogs()" class="btn-danger">Wyczyść</button>
</div>
</div>
<div class="stats-bar">
<div class="stat-item info">
<span class="count" id="infoCount">0</span>
<span>INFO</span>
</div>
<div class="stat-item warning">
<span class="count" id="warningCount">0</span>
<span>WARNING</span>
</div>
<div class="stat-item error">
<span class="count" id="errorCount">0</span>
<span>ERROR</span>
</div>
<div class="stat-item">
<span class="count" id="totalCount">0</span>
<span>Total</span>
</div>
</div>
<div class="log-container" id="logContainer">
<div class="empty-logs" id="emptyMessage">
Oczekiwanie na logi... Wykonaj jakąś akcję na stronie.
</div>
</div>
<!-- Universal Confirm Modal -->
<div class="modal-overlay" id="confirmModal">
<div class="modal" style="max-width: 420px; background: var(--surface); border-radius: var(--radius-lg); padding: var(--spacing-xl);">
<div style="text-align: center; margin-bottom: var(--spacing-lg);">
<div class="modal-icon" id="confirmModalIcon" style="font-size: 3em; margin-bottom: var(--spacing-md);"></div>
<h3 id="confirmModalTitle" style="margin-bottom: var(--spacing-sm);">Potwierdzenie</h3>
<p class="modal-description" id="confirmModalMessage" style="color: var(--text-secondary);"></p>
</div>
<div class="modal-actions" style="display: flex; gap: var(--spacing-sm); justify-content: center;">
<button type="button" class="btn btn-secondary" id="confirmModalCancel">Anuluj</button>
<button type="button" class="btn btn-primary" id="confirmModalOk">OK</button>
</div>
</div>
</div>
<style>
.modal-overlay#confirmModal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1050; align-items: center; justify-content: center; }
.modal-overlay#confirmModal.active { display: flex; }
</style>
{% endblock %}
{% block extra_js %}
let confirmResolve = null;
function showConfirm(message, options = {}) {
return new Promise(resolve => {
confirmResolve = resolve;
document.getElementById('confirmModalIcon').textContent = options.icon || '❓';
document.getElementById('confirmModalTitle').textContent = options.title || 'Potwierdzenie';
document.getElementById('confirmModalMessage').innerHTML = message;
document.getElementById('confirmModalOk').textContent = options.okText || 'OK';
document.getElementById('confirmModalOk').className = 'btn ' + (options.okClass || 'btn-primary');
document.getElementById('confirmModal').classList.add('active');
});
}
function closeConfirm(result) {
document.getElementById('confirmModal').classList.remove('active');
if (confirmResolve) { confirmResolve(result); confirmResolve = null; }
}
document.getElementById('confirmModalOk').addEventListener('click', () => closeConfirm(true));
document.getElementById('confirmModalCancel').addEventListener('click', () => closeConfirm(false));
document.getElementById('confirmModal').addEventListener('click', e => { if (e.target.id === 'confirmModal') closeConfirm(false); });
const logContainer = document.getElementById('logContainer');
const emptyMessage = document.getElementById('emptyMessage');
const statusIndicator = document.getElementById('statusIndicator');
const levelFilter = document.getElementById('levelFilter');
const autoScrollCheckbox = document.getElementById('autoScroll');
let stats = { DEBUG: 0, INFO: 0, WARNING: 0, ERROR: 0 };
let allLogs = [];
let eventSource = null;
// Format timestamp
function formatTime(isoString) {
const date = new Date(isoString);
return date.toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
// Add log entry to UI
function addLogEntry(log) {
allLogs.push(log);
stats[log.level] = (stats[log.level] || 0) + 1;
updateStats();
// Check filter
const filter = levelFilter.value;
if (filter && log.level !== filter) return;
emptyMessage.style.display = 'none';
const entry = document.createElement('div');
entry.className = `log-entry ${log.level}`;
entry.innerHTML = `
<span class="log-timestamp">${formatTime(log.timestamp)}</span>
<span class="log-level">${log.level}</span>
<span class="log-logger">${log.logger}</span>
<span class="log-message">${escapeHtml(log.message)}</span>
<span class="log-location">${log.module}:${log.lineno}</span>
`;
logContainer.appendChild(entry);
if (autoScrollCheckbox.checked) {
logContainer.scrollTop = logContainer.scrollHeight;
}
}
// Escape HTML
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Update stats
function updateStats() {
document.getElementById('infoCount').textContent = stats.INFO || 0;
document.getElementById('warningCount').textContent = stats.WARNING || 0;
document.getElementById('errorCount').textContent = stats.ERROR || 0;
document.getElementById('totalCount').textContent = allLogs.length;
}
// Poll for new logs (SSE doesn't work well with session auth)
let lastLogTimestamp = '';
let pollInterval = null;
function startPolling() {
statusIndicator.classList.add('connected');
pollInterval = setInterval(async () => {
try {
const url = lastLogTimestamp
? `/api/admin/logs?limit=50&since=${encodeURIComponent(lastLogTimestamp)}`
: '/api/admin/logs?limit=50';
const response = await fetch(url, { credentials: 'include' });
const data = await response.json();
if (data.success && data.logs.length > 0) {
data.logs.forEach(log => {
addLogEntry(log);
lastLogTimestamp = log.timestamp;
});
}
} catch (error) {
console.error('Polling error:', error);
statusIndicator.classList.remove('connected');
}
}, 1000); // Poll every second
}
function stopPolling() {
if (pollInterval) {
clearInterval(pollInterval);
pollInterval = null;
}
statusIndicator.classList.remove('connected');
}
// Load initial logs
async function loadInitialLogs() {
try {
const response = await fetch('/api/admin/logs?limit=200', {
credentials: 'include'
});
const data = await response.json();
if (data.success && data.logs.length > 0) {
data.logs.forEach(log => addLogEntry(log));
}
} catch (error) {
console.error('Failed to load logs:', error);
}
}
// Filter logs
levelFilter.addEventListener('change', function() {
// Clear container
logContainer.innerHTML = '';
const filter = this.value;
const filtered = filter ? allLogs.filter(l => l.level === filter) : allLogs;
if (filtered.length === 0) {
logContainer.innerHTML = '<div class="empty-logs">Brak logów dla wybranego filtru</div>';
} else {
filtered.forEach(log => {
const entry = document.createElement('div');
entry.className = `log-entry ${log.level}`;
entry.innerHTML = `
<span class="log-timestamp">${formatTime(log.timestamp)}</span>
<span class="log-level">${log.level}</span>
<span class="log-logger">${log.logger}</span>
<span class="log-message">${escapeHtml(log.message)}</span>
<span class="log-location">${log.module}:${log.lineno}</span>
`;
logContainer.appendChild(entry);
});
}
});
// Clear logs
async function clearLogs() {
const confirmed = await showConfirm('Czy na pewno chcesz wyczyścić wszystkie logi?', {
icon: '🗑️',
title: 'Czyszczenie logów',
okText: 'Wyczyść',
okClass: 'btn-danger'
});
if (!confirmed) return;
try {
const response = await fetch('/api/admin/logs/clear', {
method: 'POST',
credentials: 'include',
headers: {
'X-CSRFToken': '{{ csrf_token() }}'
}
});
if (response.ok) {
allLogs = [];
stats = { DEBUG: 0, INFO: 0, WARNING: 0, ERROR: 0 };
updateStats();
logContainer.innerHTML = '<div class="empty-logs" id="emptyMessage">Logi wyczyszczone</div>';
}
} catch (error) {
console.error('Failed to clear logs:', error);
}
}
// Test logs
async function testLogs() {
try {
await fetch('/api/admin/test-log', {
method: 'POST',
credentials: 'include',
headers: {
'X-CSRFToken': '{{ csrf_token() }}'
}
});
} catch (error) {
console.error('Failed to generate test logs:', error);
}
}
// Initialize
loadInitialLogs();
startPolling();
// Cleanup on page unload
window.addEventListener('beforeunload', stopPolling);
{% endblock %}

View File

@ -634,7 +634,6 @@
</div>
<div class="quick-actions">
<a href="{{ url_for('admin.debug_panel') }}" class="quick-action-btn">🐛 Debug panel</a>
<a href="{{ url_for('admin.user_insights') }}" class="quick-action-btn">📈 Analityka</a>
</div>
</div>

View File

@ -1819,14 +1819,6 @@
</svg>
Monitoring AI
</a>
{% if current_user.has_role(SystemRole.ADMIN) %}
<a href="{{ url_for('debug_panel') }}">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
</svg>
Debug Panel
</a>
{% endif %}
</div>
</div>
</div>

View File

@ -948,13 +948,6 @@
<span class="btn btn-sm btn-success">Statystyki</span>
</a>
<a href="{{ url_for('debug_panel') }}" class="admin-function-card">
<svg fill="none" stroke="#ef4444" stroke-width="2" viewBox="0 0 24 24">
<path d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
</svg>
<h6>Debug Panel</h6>
<span class="btn btn-sm btn-danger">Logi</span>
</a>
</div>
</div>
{% endif %}