{% extends "base.html" %} {% block title %}Audit Log{% endblock %} {% block breadcrumb %}Audit Log{% endblock %} {% block content %} {# ═══════ HEADER ═══════ #}

Audit Log

Complete activity trail — every action, login, and data change across all {{ total }} entries.

{# ═══════ STATS ═══════ #} {% set auth_actions = ['login','logout','login_failed'] %} {% set create_actions = ['create','admin_user_create','volume_create','network_create'] %} {% set update_actions = ['update','admin_user_update','settings_change','permission_change','profile_update','avatar_upload','password_change','alert_config'] %} {% set delete_actions = ['delete','admin_user_delete','file_delete'] %} {% set infra_actions = ['container_action','container_exec','container_export','docker_prune','docker_build','image_action','image_tag','compose_action','volume_action','network_action'] %} {% set storage_actions = ['file_upload','file_delete','file_download'] %} {% set auth_count = entries|selectattr('action','in',auth_actions)|list|length %} {% set create_count = entries|selectattr('action','in',create_actions)|list|length %} {% set update_count = entries|selectattr('action','in',update_actions)|list|length %} {% set delete_count = entries|selectattr('action','in',delete_actions)|list|length %} {% set infra_count = entries|selectattr('action','in',infra_actions)|list|length %} {% set storage_count = entries|selectattr('action','in',storage_actions)|list|length %}
{{ total }}
Total Entries
{{ auth_count }}
Authentication
{{ create_count }}
Creates
{{ update_count }}
Updates
{{ delete_count }}
Deletes
{{ infra_count }}
Infrastructure
{{ storage_count }}
Storage
{# ═══════ CATEGORY PILLS ═══════ #}
All
Auth ({{ auth_count }})
Create ({{ create_count }})
Update ({{ update_count }})
Delete ({{ delete_count }})
Infrastructure ({{ infra_count }})
Storage ({{ storage_count }})
MLOps
Views
Profile
Failed
{# ═══════ FILTER BAR ═══════ #}
{# ═══════ ENTRIES ═══════ #}
{% if entries %} {% for entry in entries %} {% set action = entry.action|default('unknown') %} {% set cat = 'auth' if action in ('login','logout','login_failed') else 'create' if action in ('create','admin_user_create','volume_create','network_create') else 'update' if action in ('update','admin_user_update','settings_change','permission_change') else 'delete' if action in ('delete','admin_user_delete','file_delete') else 'infra' if action in ('container_action','container_exec','container_export','docker_prune','docker_build','image_action','image_tag','compose_action','volume_action','network_action') else 'storage' if action in ('file_upload','file_download') else 'mlops' if action in ('ml_inference','ml_batch_inference','ml_compare','ml_health_check') else 'profile' if action in ('profile_update','avatar_upload','password_change') else 'view' if action in ('view','list','page_view','search') else 'update' %}
{% if entry.success is defined and not entry.success %} {{ action|replace('_',' ') }} {% else %} {{ action|replace('_',' ') }} {% endif %} {{ entry.username|default('unknown') }} {% if entry.role %} {{ entry.role }} {% endif %} {% if entry.model_name %} on {{ entry.model_name }} {% endif %} {% if entry.record_pk %} #{{ entry.record_pk }} {% endif %} {{ entry.timestamp|default('')|string|truncate(19, True, '') }}
{# ── Expandable detail ── #}
Action
{{ action }}
User
{{ entry.username|default('—') }}
Role
{{ entry.role|default('—') }}
Model
{{ entry.model_name|default('—') }}
Record PK
{{ entry.record_pk|default('—') }}
Status
{% if entry.success is defined and not entry.success %} ✗ Failed {% else %} ✓ Success {% endif %}
IP Address
{{ entry.ip_address|default('—') }}
User Agent
{{ (entry.user_agent or '—')|truncate(80) }}
Timestamp
{{ entry.timestamp|default('—') }}
{% if entry.user_id %}
User ID
{{ entry.user_id }}
{% endif %}
{% if entry.error_message %}
Error
{{ entry.error_message }}
{% endif %} {% if entry.changes and entry.changes is mapping %}
Changes
{% for field_name, change in entry.changes.items() %} {% if change is mapping %}
{{ field_name }}: {{ change.old|default('∅') }}{{ change.new|default('∅') }}
{% else %}
{{ field_name }}: {{ change }}
{% endif %} {% endfor %}
{% endif %} {% if entry.metadata and entry.metadata is mapping and entry.metadata|length > 0 %}
Metadata
{% for mk, mv in entry.metadata.items() %} {{ mk }}={{ mv }} {% endfor %}
{% endif %}
{% endfor %} {% else %}
No audit entries yet
Activity will appear here as users interact with the admin panel.
{% endif %}
{# ═══════ PAGINATION ═══════ #} {% if total_pages is defined and total_pages > 1 %}
Showing {{ ((page - 1) * per_page) + 1 }}–{{ [page * per_page, total]|min }} of {{ total }} entries
{% endif %} {% endblock %} {% block extra_js %} (function(){ 'use strict'; var CATEGORIES = { auth:['login','logout','login_failed'], create:['create','admin_user_create','volume_create','network_create'], update:['update','admin_user_update','settings_change','permission_change','profile_update','avatar_upload','password_change','alert_config'], 'delete':['delete','admin_user_delete','file_delete'], infra:['container_action','container_exec','container_export','docker_prune','docker_build','image_action','image_tag','compose_action','volume_action','network_action'], storage:['file_upload','file_delete','file_download'], mlops:['ml_inference','ml_batch_inference','ml_compare','ml_health_check'], view:['view','list','page_view','search'], profile:['profile_update','avatar_upload','password_change'] }; var activeCategory = 'all'; window.filterCategory = function(cat, btn) { activeCategory = cat; document.querySelectorAll('#auditCategories .audit-cat-pill').forEach(function(p) { p.classList.remove('active'); }); if (btn) btn.classList.add('active'); applyFilters(); }; window.applyFilters = function() { var search = (document.getElementById('auditSearchInput').value || '').toLowerCase(); var dateFrom = document.getElementById('auditDateFrom').value; var dateTo = document.getElementById('auditDateTo').value; document.querySelectorAll('#auditList .audit-entry-card').forEach(function(card) { var action = card.getAttribute('data-action') || ''; var searchText = card.getAttribute('data-search') || ''; var success = card.getAttribute('data-success') || 'true'; var timestamp = card.getAttribute('data-timestamp') || ''; var dateStr = timestamp.substring(0, 10); var show = true; if (activeCategory !== 'all') { if (activeCategory === 'failed') { if (success !== 'false') show = false; } else { var actions = CATEGORIES[activeCategory]; if (actions && actions.indexOf(action) === -1) show = false; } } if (show && search && !searchText.includes(search)) show = false; if (show && dateFrom && dateStr < dateFrom) show = false; if (show && dateTo && dateStr > dateTo) show = false; card.style.display = show ? '' : 'none'; }); }; window.toggleDetail = function(card) { var detail = card.querySelector('.audit-detail'); if (detail) detail.classList.toggle('open'); }; window.exportAudit = function(format) { var entries = []; document.querySelectorAll('#auditList .audit-entry-card').forEach(function(card) { if (card.style.display === 'none') return; entries.push({ action: card.getAttribute('data-action'), success: card.getAttribute('data-success'), timestamp: card.getAttribute('data-timestamp'), search: card.getAttribute('data-search') }); }); var content, mime, ext; if (format === 'json') { content = JSON.stringify(entries, null, 2); mime = 'application/json'; ext = 'json'; } else { var lines = ['action,success,timestamp,info']; entries.forEach(function(e) { lines.push([e.action, e.success, e.timestamp, e.search].map(function(v) { return '"' + (v || '').replace(/"/g, '""') + '"'; }).join(',')); }); content = lines.join('\n'); mime = 'text/csv'; ext = 'csv'; } var blob = new Blob([content], { type: mime }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'audit_log_export.' + ext; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; })(); {% endblock %}