nordabiz/templates/admin/debug.html
Maciej Pienczyn 6e4e7c2240 Sync: Current production state
- Added CompanyRecommendation system
- Made company pages public (removed @login_required)
- CSS refactor: inline styles instead of external fluent CSS
- Added release notes page
- Added admin recommendations panel
- Company logos (webp format)
- Docker compose configuration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 12:26:22 +01:00

401 lines
11 KiB
HTML
Executable File

{% extends "base.html" %}
{% block title %}Debug Panel - Norda Biznes Hub{% 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>
{% endblock %}
{% block extra_js %}
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() {
if (!confirm('Wyczyścić wszystkie logi?')) 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 %}