FastAPI/Jinja2 web app for viewing and rebooting TP-Link Omada APs across all sites. Authentik OIDC auth, SQLite audit log, Docker deploy. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
133 lines
6.6 KiB
HTML
133 lines
6.6 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}Audit Log – Salus by Stranto{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="space-y-6">
|
||
|
||
<!-- Header -->
|
||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||
<div>
|
||
<h1 class="text-2xl font-bold text-gray-900">Audit Log</h1>
|
||
<p class="text-sm text-gray-500 mt-0.5">All reboot actions · {{ logs|length }} record{{ 's' if logs|length != 1 }}</p>
|
||
</div>
|
||
<a href="/audit/export"
|
||
class="inline-flex items-center gap-2 px-3 py-2 text-sm rounded-lg bg-blue-600 hover:bg-blue-500 border border-blue-500 text-white transition-colors self-start sm:self-auto">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
|
||
</svg>
|
||
Export CSV
|
||
</a>
|
||
</div>
|
||
|
||
<!-- Filters -->
|
||
<form method="get" class="flex flex-col sm:flex-row gap-3">
|
||
<div class="relative flex-1">
|
||
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none"
|
||
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||
</svg>
|
||
<input type="text" name="username" value="{{ filter_username }}"
|
||
placeholder="Filter by username…"
|
||
class="w-full pl-9 pr-3 py-2 text-sm rounded-lg bg-white border border-gray-300 text-gray-900 placeholder-gray-400
|
||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
|
||
</div>
|
||
<div class="relative flex-1">
|
||
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none"
|
||
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||
d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.14 0"/>
|
||
</svg>
|
||
<input type="text" name="ap_name" value="{{ filter_ap }}"
|
||
placeholder="Filter by AP name…"
|
||
class="w-full pl-9 pr-3 py-2 text-sm rounded-lg bg-white border border-gray-300 text-gray-900 placeholder-gray-400
|
||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
|
||
</div>
|
||
<div class="flex gap-2">
|
||
<button type="submit"
|
||
class="px-4 py-2 text-sm rounded-lg bg-white hover:bg-gray-50 border border-gray-300 text-gray-700 transition-colors">
|
||
Filter
|
||
</button>
|
||
{% if filter_username or filter_ap %}
|
||
<a href="/audit"
|
||
class="px-4 py-2 text-sm rounded-lg bg-gray-50 hover:bg-gray-100 border border-gray-200 text-gray-500 transition-colors">
|
||
Clear
|
||
</a>
|
||
{% endif %}
|
||
</div>
|
||
</form>
|
||
|
||
{% if logs %}
|
||
<!-- Table -->
|
||
<div class="overflow-x-auto rounded-xl border border-gray-200 shadow-sm">
|
||
<table class="min-w-full divide-y divide-gray-200 text-sm">
|
||
<thead class="bg-gray-50">
|
||
<tr>
|
||
<th class="px-4 py-3 text-left font-semibold text-gray-500 uppercase tracking-wide text-xs">Timestamp (UTC)</th>
|
||
<th class="px-4 py-3 text-left font-semibold text-gray-500 uppercase tracking-wide text-xs">User</th>
|
||
<th class="px-4 py-3 text-left font-semibold text-gray-500 uppercase tracking-wide text-xs">AP Name</th>
|
||
<th class="px-4 py-3 text-left font-semibold text-gray-500 uppercase tracking-wide text-xs">MAC</th>
|
||
<th class="px-4 py-3 text-left font-semibold text-gray-500 uppercase tracking-wide text-xs">IP</th>
|
||
<th class="px-4 py-3 text-left font-semibold text-gray-500 uppercase tracking-wide text-xs">Result</th>
|
||
<th class="px-4 py-3 text-left font-semibold text-gray-500 uppercase tracking-wide text-xs">Details</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-gray-200 bg-white">
|
||
{% for log in logs %}
|
||
<tr class="hover:bg-gray-50 transition-colors">
|
||
<td class="px-4 py-3 text-gray-500 font-mono text-xs whitespace-nowrap">
|
||
{{ log.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}
|
||
</td>
|
||
<td class="px-4 py-3">
|
||
<div class="text-gray-900 text-sm font-medium">{{ log.username }}</div>
|
||
<div class="text-gray-400 text-xs">{{ log.user_email }}</div>
|
||
</td>
|
||
<td class="px-4 py-3 text-gray-900 font-medium">{{ log.ap_name }}</td>
|
||
<td class="px-4 py-3 text-gray-500 font-mono text-xs">{{ log.ap_mac }}</td>
|
||
<td class="px-4 py-3 text-gray-500 font-mono text-xs">{{ log.ap_ip or '—' }}</td>
|
||
<td class="px-4 py-3">
|
||
{% if log.result == 'success' %}
|
||
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-green-50 text-green-700 border border-green-200">
|
||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"/>
|
||
</svg>
|
||
Success
|
||
</span>
|
||
{% else %}
|
||
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-red-50 text-red-700 border border-red-200">
|
||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12"/>
|
||
</svg>
|
||
Error
|
||
</span>
|
||
{% endif %}
|
||
</td>
|
||
<td class="px-4 py-3 text-gray-400 text-xs max-w-xs truncate" title="{{ log.error_message or '' }}">
|
||
{{ log.error_message or '—' }}
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{% else %}
|
||
<div class="flex flex-col items-center justify-center py-20 text-gray-400">
|
||
<svg class="w-12 h-12 mb-4 opacity-40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||
</svg>
|
||
<p class="text-lg font-medium">No log entries found</p>
|
||
{% if filter_username or filter_ap %}
|
||
<p class="text-sm mt-1">Try adjusting your filters.</p>
|
||
{% else %}
|
||
<p class="text-sm mt-1">Reboot actions will appear here.</p>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
</div>
|
||
{% endblock %}
|