{% extends "layout.html" %}
{% block title %}Errors - MiniAPM{% endblock %}
{% block project_selector %}
{% if ctx.show_selector() %}
<form method="POST" action="/projects/switch" class="project-selector">
<select name="slug" onchange="this.form.submit()">
{% for project in ctx.projects %}
<option value="{{ project.slug }}" {% if ctx.is_current_project(project.id) %}selected{% endif %}>
{{ project.name }}
</option>
{% endfor %}
</select>
</form>
{% endif %}
{% endblock %}
{% block content %}
<h1>Errors</h1>
<p class="subtitle">{{ total_count }} error{% if total_count != 1 %}s{% endif %} found</p>
{% if !hourly_errors.is_empty() %}
<div class="card chart-card" style="margin-bottom: 1.5rem;">
<h3>Error Occurrences (24h)</h3>
<div class="chart-container" id="error-trend-chart">
<canvas id="errorTrendCanvas"></canvas>
</div>
</div>
<script>
(function() {
const data = [
{% for point in hourly_errors %}
{ label: "{{ point.hour }}", count: {{ point.count }} },
{% endfor %}
];
const dangerColor = getComputedStyle(document.documentElement).getPropertyValue('--danger').trim() || '#e74c3c';
const textMuted = getComputedStyle(document.documentElement).getPropertyValue('--text-muted').trim() || '#8b95a8';
const bgColor = getComputedStyle(document.documentElement).getPropertyValue('--bg').trim() || '#1a1f2e';
function drawChart() {
const canvas = document.getElementById('errorTrendCanvas');
if (!canvas || data.length === 0) return;
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;
const rect = canvas.parentElement.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
canvas.style.width = rect.width + 'px';
canvas.style.height = rect.height + 'px';
ctx.scale(dpr, dpr);
const width = rect.width;
const height = rect.height;
const padding = { top: 10, right: 10, bottom: 25, left: 35 };
const chartWidth = width - padding.left - padding.right;
const chartHeight = height - padding.top - padding.bottom;
const values = data.map(d => d.count);
const maxVal = Math.max(...values, 1);
const stepX = chartWidth / (values.length - 1 || 1);
ctx.clearRect(0, 0, width, height);
ctx.strokeStyle = bgColor;
ctx.lineWidth = 1;
for (let i = 0; i <= 4; i++) {
const y = padding.top + (chartHeight / 4) * i;
ctx.beginPath();
ctx.moveTo(padding.left, y);
ctx.lineTo(width - padding.right, y);
ctx.stroke();
}
ctx.strokeStyle = dangerColor;
ctx.lineWidth = 2;
ctx.beginPath();
values.forEach((val, i) => {
const x = padding.left + i * stepX;
const y = padding.top + chartHeight - (val / maxVal) * chartHeight;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.stroke();
ctx.fillStyle = dangerColor + '20';
ctx.beginPath();
ctx.moveTo(padding.left, padding.top + chartHeight);
values.forEach((val, i) => {
const x = padding.left + i * stepX;
const y = padding.top + chartHeight - (val / maxVal) * chartHeight;
ctx.lineTo(x, y);
});
ctx.lineTo(padding.left + (values.length - 1) * stepX, padding.top + chartHeight);
ctx.closePath();
ctx.fill();
ctx.fillStyle = textMuted;
ctx.font = '10px -apple-system, BlinkMacSystemFont, sans-serif';
ctx.textAlign = 'right';
for (let i = 0; i <= 4; i++) {
const val = Math.round(maxVal * (4 - i) / 4);
const y = padding.top + (chartHeight / 4) * i + 3;
ctx.fillText(val.toString(), padding.left - 5, y);
}
ctx.textAlign = 'center';
const labelStep = Math.ceil(data.length / 6);
data.forEach((d, i) => {
if (i % labelStep === 0 || i === data.length - 1) {
const x = padding.left + i * stepX;
const hour = d.label.split(' ')[1] || d.label;
ctx.fillText(hour, x, height - 5);
}
});
}
drawChart();
window.addEventListener('resize', drawChart);
const observer = new MutationObserver(() => setTimeout(drawChart, 50));
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
})();
</script>
{% endif %}
<div class="filter-bar">
<div class="filter-group">
<label>Status</label>
<div class="filters">
<a href="?period={{ period }}&sort={{ sort }}{% if let Some(s) = search %}&search={{ s }}{% endif %}" class="{% if status.is_none() %}active{% endif %}">All</a>
<a href="?status=open&period={{ period }}&sort={{ sort }}{% if let Some(s) = search %}&search={{ s }}{% endif %}" class="{% if status.as_deref() == Some("open") %}active{% endif %}">Open</a>
<a href="?status=resolved&period={{ period }}&sort={{ sort }}{% if let Some(s) = search %}&search={{ s }}{% endif %}" class="{% if status.as_deref() == Some("resolved") %}active{% endif %}">Resolved</a>
<a href="?status=ignored&period={{ period }}&sort={{ sort }}{% if let Some(s) = search %}&search={{ s }}{% endif %}" class="{% if status.as_deref() == Some("ignored") %}active{% endif %}">Ignored</a>
</div>
</div>
<div class="filter-group">
<label>Period</label>
<div class="filters">
<a href="?period=all&sort={{ sort }}{% if let Some(st) = status %}&status={{ st }}{% endif %}{% if let Some(s) = search %}&search={{ s }}{% endif %}" class="{% if period == "all" %}active{% endif %}">All</a>
<a href="?period=1h&sort={{ sort }}{% if let Some(st) = status %}&status={{ st }}{% endif %}{% if let Some(s) = search %}&search={{ s }}{% endif %}" class="{% if period == "1h" %}active{% endif %}">1h</a>
<a href="?period=24h&sort={{ sort }}{% if let Some(st) = status %}&status={{ st }}{% endif %}{% if let Some(s) = search %}&search={{ s }}{% endif %}" class="{% if period == "24h" %}active{% endif %}">24h</a>
<a href="?period=7d&sort={{ sort }}{% if let Some(st) = status %}&status={{ st }}{% endif %}{% if let Some(s) = search %}&search={{ s }}{% endif %}" class="{% if period == "7d" %}active{% endif %}">7d</a>
<a href="?period=30d&sort={{ sort }}{% if let Some(st) = status %}&status={{ st }}{% endif %}{% if let Some(s) = search %}&search={{ s }}{% endif %}" class="{% if period == "30d" %}active{% endif %}">30d</a>
</div>
</div>
<div class="filter-group">
<label>Sort by</label>
<div class="filters">
<a href="?period={{ period }}&sort=last_seen{% if let Some(st) = status %}&status={{ st }}{% endif %}{% if let Some(s) = search %}&search={{ s }}{% endif %}" class="{% if sort == "last_seen" %}active{% endif %}">Last Seen</a>
<a href="?period={{ period }}&sort=first_seen{% if let Some(st) = status %}&status={{ st }}{% endif %}{% if let Some(s) = search %}&search={{ s }}{% endif %}" class="{% if sort == "first_seen" %}active{% endif %}">First Seen</a>
<a href="?period={{ period }}&sort=count{% if let Some(st) = status %}&status={{ st }}{% endif %}{% if let Some(s) = search %}&search={{ s }}{% endif %}" class="{% if sort == "count" %}active{% endif %}">Count</a>
</div>
</div>
<div class="filter-group filter-search">
<label>Search</label>
<form method="GET" action="/errors" class="search-form">
<input type="hidden" name="period" value="{{ period }}">
<input type="hidden" name="sort" value="{{ sort }}">
{% if let Some(st) = status %}<input type="hidden" name="status" value="{{ st }}">{% endif %}
<input type="text" name="search" placeholder="Exception or message..." value="{% if let Some(s) = search %}{{ s }}{% endif %}">
<button type="submit">Search</button>
</form>
</div>
</div>
{% if errors.is_empty() %}
<p class="empty">No errors found</p>
{% else %}
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Status</th>
<th>Exception</th>
<th>Message</th>
<th class="num">Count</th>
<th>First Seen</th>
<th>Last Seen</th>
</tr>
</thead>
<tbody>
{% for error in errors %}
<tr>
<td><span class="badge badge-{{ error.status }}">{{ error.status }}</span></td>
<td><a href="/errors/{{ error.id }}">{{ error.exception_class }}</a></td>
<td class="truncate">{{ error.message }}</td>
<td class="num">{{ error.occurrence_count }}</td>
<td>{{ error.first_seen_at }}</td>
<td>{{ error.last_seen_at }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if total_pages > 1 %}
<div class="pagination">
{% if page > 1 %}
<a href="?page={{ page - 1 }}&period={{ period }}&sort={{ sort }}{% if let Some(st) = status %}&status={{ st }}{% endif %}{% if let Some(s) = search %}&search={{ s }}{% endif %}" class="pagination-link">Previous</a>
{% endif %}
<span class="pagination-info">Page {{ page }} of {{ total_pages }}</span>
{% if page < total_pages %}
<a href="?page={{ page + 1 }}&period={{ period }}&sort={{ sort }}{% if let Some(st) = status %}&status={{ st }}{% endif %}{% if let Some(s) = search %}&search={{ s }}{% endif %}" class="pagination-link">Next</a>
{% endif %}
</div>
{% endif %}
{% endif %}
{% endblock %}