update
This commit is contained in:
18
templates/base.html
Normal file
18
templates/base.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Status Monitor{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1><a href="/">Status Monitor</a></h1>
|
||||
<span class="version">v{{ version }}</span>
|
||||
</header>
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
111
templates/dashboard.html
Normal file
111
templates/dashboard.html
Normal file
@@ -0,0 +1,111 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dashboard - Status Monitor{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Services</h2>
|
||||
|
||||
<form id="add-service-form" class="add-form">
|
||||
<input type="text" name="name" placeholder="Service name" required>
|
||||
<input type="text" name="target" placeholder="URL or host:port" required>
|
||||
<select name="protocol">
|
||||
<option value="https">HTTPS</option>
|
||||
<option value="http">HTTP</option>
|
||||
<option value="tcp">TCP</option>
|
||||
</select>
|
||||
<input type="number" name="interval_seconds" value="60" min="10" max="3600" title="Check interval (seconds)">
|
||||
<button type="submit">Add Service</button>
|
||||
</form>
|
||||
<script>
|
||||
document.getElementById('add-service-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const payload = {
|
||||
name: form.name.value.trim(),
|
||||
target: form.target.value.trim(),
|
||||
protocol: form.protocol.value,
|
||||
interval_seconds: parseInt(form.interval_seconds.value, 10) || 60
|
||||
};
|
||||
const res = await fetch('{{ url_for("api_add_service") }}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (res.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
alert(err.error || 'Failed to add service');
|
||||
}
|
||||
});
|
||||
document.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.btn-delete');
|
||||
if (!btn) return;
|
||||
e.preventDefault();
|
||||
if (!confirm('Delete "' + btn.dataset.name + '"?')) return;
|
||||
const res = await fetch('/api/services/' + btn.dataset.id, { method: 'DELETE' });
|
||||
if (res.ok) window.location.reload();
|
||||
else alert('Failed to delete');
|
||||
});
|
||||
</script>
|
||||
|
||||
<table class="services-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Target</th>
|
||||
<th>Protocol</th>
|
||||
<th>Status</th>
|
||||
<th>Uptime</th>
|
||||
<th>Last Check</th>
|
||||
<th>Response</th>
|
||||
<th>Report</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in services %}
|
||||
<tr>
|
||||
<td>{{ s.name }}</td>
|
||||
<td><code>{{ s.target }}</code></td>
|
||||
<td>{{ s.protocol | upper }}</td>
|
||||
<td>
|
||||
{% if s.last_success is none %}
|
||||
<span class="badge badge-pending">Pending</span>
|
||||
{% elif s.last_success %}
|
||||
<span class="badge badge-up">Up</span>
|
||||
{% else %}
|
||||
<span class="badge badge-down">Down</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if s.uptime_pct is not none %}
|
||||
<a href="{{ url_for('report', service_id=s.id) }}">{{ s.uptime_pct }}%</a>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ s.last_check[:19] if s.last_check else '-' }}</td>
|
||||
<td>
|
||||
{% if s.last_response_ms is not none %}
|
||||
{{ (s.last_response_ms | round(0) | int) }} ms
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('report', service_id=s.id) }}">Report</a>
|
||||
<a href="{{ url_for('edit_service', service_id=s.id) }}" style="margin-left: 0.5rem;">Edit</a>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn-delete" data-id="{{ s.id }}" data-name="{{ s.name }}" title="Delete">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="9">No services yet. Add one above.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
45
templates/edit.html
Normal file
45
templates/edit.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Edit {{ service.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Edit Service</h2>
|
||||
|
||||
<form id="edit-service-form" class="add-form">
|
||||
<input type="text" name="name" value="{{ service.name }}" placeholder="Service name" required>
|
||||
<input type="text" name="target" value="{{ service.target }}" placeholder="URL or host:port" required>
|
||||
<select name="protocol">
|
||||
<option value="https" {% if service.protocol == 'https' %}selected{% endif %}>HTTPS</option>
|
||||
<option value="http" {% if service.protocol == 'http' %}selected{% endif %}>HTTP</option>
|
||||
<option value="tcp" {% if service.protocol == 'tcp' %}selected{% endif %}>TCP</option>
|
||||
</select>
|
||||
<input type="number" name="interval_seconds" value="{{ service.interval_seconds }}" min="10" max="3600" title="Check interval (seconds)">
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
|
||||
<p><a href="/">← Back to Dashboard</a> | <a href="{{ url_for('report', service_id=service.id) }}">View Report</a></p>
|
||||
|
||||
<script>
|
||||
document.getElementById('edit-service-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const payload = {
|
||||
name: form.name.value.trim(),
|
||||
target: form.target.value.trim(),
|
||||
protocol: form.protocol.value,
|
||||
interval_seconds: parseInt(form.interval_seconds.value, 10) || 60
|
||||
};
|
||||
const res = await fetch('/api/services/{{ service.id }}', {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (res.ok) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
alert(err.error || 'Failed to update service');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
177
templates/report.html
Normal file
177
templates/report.html
Normal file
@@ -0,0 +1,177 @@
|
||||
{% 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">
|
||||
<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 }}">
|
||||
<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...">
|
||||
<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>
|
||||
|
||||
<p>
|
||||
<a href="/">← 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 %}
|
||||
Reference in New Issue
Block a user