Files
jenkins-docker-deploy-example/templates/report.html
2026-03-10 14:20:50 +00:00

228 lines
10 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ service.name }} - Report{% endblock %}
{% block content %}
<h2>{{ service.name }}</h2>
<p>
<code>{{ service.protocol | upper }}: {{ service.target }}</code>
<a href="{{ url_for('edit_service', service_id=service.id) }}" style="margin-left: 1rem;">Edit</a>
</p>
<div class="period-section">
<h3>Uptime by Period</h3>
<div class="preset-buttons">
<a href="{{ url_for('report', service_id=service.id) }}" class="preset-btn{% if not preset %} preset-active{% endif %}">All time</a>
<a href="{{ url_for('report', service_id=service.id, preset='24h') }}" class="preset-btn{% if preset == '24h' %} preset-active{% endif %}">Last 24h</a>
<a href="{{ url_for('report', service_id=service.id, preset='7d') }}" class="preset-btn{% if preset == '7d' %} preset-active{% endif %}">Last 7 days</a>
<a href="{{ url_for('report', service_id=service.id, preset='30d') }}" class="preset-btn{% if preset == '30d' %} preset-active{% endif %}">Last 30 days</a>
</div>
{% if period_label %}
<p class="period-label">Showing: {{ period_label }}</p>
{% endif %}
<form method="get" action="{{ url_for('report', service_id=service.id) }}" class="date-range-form">
<input type="hidden" name="preset" value="{{ preset or '' }}">
<input type="hidden" name="status" value="{{ status_filter or '' }}">
<input type="hidden" name="search" value="{{ search or '' }}">
<input type="hidden" name="per_page" value="{{ per_page }}">
<label>From</label>
<input type="datetime-local" name="from" value="{{ from_date }}" placeholder="Start (optional)">
<label>To</label>
<input type="datetime-local" name="to" value="{{ to_date }}" placeholder="End (optional)">
<button type="submit">Apply</button>
{% if from_date or to_date %}
<a href="{{ url_for('report', service_id=service.id) }}">Clear</a>
{% endif %}
</form>
</div>
<div class="stats-grid">
<div class="stat-card">
<span class="stat-label">Uptime{% if from_date or to_date %} (period){% endif %}</span>
<span class="stat-value">{{ stats.uptime_pct }}%</span>
</div>
<div class="stat-card">
<span class="stat-label">Checks</span>
<span class="stat-value">{{ stats.total }}</span>
</div>
<div class="stat-card">
<span class="stat-label">Avg Latency</span>
<span class="stat-value">{{ stats.avg_ms or '-' }}{% if stats.avg_ms %} ms{% endif %}</span>
</div>
<div class="stat-card">
<span class="stat-label">Min</span>
<span class="stat-value">{{ (stats.min_ms | round(0) | int) if stats.min_ms is not none else '-' }}{% if stats.min_ms is not none %} ms{% endif %}</span>
</div>
<div class="stat-card">
<span class="stat-label">Max</span>
<span class="stat-value">{{ (stats.max_ms | round(0) | int) if stats.max_ms is not none else '-' }}{% if stats.max_ms is not none %} ms{% endif %}</span>
</div>
</div>
<div class="chart-container">
<h3>Response Time</h3>
{% if chart_checks %}
<div class="chart-wrapper">
<canvas id="response-chart"></canvas>
</div>
{% else %}
<p class="chart-empty">No check data yet. Checks will appear after the first run.</p>
{% endif %}
</div>
<h3>Recent Checks</h3>
<form method="get" action="{{ url_for('report', service_id=service.id) }}" class="checks-filter-form">
<input type="hidden" name="preset" value="{{ preset or '' }}">
<input type="hidden" name="from" value="{{ from_date }}">
<input type="hidden" name="to" value="{{ to_date }}">
<input type="hidden" name="page" value="1">
<select name="status">
<option value="">All</option>
<option value="ok" {% if status_filter == 'ok' %}selected{% endif %}>OK only</option>
<option value="error" {% if status_filter == 'error' %}selected{% endif %}>Errors only</option>
</select>
<input type="text" name="search" value="{{ search }}" placeholder="Search error message...">
<select name="per_page">
<option value="10" {% if per_page == 10 %}selected{% endif %}>10 per page</option>
<option value="25" {% if per_page == 25 %}selected{% endif %}>25 per page</option>
<option value="50" {% if per_page == 50 %}selected{% endif %}>50 per page</option>
<option value="100" {% if per_page == 100 %}selected{% endif %}>100 per page</option>
</select>
<button type="submit">Filter</button>
</form>
<table class="checks-table">
<thead>
<tr>
<th>Timestamp</th>
<th>Status</th>
<th>Response (ms)</th>
<th>Error / Reason</th>
</tr>
</thead>
<tbody>
{% for c in checks %}
<tr>
<td>{{ c.timestamp[:19] }}</td>
<td>
{% if c.success %}
<span class="badge badge-up">OK</span>
{% else %}
<span class="badge badge-down">ERROR</span>
{% endif %}
</td>
<td>{{ (c.response_time_ms | round(0) | int) if c.response_time_ms is not none else '-' }}</td>
<td class="error-cell" {% if c.error_message %}title="{{ c.error_message | e }}"{% endif %}>{{ c.error_message or '-' }}</td>
</tr>
{% else %}
<tr>
<td colspan="4">No checks yet.{% if status_filter or search %} No matches for filter.{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if checks_total > 0 %}
<nav class="pagination">
<span class="pagination-info">
Showing {{ (page - 1) * per_page + 1 }}-{{ [page * per_page, checks_total] | min }} of {{ checks_total }}
</span>
<div class="pagination-links">
{% if page > 1 %}
<a href="{{ url_for('report', service_id=service.id, preset=preset or '', from=from_date, to=to_date, status=status_filter or '', search=search or '', per_page=per_page, page=page-1) }}" class="pagination-btn">Previous</a>
{% endif %}
{% if total_pages <= 7 %}
{% for p in range(1, total_pages + 1) %}
{% if p == page %}
<span class="pagination-btn pagination-current">{{ p }}</span>
{% else %}
<a href="{{ url_for('report', service_id=service.id, preset=preset or '', from=from_date, to=to_date, status=status_filter or '', search=search or '', per_page=per_page, page=p) }}" class="pagination-btn">{{ p }}</a>
{% endif %}
{% endfor %}
{% else %}
<a href="{{ url_for('report', service_id=service.id, preset=preset or '', from=from_date, to=to_date, status=status_filter or '', search=search or '', per_page=per_page, page=1) }}" class="pagination-btn">1</a>
{% if page > 3 %}<span class="pagination-ellipsis"></span>{% endif %}
{% for p in range(max(2, page - 1), min(total_pages, page + 1) + 1) %}
{% if p == page %}
<span class="pagination-btn pagination-current">{{ p }}</span>
{% else %}
<a href="{{ url_for('report', service_id=service.id, preset=preset or '', from=from_date, to=to_date, status=status_filter or '', search=search or '', per_page=per_page, page=p) }}" class="pagination-btn">{{ p }}</a>
{% endif %}
{% endfor %}
{% if page < total_pages - 2 %}<span class="pagination-ellipsis"></span>{% endif %}
{% if total_pages > 1 %}
<a href="{{ url_for('report', service_id=service.id, preset=preset or '', from=from_date, to=to_date, status=status_filter or '', search=search or '', per_page=per_page, page=total_pages) }}" class="pagination-btn">{{ total_pages }}</a>
{% endif %}
{% endif %}
{% if page < total_pages %}
<a href="{{ url_for('report', service_id=service.id, preset=preset or '', from=from_date, to=to_date, status=status_filter or '', search=search or '', per_page=per_page, page=page+1) }}" class="pagination-btn">Next</a>
{% endif %}
</div>
</nav>
{% endif %}
<p>
<a href="/">&larr; Back to Dashboard</a>
<span style="margin-left: 1rem;">
<button type="button" class="btn-delete" data-id="{{ service.id }}" data-name="{{ service.name }}">Delete Service</button>
</span>
</p>
{% if chart_checks %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
(function() {
const canvas = document.getElementById('response-chart');
if (!canvas) return;
const chartData = {{ chart_checks | tojson }};
if (chartData.length === 0) return;
const reversed = chartData.slice().reverse();
const labels = reversed.map(c => c.timestamp ? c.timestamp.slice(0, 19).replace('T', ' ') : '');
const values = reversed.map(c => c.response_time_ms != null ? c.response_time_ms : null);
const successColors = reversed.map(c => c.success ? 'rgba(34, 197, 94, 0.8)' : 'rgba(239, 68, 68, 0.8)');
new Chart(canvas, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Response (ms)',
data: values,
borderColor: 'rgba(59, 130, 246, 0.9)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
fill: true,
tension: 0.2,
pointBackgroundColor: successColors,
pointBorderColor: successColors,
pointRadius: 3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
x: {
ticks: { maxTicksLimit: 8, maxRotation: 45 },
grid: { color: 'rgba(148, 163, 184, 0.2)' }
},
y: {
beginAtZero: true,
ticks: { callback: v => v + ' ms' },
grid: { color: 'rgba(148, 163, 184, 0.2)' }
}
}
}
});
})();
</script>
{% endif %}
<script>
document.querySelector('.btn-delete')?.addEventListener('click', async () => {
if (!confirm('Delete "{{ service.name }}"?')) return;
const res = await fetch('/api/services/{{ service.id }}', { method: 'DELETE' });
if (res.ok) window.location.href = '/';
else alert('Failed to delete');
});
</script>
{% endblock %}