- Add /admin/ai-usage/user/<id> route for detailed AI usage per user - Add ai_usage_user.html template with stats, usage breakdown, logs - Make user names clickable in AI usage dashboard ranking - Replace all native browser dialogs (alert, confirm) with styled modals/toasts: - admin/fees.html, forum.html, recommendations.html, announcements.html, debug.html - calendar/admin.html, event.html - company_detail.html, company/recommend.html - forum/new_topic.html, topic.html - classifieds/view.html - auth/reset_password.html Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
449 lines
14 KiB
HTML
Executable File
449 lines
14 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>
|
|
|
|
<!-- 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 %}
|