Files
GoMFT/components/admin_tools.templ
T
StarFleetCPTN 874c7ad69e Refactor Admin Tools Template and Handler Context
- Updated the AdminTools template to format JSON payloads for better readability.
- Modified the HandleAdminTools function to create a template context with user information before rendering the AdminTools view.
2025-03-20 21:23:59 -07:00

1395 lines
52 KiB
Templ

package components
import (
"context"
"fmt"
"time"
)
type BackupFile struct {
Name string
Size string
ModTime time.Time
}
type LogFile struct {
Name string
Size string
ModTime time.Time
Path string
}
type AdminToolsData struct {
JobHistoryCount int
DatabaseSize string
LastBackupTime *time.Time
BackupCount int
SystemUptime string
ActiveJobs int
TotalConfigs int
TotalJobs int
TotalUsers int
DatabasePath string
BackupPath string
MaintenanceMessage string
BackupFiles []BackupFile
LogFiles []LogFile
LogContent string
CurrentLogFile string
EmailTestSuccess *bool
EmailTestMessage string
SmtpServer string
WebhookTestSuccess *bool
WebhookTestMessage string
WebhookStatusCode int
WebhookResponse string
}
// Dialog component for confirmation dialogs
templ Dialog(id string, title string, message string, confirmClass string, confirmText string, formId string, targetAction string) {
<div id={ id } class="hidden fixed inset-0 bg-secondary-900/50 dark:bg-secondary-900/80 backdrop-blur-sm z-50 flex items-center justify-center">
<div class="bg-white dark:bg-secondary-800 rounded-lg shadow-xl max-w-md w-full mx-4 overflow-hidden">
<div class="px-6 pt-5 pb-3 text-center">
<div class="flex justify-center mb-2">
<i class="fas fa-exclamation-triangle text-yellow-400 text-3xl"></i>
</div>
<h3 class="text-xl font-medium text-secondary-900 dark:text-secondary-100">
{ title }
</h3>
</div>
<div class="px-6 py-4 text-center">
<p class="text-secondary-700 dark:text-secondary-300">
{ message }
</p>
</div>
<div class="px-6 py-4 flex justify-end space-x-3">
<button type="button" class="btn-secondary" onclick={ hideDialog(id) }>
Cancel
</button>
if formId != "" {
<button
type="button"
class={ confirmClass }
onclick={ submitFormAndHideDialog(formId, id) }>
{ confirmText }
</button>
} else {
<button
type="button"
class={ confirmClass }
hx-target={ targetAction }
onclick={ hideDialog(id) }>
{ confirmText }
</button>
}
</div>
</div>
</div>
}
script hideDialog(id string) {
document.getElementById(id).classList.add("hidden");
}
script submitFormAndHideDialog(formId string, dialogId string) {
// Use HTMX's API to trigger the request instead of bypassing it
htmx.trigger(document.getElementById(formId), 'submit');
document.getElementById(dialogId).classList.add("hidden");
}
script showDialog(id string) {
document.getElementById(id).classList.remove("hidden");
}
// Backup dialog component specifically for restore and delete actions
templ BackupActionDialog(id string, title string, message string, confirmClass string, confirmText string, action string, backupName string) {
<div id={ id } class="hidden fixed inset-0 bg-secondary-900/50 dark:bg-secondary-900/80 backdrop-blur-sm z-50 flex items-center justify-center">
<div class="bg-white dark:bg-secondary-800 rounded-lg shadow-xl max-w-md w-full mx-4 overflow-hidden">
<div class="px-6 pt-5 pb-3 text-center">
<div class="flex justify-center mb-2">
<i class="fas fa-exclamation-triangle text-yellow-400 text-5xl"></i>
</div>
<h3 class="text-xl font-medium text-secondary-900 dark:text-secondary-100">
{ title }
</h3>
</div>
<div class="px-6 py-4 text-center">
<p class="text-secondary-700 dark:text-secondary-300">
{ message }
</p>
</div>
<div class="px-6 py-4 flex justify-center space-x-3">
<button type="button" class="btn-secondary" onclick={ hideDialog(id) }>
Cancel
</button>
if action == "restore" {
<button
type="button"
class="btn-warning"
hx-post={ fmt.Sprintf("/admin/restore-database/%s", backupName) }
hx-swap="none"
onclick={ hideDialog(id) }>
{ confirmText }
</button>
} else if action == "delete" {
<button
type="button"
class="btn-danger"
hx-delete={ fmt.Sprintf("/admin/delete-backup/%s", backupName) }
hx-target="closest tr"
hx-swap="delete"
onclick={ hideDialog(id) }>
{ confirmText }
</button>
}
</div>
</div>
</div>
}
templ AdminTools(ctx context.Context, data AdminToolsData) {
@LayoutWithContext("Admin Tools", ctx) {
<style>
/* Custom scrollbar styles - more aggressive */
.log-scrollbar {
scrollbar-width: thin !important; /* Firefox */
scrollbar-color: rgba(0,0,0,0.3) rgba(0,0,0,0.1) !important; /* Firefox */
overflow: auto !important;
}
.dark .log-scrollbar {
scrollbar-color: rgba(255,255,255,0.3) rgba(255,255,255,0.1) !important; /* Firefox */
}
.log-scrollbar::-webkit-scrollbar {
width: 10px !important;
height: 10px !important;
display: block !important;
}
.log-scrollbar::-webkit-scrollbar-track {
background: rgba(0,0,0,0.1) !important;
border-radius: 4px !important;
}
.log-scrollbar::-webkit-scrollbar-thumb {
background: rgba(0,0,0,0.3) !important;
border-radius: 4px !important;
border: 2px solid transparent !important;
background-clip: content-box !important;
}
.log-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(0,0,0,0.5) !important;
border: 2px solid transparent !important;
background-clip: content-box !important;
}
.dark .log-scrollbar::-webkit-scrollbar-track {
background: rgba(255,255,255,0.1) !important;
}
.dark .log-scrollbar::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.3) !important;
border: 2px solid transparent !important;
background-clip: content-box !important;
}
.dark .log-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(255,255,255,0.5) !important;
border: 2px solid transparent !important;
background-clip: content-box !important;
}
/* Force scrollbar to appear */
.force-scroll {
overflow-y: scroll !important;
min-height: 100px !important;
}
</style>
<div class="py-6">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between mb-8">
<h1 class="text-3xl font-bold text-secondary-900 dark:text-secondary-100">
<i class="fas fa-tools mr-2 text-primary-600 dark:text-primary-400"></i>
Admin Tools
</h1>
<div class="text-sm text-secondary-500 dark:text-secondary-400">
<span id="current-date" class="font-medium"></span>
<script>
document.getElementById('current-date').textContent = new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
</script>
</div>
</div>
if data.MaintenanceMessage != "" {
<div class="mb-6 bg-yellow-50 dark:bg-yellow-900/20 border-l-4 border-yellow-400 p-4 rounded">
<div class="flex">
<div class="flex-shrink-0">
<i class="fas fa-exclamation-triangle text-yellow-400"></i>
</div>
<div class="ml-3">
<p class="text-sm text-yellow-700 dark:text-yellow-300">
{ data.MaintenanceMessage }
</p>
</div>
</div>
</div>
}
<!-- System Overview -->
<div class="mb-8">
<h2 class="text-xl font-semibold text-secondary-900 dark:text-secondary-100 mb-4">
<i class="fas fa-chart-line mr-2 text-primary-500"></i>
System Overview
</h2>
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
<div class="card overflow-hidden">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<i class="fas fa-database text-blue-500 text-2xl"></i>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400 truncate">
Database Size
</dt>
<dd>
<div class="text-lg font-medium text-secondary-900 dark:text-secondary-100">
{ data.DatabaseSize }
</div>
</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="card overflow-hidden">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<i class="fas fa-history text-green-500 text-2xl"></i>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400 truncate">
Job History Records
</dt>
<dd>
<div class="text-lg font-medium text-secondary-900 dark:text-secondary-100">
{ fmt.Sprint(data.JobHistoryCount) }
</div>
</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="card overflow-hidden">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<i class="fas fa-clock text-purple-500 text-2xl"></i>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400 truncate">
System Uptime
</dt>
<dd>
<div class="text-lg font-medium text-secondary-900 dark:text-secondary-100">
{ data.SystemUptime }
</div>
</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="card overflow-hidden">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<i class="fas fa-tasks text-orange-500 text-2xl"></i>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400 truncate">
Active Jobs
</dt>
<dd>
<div class="text-lg font-medium text-secondary-900 dark:text-secondary-100">
{ fmt.Sprint(data.ActiveJobs) }
</div>
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Admin Tools Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Backup & Restore -->
<div class="card">
<div class="card-header">
<h3 class="text-lg font-medium text-secondary-900 dark:text-secondary-100">
<i class="fas fa-save mr-2 text-primary-500"></i>
Backup & Restore
</h3>
</div>
<div class="card-body space-y-4">
<div class="flex items-center justify-between text-sm text-secondary-500 dark:text-secondary-400 mb-2">
if data.LastBackupTime != nil {
<span>Last backup: { data.LastBackupTime.Format("Jan 02, 2006 15:04:05") }</span>
} else {
<span>Last backup: Never</span>
}
</div>
<button id="backup-form"
hx-post="/admin/backup-database"
hx-swap="none"
hx-indicator="#backup-indicator"
hx-on::after-request="htmx.trigger('#refresh-backups-trigger', 'click')"
class="btn-primary w-full flex items-center justify-center">
<i class="fas fa-database mr-2"></i>
<span>Backup Database</span>
<div id="backup-indicator" class="htmx-indicator ml-2">
<i class="fas fa-circle-notch fa-spin"></i>
</div>
</button>
<!-- Hidden refresh trigger -->
<button id="refresh-backups-trigger"
class="hidden"
hx-get="/admin/refresh-backups"
hx-target="#backups-container"></button>
<form id="export-configs-form" action="/admin/export-configs" method="GET">
<button type="submit" class="btn-secondary w-full flex items-center justify-center">
<i class="fas fa-file-export mr-2"></i>
<span>Export All Configurations</span>
<div id="export-configs-indicator" class="htmx-indicator ml-2">
<i class="fas fa-circle-notch fa-spin"></i>
</div>
</button>
</form>
<form id="export-jobs-form" action="/admin/export-jobs" method="GET">
<button type="submit" class="btn-secondary w-full flex items-center justify-center">
<i class="fas fa-file-export mr-2"></i>
<span>Export All Jobs</span>
<div id="export-jobs-indicator" class="htmx-indicator ml-2">
<i class="fas fa-circle-notch fa-spin"></i>
</div>
</button>
</form>
<div class="border-t border-secondary-200 dark:border-secondary-700 pt-4 mt-4">
<h4 class="text-sm font-medium text-secondary-900 dark:text-secondary-100 mb-2">Restore Database</h4>
<form id="restore-form" hx-post="/admin/restore-database" hx-encoding="multipart/form-data" hx-swap="none" hx-indicator="#restore-indicator">
<div class="flex items-center space-x-2">
<div class="flex-grow">
<label class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
Select Backup File
</label>
<input type="file" name="backup_file" class="block w-full text-sm text-secondary-500 dark:text-secondary-400
file:mr-4 file:py-2 file:px-4
file:rounded-md file:border-0
file:text-sm file:font-medium
file:bg-primary-50 file:text-primary-700
dark:file:bg-primary-900 dark:file:text-primary-300
hover:file:bg-primary-100 dark:hover:file:bg-primary-800
focus:outline-none" required />
</div>
<button type="submit" class="btn-warning flex-shrink-0 flex items-center justify-center h-10">
<div class="flex items-center justify-center">
<i class="fas fa-upload mr-1"></i>
<span>Restore</span>
<div id="restore-indicator" class="htmx-indicator ml-2">
<i class="fas fa-circle-notch fa-spin"></i>
</div>
</div>
</button>
</div>
<p class="text-xs text-red-600 dark:text-red-400 mt-2">
<i class="fas fa-exclamation-triangle mr-1"></i>
Warning: This will replace your current database. Make sure to backup first!
</p>
</form>
</div>
</div>
</div>
<!-- Maintenance Tools -->
<div class="card">
<div class="card-header">
<h3 class="text-lg font-medium text-secondary-900 dark:text-secondary-100">
<i class="fas fa-wrench mr-2 text-primary-500"></i>
Maintenance Tools
</h3>
</div>
<div class="card-body space-y-4">
<!-- Clear Job History Button and Dialog -->
@Dialog("clear-job-dialog", "Clear Job History", "Are you sure you want to clear all job history? This cannot be undone.", "btn-danger", "Clear History", "purge-form", "")
<form id="purge-form" hx-post="/admin/clear-job-history" hx-swap="none" hx-indicator="#clear-history-indicator">
<button type="button" class="btn-danger w-full flex items-center justify-center" onclick={ showDialog("clear-job-dialog") }>
<i class="fas fa-trash-alt mr-2"></i>
<span>Clear Job History</span>
<div id="clear-history-indicator" class="htmx-indicator ml-2">
<i class="fas fa-circle-notch fa-spin"></i>
</div>
</button>
</form>
<div class="border-t border-secondary-200 dark:border-secondary-700 pt-4 mt-4">
<form id="vacuum-form" hx-post="/admin/vacuum-database" hx-swap="none" hx-indicator="#vacuum-indicator">
<button type="submit" class="btn-secondary w-full flex items-center justify-center">
<i class="fas fa-compress-alt mr-2"></i>
<span>Optimize Database</span>
<div id="vacuum-indicator" class="htmx-indicator ml-2">
<i class="fas fa-circle-notch fa-spin"></i>
</div>
</button>
<p class="text-xs text-secondary-500 dark:text-secondary-400 mt-2">
Runs VACUUM to optimize the database and reclaim unused space.
</p>
</form>
</div>
</div>
</div>
</div>
<!-- Email Testing Tools -->
<div class="mt-8">
<div class="card">
<div class="card-header">
<h3 class="text-lg font-medium text-secondary-900 dark:text-secondary-100">
<i class="fas fa-envelope mr-2 text-primary-500"></i>
Email Testing
</h3>
</div>
<div class="card-body">
<p class="text-sm text-secondary-600 dark:text-secondary-400 mb-4">
Test your email configuration by sending a test email to verify the server can send emails properly.
</p>
<form id="test-email-form" hx-post="/admin/test-email" hx-target="#email-test-result" hx-swap="outerHTML" hx-indicator="#email-test-indicator">
<div class="space-y-4">
<div>
<label for="test-email-recipient" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
Recipient Email
</label>
<input
type="email"
id="test-email-recipient"
name="recipient"
class="form-input w-full"
placeholder="recipient@example.com"
required
/>
</div>
<div>
<label for="test-email-subject" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
Subject (Optional)
</label>
<input
type="text"
id="test-email-subject"
name="subject"
class="form-input w-full"
placeholder="Test Email from GoMFT"
/>
</div>
<div>
<label for="test-email-message" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
Message (Optional)
</label>
<textarea
id="test-email-message"
name="message"
rows="3"
class="form-textarea w-full"
placeholder="This is a test email from GoMFT."
></textarea>
</div>
<div class="flex justify-end">
<button type="submit" class="btn-primary flex items-center justify-center" onclick="fadeInAnimation()">
<i class="fas fa-paper-plane mr-2"></i>
<span>Send Test Email</span>
<div id="email-test-indicator" class="htmx-indicator ml-2">
<i class="fas fa-circle-notch fa-spin"></i>
</div>
</button>
</div>
</div>
</form>
<!-- Toast container for email test results -->
<div id="email-test-result" class="mt-4 hidden">
if data.EmailTestSuccess != nil {
@EmailTestToast(*data.EmailTestSuccess, data.EmailTestMessage)
}
</div>
<!-- Email settings reminder -->
<div class="mt-6 bg-secondary-50 dark:bg-secondary-800/50 p-4 rounded-lg">
<h4 class="text-sm font-medium text-secondary-900 dark:text-secondary-100 flex items-center">
<i class="fas fa-info-circle mr-2 text-blue-500"></i>
Email Configuration
</h4>
if data.SmtpServer != "" {
<p class="mt-2 text-xs text-secondary-600 dark:text-secondary-400">
Current SMTP server: <span class="font-mono">{ data.SmtpServer }</span>
</p>
} else {
<p class="mt-2 text-xs text-secondary-600 dark:text-secondary-400">
Email settings are configured in your application configuration file. Make sure SMTP settings are properly configured before testing.
</p>
}
</div>
</div>
</div>
</div>
<!-- Notification Testing Tools -->
<div class="mt-8">
<div class="card">
<div class="card-header">
<h3 class="text-lg font-medium text-secondary-900 dark:text-secondary-100">
<i class="fas fa-bell mr-2 text-primary-500"></i>
Webhook Notification Testing
</h3>
</div>
<div class="card-body">
<p class="text-sm text-secondary-600 dark:text-secondary-400 mb-4">
Test webhook notifications by sending a sample job execution payload to your webhook endpoint.
</p>
<form id="webhook-test-form" hx-post="/admin/test-webhook" hx-target="#webhook-test-result" hx-swap="outerHTML" hx-indicator="#webhook-test-indicator">
<div class="space-y-4">
<div>
<label for="webhook-url" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
Webhook URL
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-link text-secondary-400 dark:text-secondary-600"></i>
</div>
<input
type="url"
id="webhook-url"
name="webhook_url"
class="form-input pl-10 w-full"
placeholder="https://example.com/webhook"
required
/>
</div>
</div>
<div>
<label for="webhook-secret" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
Webhook Secret <span class="text-secondary-500 dark:text-secondary-400">(optional)</span>
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-key text-secondary-400 dark:text-secondary-600"></i>
</div>
<input
type="password"
id="webhook-secret"
name="webhook_secret"
class="form-input pl-10 w-full"
placeholder="Secret token for signing requests"
/>
</div>
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
<i class="fas fa-info-circle mr-1"></i>
Used to sign webhook payloads (X-Hub-Signature-256 header)
</p>
</div>
<div>
<label for="webhook-headers" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
Custom Headers <span class="text-secondary-500 dark:text-secondary-400">(optional)</span>
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-code text-secondary-400 dark:text-secondary-600"></i>
</div>
<input
type="text"
id="webhook-headers"
name="webhook_headers"
class="form-input pl-10 w-full"
placeholder='{"X-Custom-Header": "value"}'
/>
</div>
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
<i class="fas fa-info-circle mr-1"></i>
Additional HTTP headers as JSON
</p>
</div>
<div>
<label for="webhook-payload" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
Custom Payload <span class="text-secondary-500 dark:text-secondary-400">(optional)</span>
</label>
<textarea
id="webhook-payload"
name="webhook_payload"
rows="5"
class="form-textarea w-full font-mono text-sm"
placeholder='{
"event_type": "job_execution",
"job_id": 123,
"job_name": "Test Job",
"status": "completed"
}'></textarea>
<p class="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
<i class="fas fa-info-circle mr-1"></i>
Leave empty to use default test payload
</p>
</div>
<div class="flex justify-end">
<button type="submit" class="btn-primary flex items-center justify-center" onclick="fadeInAnimation()">
<i class="fas fa-paper-plane mr-2"></i>
<span>Send Test Webhook</span>
<div id="webhook-test-indicator" class="htmx-indicator ml-2">
<i class="fas fa-circle-notch fa-spin"></i>
</div>
</button>
</div>
</div>
</form>
<!-- Result container for webhook test -->
<div id="webhook-test-result" class="mt-4 hidden">
if data.WebhookTestSuccess != nil {
@WebhookTestToast(*data.WebhookTestSuccess, data.WebhookTestMessage, data.WebhookStatusCode, data.WebhookResponse)
}
</div>
<!-- Sample payload section -->
<div class="mt-6 bg-secondary-50 dark:bg-secondary-800/50 p-4 rounded-lg">
<h4 class="text-sm font-medium text-secondary-900 dark:text-secondary-100 flex items-center">
<i class="fas fa-info-circle mr-2 text-blue-500"></i>
Default Webhook Test Payload
</h4>
<div class="mt-2 bg-white dark:bg-secondary-900 p-3 rounded border border-secondary-200 dark:border-secondary-700 overflow-auto">
<pre class="text-xs text-secondary-600 dark:text-secondary-400 font-mono">&#123;
"event_type": "job_execution",
"job_id": 123,
"job_name": "Test Job",
"config_id": 456,
"config_name": "Test Config",
"status": "completed",
"start_time": "2023-06-18T15:30:45Z",
"end_time": "2023-06-18T15:35:12Z",
"duration_seconds": 267,
"history_id": 789,
"bytes_transferred": 1048576,
"files_transferred": 5,
"source": &#123;
"type": "local",
"path": "/path/to/source"
&#125;,
"destination": &#123;
"type": "s3",
"path": "bucket/path"
&#125;
&#125;</pre>
</div>
</div>
</div>
</div>
</div>
<!-- Available Backups -->
<div id="backups-container" class="mt-8">
@BackupsList(data)
</div>
<!-- System Information -->
<div class="mt-8">
<div class="card">
<div class="card-header">
<h3 class="text-lg font-medium text-secondary-900 dark:text-secondary-100">
<i class="fas fa-info-circle mr-2 text-primary-500"></i>
System Information
</h3>
</div>
<div class="card-body">
<dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
<div class="sm:col-span-1">
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400">Database Path</dt>
<dd class="mt-1 text-sm text-secondary-900 dark:text-secondary-100 font-mono bg-secondary-50 dark:bg-secondary-900 p-2 rounded overflow-auto">
{ data.DatabasePath }
</dd>
</div>
<div class="sm:col-span-1">
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400">Backup Directory</dt>
<dd class="mt-1 text-sm text-secondary-900 dark:text-secondary-100 font-mono bg-secondary-50 dark:bg-secondary-900 p-2 rounded overflow-auto">
{ data.BackupPath }
</dd>
</div>
<div class="sm:col-span-1">
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400">Total Users</dt>
<dd class="mt-1 text-sm text-secondary-900 dark:text-secondary-100">
{ fmt.Sprint(data.TotalUsers) }
</dd>
</div>
<div class="sm:col-span-1">
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400">Total Configurations</dt>
<dd class="mt-1 text-sm text-secondary-900 dark:text-secondary-100">
{ fmt.Sprint(data.TotalConfigs) }
</dd>
</div>
<div class="sm:col-span-1">
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400">Total Jobs</dt>
<dd class="mt-1 text-sm text-secondary-900 dark:text-secondary-100">
{ fmt.Sprint(data.TotalJobs) }
</dd>
</div>
<div class="sm:col-span-1">
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400">Application Version</dt>
<dd class="mt-1 text-sm text-secondary-900 dark:text-secondary-100">
<div class="flex items-center">
<span>{ AppVersion }</span>
if AppVersion == "dev" {
<a href="https://github.com/starfleetcptn/gomft/releases"
class="ml-2 text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300"
target="_blank" rel="noopener">
<i class="fas fa-external-link-alt text-xs"></i>
</a>
} else if AppVersion == "1.0.0" {
<a href="https://github.com/starfleetcptn/gomft/releases/tag/v1.0.0"
class="ml-2 text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300"
target="_blank" rel="noopener">
<i class="fas fa-external-link-alt text-xs"></i>
</a>
} else if AppVersion == "1.1.0" {
<a href="https://github.com/starfleetcptn/gomft/releases/tag/v1.1.0"
class="ml-2 text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300"
target="_blank" rel="noopener">
<i class="fas fa-external-link-alt text-xs"></i>
</a>
} else {
<a href="https://github.com/starfleetcptn/gomft/releases"
class="ml-2 text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300"
target="_blank" rel="noopener">
<i class="fas fa-external-link-alt text-xs"></i>
</a>
}
</div>
</dd>
</div>
</dl>
</div>
</div>
</div>
<!-- Build Information -->
<div class="mt-6">
<div class="card">
<div class="card-header">
<h3 class="text-lg font-medium text-secondary-900 dark:text-secondary-100">
<i class="fas fa-code-branch mr-2 text-primary-500"></i>
Build Information
</h3>
</div>
<div class="card-body">
<div class="relative overflow-hidden bg-secondary-50 dark:bg-secondary-900 rounded-lg">
<div class="px-4 py-5 sm:p-6">
<dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-3">
<div class="sm:col-span-1">
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400">Version</dt>
<dd class="mt-1 text-sm text-secondary-900 dark:text-secondary-100 font-mono">
{ AppVersion }
</dd>
</div>
<div class="sm:col-span-1">
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400">Build Time</dt>
<dd class="mt-1 text-sm text-secondary-900 dark:text-secondary-100 font-mono">
{ getBuildTime() }
</dd>
</div>
<div class="sm:col-span-1">
<dt class="text-sm font-medium text-secondary-500 dark:text-secondary-400">Commit</dt>
<dd class="mt-1 text-sm text-secondary-900 dark:text-secondary-100 font-mono">
{ getCommit() }
</dd>
</div>
</dl>
<div class="mt-6 flex justify-end">
<a
href="https://github.com/starfleetcptn/gomft"
class="inline-flex items-center px-4 py-2 text-sm text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 transition-colors"
target="_blank" rel="noopener"
>
<i class="fab fa-github mr-2"></i>
View on GitHub
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Log Viewer -->
<div id="logs-container" class="mt-8">
@AdminLogViewer(data)
</div>
<!-- Webhook Documentation -->
<div class="mt-8">
<div class="card">
<div class="card-header">
<h3 class="text-lg font-medium text-secondary-900 dark:text-secondary-100">
<i class="fas fa-bell mr-2 text-primary-500"></i>
Webhook Notifications
</h3>
</div>
<div class="card-body">
<p class="text-secondary-600 dark:text-secondary-400 mb-4">
GoMFT can send webhook notifications when jobs run. You can configure webhooks
for individual jobs in the job edit form. Below is the format of the webhook payload:
</p>
<div class="bg-secondary-50 dark:bg-secondary-900 p-4 rounded-lg overflow-auto font-mono text-sm">
<pre>&#123;
"event_type": "job_execution",
"job_id": 123,
"job_name": "Daily Backup",
"config_id": 456,
"config_name": "Backup Config",
"status": "completed",
"start_time": "2023-06-18T15:30:45Z",
"end_time": "2023-06-18T15:35:12Z",
"duration_seconds": 267,
"history_id": 789,
"bytes_transferred": 1048576,
"files_transferred": 5,
"source": &#123;
"type": "local",
"path": "/path/to/source"
&#125;,
"destination": &#123;
"type": "s3",
"path": "bucket/path"
&#125;
&#125;</pre>
</div>
<h4 class="text-lg font-medium text-secondary-900 dark:text-secondary-100 mt-6 mb-2">Authentication</h4>
<p class="text-secondary-600 dark:text-secondary-400 mb-4">
When configuring a webhook, you can optionally provide a secret key. This will be used to sign
the webhook payload with HMAC-SHA256. The signature is provided in the <code>X-Hub-Signature-256</code> header.
</p>
<h4 class="text-lg font-medium text-secondary-900 dark:text-secondary-100 mt-6 mb-2">HTTP Request Details</h4>
<div class="overflow-x-auto">
<table class="min-w-full">
<thead>
<tr>
<th class="text-left text-sm font-medium text-secondary-500 dark:text-secondary-400 pb-2">Property</th>
<th class="text-left text-sm font-medium text-secondary-500 dark:text-secondary-400 pb-2">Value</th>
</tr>
</thead>
<tbody class="divide-y divide-secondary-200 dark:divide-secondary-700">
<tr>
<td class="py-2 text-sm text-secondary-900 dark:text-secondary-100 font-medium">Method</td>
<td class="py-2 text-sm text-secondary-600 dark:text-secondary-400">POST</td>
</tr>
<tr>
<td class="py-2 text-sm text-secondary-900 dark:text-secondary-100 font-medium">Content-Type</td>
<td class="py-2 text-sm text-secondary-600 dark:text-secondary-400">application/json</td>
</tr>
<tr>
<td class="py-2 text-sm text-secondary-900 dark:text-secondary-100 font-medium">User-Agent</td>
<td class="py-2 text-sm text-secondary-600 dark:text-secondary-400">GoMFT-Webhook/1.0</td>
</tr>
<tr>
<td class="py-2 text-sm text-secondary-900 dark:text-secondary-100 font-medium">X-Hub-Signature-256</td>
<td class="py-2 text-sm text-secondary-600 dark:text-secondary-400">HMAC SHA256 signature (if secret configured)</td>
</tr>
<tr>
<td class="py-2 text-sm text-secondary-900 dark:text-secondary-100 font-medium">Custom Headers</td>
<td class="py-2 text-sm text-secondary-600 dark:text-secondary-400">Any additional headers specified in the job configuration</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
}
}
// BackupsList is a separate component for the backups list that can be refreshed via HTMX
templ BackupsList(data AdminToolsData) {
if len(data.BackupFiles) > 0 {
<div class="card">
<div class="card-header flex justify-between items-center">
<h3 class="text-lg font-medium text-secondary-900 dark:text-secondary-100">
<i class="fas fa-history mr-2 text-primary-500"></i>
Available Backups
</h3>
<button
hx-get="/admin/refresh-backups"
hx-target="#backups-container"
hx-indicator="#refresh-backups-indicator"
class="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300"
title="Refresh backups list">
<i class="fas fa-sync-alt"></i>
<span id="refresh-backups-indicator" class="htmx-indicator ml-1">
<i class="fas fa-circle-notch fa-spin"></i>
</span>
</button>
</div>
<div class="card-body">
<div class="bg-secondary-50 dark:bg-secondary-900 rounded-lg overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-secondary-200 dark:divide-secondary-700">
<thead>
<tr class="bg-secondary-100 dark:bg-secondary-800">
<th class="px-4 py-2 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400">Name</th>
<th class="px-4 py-2 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400">Size</th>
<th class="px-4 py-2 text-left text-xs font-medium text-secondary-500 dark:text-secondary-400">Date</th>
<th class="px-4 py-2 text-right text-xs font-medium text-secondary-500 dark:text-secondary-400">Action</th>
</tr>
</thead>
<tbody class="divide-y divide-secondary-200 dark:divide-secondary-700">
for _, backup := range data.BackupFiles {
<tr class="hover:bg-secondary-100 dark:hover:bg-secondary-800 transition-colors">
<td class="px-4 py-2 text-sm text-secondary-900 dark:text-secondary-100">{ backup.Name }</td>
<td class="px-4 py-2 text-sm text-secondary-500 dark:text-secondary-400">{ backup.Size }</td>
<td class="px-4 py-2 text-sm text-secondary-500 dark:text-secondary-400">{ backup.ModTime.Format("Jan 02, 2006 15:04:05") }</td>
<td class="px-4 py-2 text-right">
<a href={ templ.SafeURL(fmt.Sprintf("/admin/download-backup/%s", backup.Name)) }
class="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 mr-2"
title="Download backup">
<i class="fas fa-download"></i>
</a>
<!-- Restore Dialog -->
@BackupActionDialog(
fmt.Sprintf("restore-dialog-%s", backup.Name),
"RESTORE BACKUP",
fmt.Sprintf("Are you sure you want to restore the backup '%s'? This will replace your current database.", backup.Name),
"btn-warning",
"Restore",
"restore",
backup.Name,
)
<button
type="button"
class="text-yellow-600 hover:text-yellow-700 dark:text-yellow-400 dark:hover:text-yellow-300 mr-2"
title="Restore this backup"
onclick={ showDialog(fmt.Sprintf("restore-dialog-%s", backup.Name)) }>
<i class="fas fa-upload"></i>
</button>
<!-- Delete Dialog -->
@BackupActionDialog(
fmt.Sprintf("delete-dialog-%s", backup.Name),
"DELETE BACKUP",
fmt.Sprintf("Are you sure you want to delete the backup '%s'? This cannot be undone.", backup.Name),
"btn-danger",
"Delete",
"delete",
backup.Name,
)
<button
type="button"
class="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
title="Delete backup"
onclick={ showDialog(fmt.Sprintf("delete-dialog-%s", backup.Name)) }>
<i class="fas fa-trash-alt"></i>
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
} else {
<div class="card">
<div class="card-header flex justify-between items-center">
<h3 class="text-lg font-medium text-secondary-900 dark:text-secondary-100">
<i class="fas fa-history mr-2 text-primary-500"></i>
Available Backups
</h3>
<button
hx-get="/admin/refresh-backups"
hx-target="#backups-container"
hx-indicator="#refresh-backups-indicator"
class="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300"
title="Refresh backups list">
<i class="fas fa-sync-alt"></i>
<span id="refresh-backups-indicator" class="htmx-indicator ml-1">
<i class="fas fa-circle-notch fa-spin"></i>
</span>
</button>
</div>
<div class="card-body">
<div class="text-center py-6">
<div class="mx-auto w-12 h-12 rounded-full bg-secondary-100 dark:bg-secondary-800 flex items-center justify-center mb-4">
<i class="fas fa-folder-open text-secondary-400 dark:text-secondary-500"></i>
</div>
<h3 class="text-sm font-medium text-secondary-900 dark:text-secondary-100">No Backups Available</h3>
<p class="mt-1 text-sm text-secondary-500 dark:text-secondary-400">
Create a backup using the "Backup Database" button.
</p>
</div>
</div>
</div>
}
}
// Add this new template after other admin tool templates
templ AdminLogViewer(data AdminToolsData) {
<div class="bg-white dark:bg-secondary-800 rounded-lg shadow-md p-6 mb-6">
<h3 class="text-xl font-semibold mb-4 text-secondary-900 dark:text-secondary-100 flex items-center">
<i class="fas fa-file-alt mr-2"></i> Log Files
</h3>
<div class="grid grid-cols-1 lg:grid-cols-4 gap-4 mb-4">
<div class="lg:col-span-1 border-r border-secondary-200 dark:border-secondary-700 pr-4">
<h4 class="text-lg font-medium mb-2 text-secondary-900 dark:text-secondary-100">Available Logs</h4>
<div class="space-y-2 max-h-96 overflow-y-auto pr-2 log-scrollbar">
if len(data.LogFiles) == 0 {
<div class="text-secondary-600 dark:text-secondary-400 italic">
No log files found
</div>
} else {
<div class="flex flex-col space-y-1">
for _, logFile := range data.LogFiles {
<button
class={
"text-left px-3 py-2 rounded transition-colors flex justify-between items-center",
templ.KV("bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400", logFile.Name == data.CurrentLogFile),
templ.KV("hover:bg-secondary-50 dark:hover:bg-secondary-700/50 text-secondary-700 dark:text-secondary-300", logFile.Name != data.CurrentLogFile)
}
hx-get={ fmt.Sprintf("/admin/logs/view/%s", logFile.Name) }
hx-target="#log-content"
hx-indicator="#log-loading"
>
<span class="flex items-center">
<i class="fas fa-file-alt mr-2"></i>
{ logFile.Name }
</span>
<span class="text-xs text-secondary-500 dark:text-secondary-400">{ logFile.Size }</span>
</button>
}
</div>
}
</div>
<div class="mt-4 flex justify-between">
<button
class="btn-secondary btn-sm"
hx-get="/admin/logs/refresh"
hx-target="#logs-container"
hx-indicator="#refresh-logs-indicator"
>
<span id="refresh-logs-indicator" class="htmx-indicator">
<i class="fas fa-spinner fa-spin"></i>
</span>
<i class="fas fa-sync-alt mr-1"></i> Refresh
</button>
</div>
</div>
<div class="lg:col-span-3 pl-0 lg:pl-4">
<div class="flex justify-between items-center mb-2">
<h4 class="text-lg font-medium text-secondary-900 dark:text-secondary-100">
if data.CurrentLogFile != "" {
Log: { data.CurrentLogFile }
} else {
Select a log file
}
</h4>
if data.CurrentLogFile != "" {
<div class="flex space-x-2">
<button
class="btn-secondary btn-sm"
hx-get={ fmt.Sprintf("/admin/logs/download/%s", data.CurrentLogFile) }
>
<i class="fas fa-download mr-1"></i> Download
</button>
</div>
}
</div>
@AdminLogContent(data)
</div>
</div>
</div>
}
// AdminLogContent template for log view
templ AdminLogContent(data AdminToolsData) {
<div id="log-content" class="relative">
<div id="log-loading" class="htmx-indicator absolute inset-0 bg-white/75 dark:bg-secondary-800/75 flex items-center justify-center">
<i class="fas fa-spinner fa-spin text-primary-600 text-2xl"></i>
</div>
if data.CurrentLogFile == "" {
<div class="border border-secondary-200 dark:border-secondary-700 rounded p-4 text-secondary-600 dark:text-secondary-400 bg-secondary-50 dark:bg-secondary-900/30 text-center h-96 flex items-center justify-center">
<div>
<i class="fas fa-file-alt text-4xl mb-2"></i>
<p>Select a log file to view its contents</p>
</div>
</div>
} else {
<!-- Fixed height log content container with guaranteed scrollbars -->
<div class="log-content-container" style="height: 400px; border: 1px solid #ccc; border-radius: 0.375rem; position: relative;">
<!-- Standard scrollable div -->
<div id="log-content-text" class="p-4 h-full overflow-y-scroll bg-secondary-50 dark:bg-secondary-900/30 text-secondary-800 dark:text-secondary-200 text-sm font-mono whitespace-pre-wrap" style="scrollbar-width: thin;">
{ data.LogContent }
</div>
<!-- Custom scrollbar -->
<div class="custom-scrollbar dark:bg-white dark:bg-opacity-10" style="position: absolute; right: 0; top: 0; width: 12px; height: 100%; background-color: rgba(0,0,0,0.05); border-radius: 0 0.375rem 0.375rem 0;">
<div class="scrollbar-thumb dark:bg-opacity-30 dark:bg-white" style="position: absolute; right: 0; width: 12px; background-color: rgba(0,0,0,0.3); border-radius: 6px; cursor: pointer; min-height: 40px;"></div>
</div>
</div>
<script>
// Custom scrollbar implementation
(function() {
const content = document.getElementById('log-content-text');
const scrollThumb = document.querySelector('.scrollbar-thumb');
// Initial position
updateScrollThumb();
// Update scrollbar position when content is scrolled
content.addEventListener('scroll', updateScrollThumb);
function updateScrollThumb() {
const scrollPercentage = content.scrollTop / (content.scrollHeight - content.clientHeight);
const thumbHeight = Math.max(40, (content.clientHeight / content.scrollHeight) * content.clientHeight);
const thumbTop = scrollPercentage * (content.clientHeight - thumbHeight);
scrollThumb.style.height = thumbHeight + 'px';
scrollThumb.style.top = thumbTop + 'px';
}
// Dragging the scrollbar
let isDragging = false;
let startY, startTop;
scrollThumb.addEventListener('mousedown', function(e) {
isDragging = true;
startY = e.clientY;
startTop = parseInt(scrollThumb.style.top) || 0;
document.body.style.userSelect = 'none'; // Prevent text selection during drag
});
document.addEventListener('mousemove', function(e) {
if (!isDragging) return;
const deltaY = e.clientY - startY;
const newTop = Math.max(0, Math.min(content.clientHeight - scrollThumb.offsetHeight, startTop + deltaY));
scrollThumb.style.top = newTop + 'px';
// Update scroll position
const scrollPercentage = newTop / (content.clientHeight - scrollThumb.offsetHeight);
content.scrollTop = scrollPercentage * (content.scrollHeight - content.clientHeight);
});
document.addEventListener('mouseup', function() {
isDragging = false;
document.body.style.userSelect = '';
});
// Auto-scroll to bottom
content.scrollTop = content.scrollHeight;
})();
</script>
}
</div>
}
// getBuildTime returns the build time of the application
// This can be set using ldflags during build
// Example: go build -ldflags "-X github.com/starfleetcptn/gomft/components.BuildTime=2023-01-01T00:00:00Z"
var BuildTime = "unknown"
func getBuildTime() string {
return BuildTime
}
// getCommit returns the commit hash of the application
// This can be set using ldflags during build
// Example: go build -ldflags "-X github.com/starfleetcptn/gomft/components.Commit=abcdef123456"
var Commit = "unknown"
func getCommit() string {
return Commit
}
// EmailTestToast is a component for showing email test results
templ EmailTestToast(success bool, message string) {
<div id="email-test-result" class="mt-4 animate-fade-in">
<div class={
"rounded-lg p-4 flex items-start",
templ.KV("bg-green-50 dark:bg-green-900/20 border-l-4 border-green-400", success),
templ.KV("bg-red-50 dark:bg-red-900/20 border-l-4 border-red-400", !success),
}>
<div class="flex-shrink-0">
if success {
<i class="fas fa-check-circle text-green-400"></i>
} else {
<i class="fas fa-exclamation-circle text-red-400"></i>
}
</div>
<div class="ml-3">
<h3 class={
"text-sm font-medium",
templ.KV("text-green-800 dark:text-green-300", success),
templ.KV("text-red-800 dark:text-red-300", !success),
}>
if success {
Email Sent Successfully
} else {
Email Sending Failed
}
</h3>
<div class={
"mt-1 text-sm",
templ.KV("text-green-700 dark:text-green-400", success),
templ.KV("text-red-700 dark:text-red-400", !success),
}>
{ message }
</div>
</div>
<div class="ml-auto pl-3 flex-shrink-0">
<button
type="button"
onclick="document.getElementById('email-test-result').classList.add('hidden');"
class={
"inline-flex rounded-md p-1.5 focus:outline-none focus:ring-2 focus:ring-offset-2",
templ.KV("text-green-500 hover:bg-green-100 dark:hover:bg-green-900/30 focus:ring-green-500", success),
templ.KV("text-red-500 hover:bg-red-100 dark:hover:bg-red-900/30 focus:ring-red-500", !success),
}
>
<i class="fas fa-times"></i>
</button>
</div>
</div>
</div>
}
// Add a style for the animate-fade-in animation
script fadeInAnimation() {
// Add CSS animation if it doesn't exist
if (!document.getElementById('fade-in-animation')) {
const style = document.createElement('style');
style.id = 'fade-in-animation';
style.textContent = `
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in {
animation: fadeIn 0.3s ease-out forwards;
}
`;
document.head.appendChild(style);
}
}
// WebhookTestToast is a component for showing webhook test results
templ WebhookTestToast(success bool, message string, statusCode int, responseBody string) {
<div id="webhook-test-result" class="mt-4 animate-fade-in">
<div class={
"rounded-lg p-4 flex items-start",
templ.KV("bg-green-50 dark:bg-green-900/20 border-l-4 border-green-400", success),
templ.KV("bg-red-50 dark:bg-red-900/20 border-l-4 border-red-400", !success),
}>
<div class="flex-shrink-0">
if success {
<i class="fas fa-check-circle text-green-400"></i>
} else {
<i class="fas fa-exclamation-circle text-red-400"></i>
}
</div>
<div class="ml-3 flex-grow">
<h3 class={
"text-sm font-medium",
templ.KV("text-green-800 dark:text-green-300", success),
templ.KV("text-red-800 dark:text-red-300", !success),
}>
if success {
Webhook Sent Successfully
} else {
Webhook Sending Failed
}
</h3>
<div class={
"mt-1 text-sm",
templ.KV("text-green-700 dark:text-green-400", success),
templ.KV("text-red-700 dark:text-red-400", !success),
}>
{ message }
</div>
<!-- Additional response details -->
<div class="mt-3 text-sm">
<div class="font-medium text-secondary-700 dark:text-secondary-300">Status Code: { fmt.Sprint(statusCode) }</div>
if responseBody != "" {
<div class="mt-2">
<div class="font-medium text-secondary-700 dark:text-secondary-300 mb-1">Response:</div>
<div class="bg-white dark:bg-secondary-900 p-2 rounded border border-secondary-200 dark:border-secondary-700">
<pre class="text-xs text-secondary-600 dark:text-secondary-400 font-mono overflow-auto max-h-40">{ responseBody }</pre>
</div>
</div>
}
</div>
</div>
<div class="ml-auto pl-3 flex-shrink-0">
<button
type="button"
onclick="document.getElementById('webhook-test-result').classList.add('hidden');"
class={
"inline-flex rounded-md p-1.5 focus:outline-none focus:ring-2 focus:ring-offset-2",
templ.KV("text-green-500 hover:bg-green-100 dark:hover:bg-green-900/30 focus:ring-green-500", success),
templ.KV("text-red-500 hover:bg-red-100 dark:hover:bg-red-900/30 focus:ring-red-500", !success),
}
>
<i class="fas fa-times"></i>
</button>
</div>
</div>
</div>
}