Files
oslstats/internal/view/adminview/audit_log_detail.templ
2026-02-14 14:54:06 +11:00

198 lines
8.5 KiB
Plaintext

package adminview
import "git.haelnorr.com/h/oslstats/internal/db"
import "fmt"
import "time"
import "encoding/json"
templ AuditLogDetail(log *db.AuditLog) {
<!-- Modal overlay -->
<div
class="fixed inset-0 bg-crust/80 flex items-center justify-center z-50 p-4"
x-data="{ show: true }"
x-show="show"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click.self="show = false; setTimeout(() => document.getElementById('modal-container').innerHTML = '', 200)"
>
<!-- Modal content -->
<div
class="bg-base border border-surface1 rounded-lg max-w-5xl w-full max-h-[90vh] overflow-y-auto"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
>
<!-- Header -->
<div class="bg-mantle border-b border-surface1 px-6 py-4">
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="flex items-center gap-3">
<h2 class="text-xl font-bold text-text">Audit Log #{ fmt.Sprintf("%d", log.ID) }</h2>
@resultBadge(log.Result)
</div>
<p class="text-sm text-subtext0 mt-1">{ formatDetailTimestamp(log.CreatedAt) }</p>
</div>
<button
@click="show = false; setTimeout(() => document.getElementById('modal-container').innerHTML = '', 200)"
class="text-subtext0 hover:text-text transition ml-4"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
</div>
<!-- Body -->
<div class="p-6">
<!-- Main Info Grid -->
<div class="bg-surface0 border border-surface1 rounded-lg p-4 mb-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-3">
<!-- User -->
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-subtext0 flex-shrink-0" 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"></path>
</svg>
<div class="min-w-0">
<div class="text-xs text-subtext0">User</div>
<div class="text-sm text-text font-medium truncate">
if log.User != nil {
{ log.User.Username }
<span class="text-subtext1 font-normal">({ fmt.Sprintf("%d", log.UserID) })</span>
} else {
<span class="text-subtext1 italic">Unknown ({ fmt.Sprintf("%d", log.UserID) })</span>
}
</div>
</div>
</div>
<!-- Action -->
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-subtext0 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
<div class="min-w-0">
<div class="text-xs text-subtext0">Action</div>
<div class="text-sm text-text font-mono truncate">{ log.Action }</div>
</div>
</div>
<!-- Resource Type -->
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-subtext0 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"></path>
</svg>
<div class="min-w-0">
<div class="text-xs text-subtext0">Resource Type</div>
<div class="text-sm text-text truncate">{ log.ResourceType }</div>
</div>
</div>
<!-- Resource ID -->
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-subtext0 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
</svg>
<div class="min-w-0">
<div class="text-xs text-subtext0">Resource ID</div>
<div class="text-sm text-text font-mono truncate">
if log.ResourceID != nil {
{ *log.ResourceID }
} else {
<span class="text-subtext1 italic">N/A</span>
}
</div>
</div>
</div>
<!-- IP Address -->
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-subtext0 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"></path>
</svg>
<div class="min-w-0">
<div class="text-xs text-subtext0">IP Address</div>
<div class="text-sm text-text font-mono truncate">{ log.IPAddress }</div>
</div>
</div>
</div>
</div>
<!-- Error Message (if applicable) -->
if log.ErrorMessage != nil && *log.ErrorMessage != "" {
<div class="bg-red/10 border border-red/30 rounded-lg p-4 mb-4">
<div class="flex gap-2">
<svg class="w-5 h-5 text-red flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<div class="flex-1 min-w-0">
<div class="text-sm font-semibold text-red mb-1">Error Message</div>
<p class="text-red font-mono text-sm break-words">{ *log.ErrorMessage }</p>
</div>
</div>
</div>
}
<!-- User Agent -->
<div class="bg-surface0 border border-surface1 rounded-lg p-4 mb-4">
<div class="flex gap-2">
<svg class="w-4 h-4 text-subtext0 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
</svg>
<div class="flex-1 min-w-0">
<div class="text-xs text-subtext0 mb-1">User Agent</div>
<p class="text-sm text-text break-words">{ log.UserAgent }</p>
</div>
</div>
</div>
<!-- Details JSON -->
if log.Details != nil && len(log.Details) > 0 && string(log.Details) != "null" {
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden">
<div class="bg-surface0 px-4 py-2 border-b border-surface1">
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-subtext0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path>
</svg>
<span class="text-sm font-semibold text-text">Details (JSON)</span>
</div>
</div>
<div class="p-4 overflow-x-auto max-h-96">
<pre class="text-text text-xs font-mono whitespace-pre-wrap">{ formatJSON(log.Details) }</pre>
</div>
</div>
}
</div>
<!-- Footer -->
<div class="flex justify-end gap-2 px-6 py-4 border-t border-surface1 bg-mantle">
<button
@click="show = false; setTimeout(() => document.getElementById('modal-container').innerHTML = '', 200)"
class="px-4 py-2 bg-blue hover:bg-blue/80 text-mantle rounded font-medium transition hover:cursor-pointer"
>
Close
</button>
</div>
</div>
</div>
}
func formatDetailTimestamp(unixTime int64) string {
t := time.Unix(unixTime, 0)
return t.Format("Monday, January 2, 2006 at 3:04:05 PM MST")
}
func formatJSON(raw []byte) string {
if len(raw) == 0 || string(raw) == "null" {
return "No details available"
}
// Pretty print the JSON
var obj any
if err := json.Unmarshal(raw, &obj); err != nil {
return string(raw)
}
pretty, err := json.MarshalIndent(obj, "", " ")
if err != nil {
return string(raw)
}
return string(pretty)
}