mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-08 15:41:20 +02:00
Merge pull request #51 from StarFleetCPTN/development
Email and Webhook notification testing. Job order for multi configs. Debugging for scheduler
This commit is contained in:
@@ -36,6 +36,13 @@ type AdminToolsData struct {
|
|||||||
LogFiles []LogFile
|
LogFiles []LogFile
|
||||||
LogContent string
|
LogContent string
|
||||||
CurrentLogFile string
|
CurrentLogFile string
|
||||||
|
EmailTestSuccess *bool
|
||||||
|
EmailTestMessage string
|
||||||
|
SmtpServer string
|
||||||
|
WebhookTestSuccess *bool
|
||||||
|
WebhookTestMessage string
|
||||||
|
WebhookStatusCode int
|
||||||
|
WebhookResponse string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dialog component for confirmation dialogs
|
// Dialog component for confirmation dialogs
|
||||||
@@ -463,6 +470,255 @@ templ AdminTools(ctx context.Context, data AdminToolsData) {
|
|||||||
</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">{
|
||||||
|
"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": {
|
||||||
|
"type": "local",
|
||||||
|
"path": "/path/to/source"
|
||||||
|
},
|
||||||
|
"destination": {
|
||||||
|
"type": "s3",
|
||||||
|
"path": "bucket/path"
|
||||||
|
}
|
||||||
|
}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Available Backups -->
|
<!-- Available Backups -->
|
||||||
<div id="backups-container" class="mt-8">
|
<div id="backups-container" class="mt-8">
|
||||||
@BackupsList(data)
|
@BackupsList(data)
|
||||||
@@ -1000,3 +1256,139 @@ var Commit = "unknown"
|
|||||||
func getCommit() string {
|
func getCommit() string {
|
||||||
return Commit
|
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>
|
||||||
|
}
|
||||||
|
|||||||
+197
-10
@@ -72,6 +72,172 @@ templ configSearchScript() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle job ordering
|
||||||
|
const setupJobOrdering = (configListId, selectedListId, formId, savedOrder) => {
|
||||||
|
const configList = document.getElementById(configListId);
|
||||||
|
const selectedList = document.getElementById(selectedListId);
|
||||||
|
const form = document.getElementById(formId);
|
||||||
|
|
||||||
|
if (!configList || !selectedList || !form) return;
|
||||||
|
|
||||||
|
// Get saved order if available
|
||||||
|
const orderedIds = savedOrder ? savedOrder.split(',').map(id => id.trim()) : [];
|
||||||
|
console.log('Initial saved order:', orderedIds);
|
||||||
|
|
||||||
|
// Initialize selected items from checked checkboxes
|
||||||
|
const updateSelectedItems = (initialLoad = false) => {
|
||||||
|
// Clear current list
|
||||||
|
selectedList.innerHTML = '';
|
||||||
|
|
||||||
|
// Get all checked checkboxes
|
||||||
|
const checkedItems = configList.querySelectorAll('input[type="checkbox"]:checked');
|
||||||
|
|
||||||
|
if (checkedItems.length === 0) {
|
||||||
|
selectedList.innerHTML = '<div class="text-center py-4 text-secondary-500 dark:text-secondary-400">No configurations selected</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a map of config items for easy access
|
||||||
|
const configItems = {};
|
||||||
|
checkedItems.forEach(checkbox => {
|
||||||
|
configItems[checkbox.value] = {
|
||||||
|
checkbox: checkbox,
|
||||||
|
configId: checkbox.value,
|
||||||
|
configName: checkbox.nextElementSibling.textContent.trim()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// If we have a saved order and this is the initial load, use that order
|
||||||
|
let itemsToShow = [];
|
||||||
|
if (initialLoad && orderedIds.length > 0) {
|
||||||
|
// First add items in the saved order
|
||||||
|
orderedIds.forEach(id => {
|
||||||
|
if (configItems[id]) {
|
||||||
|
itemsToShow.push(configItems[id]);
|
||||||
|
delete configItems[id]; // Remove from map to avoid duplicates
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then add any remaining checked items not in the saved order
|
||||||
|
Object.values(configItems).forEach(item => {
|
||||||
|
itemsToShow.push(item);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Just add all checked items in their current order
|
||||||
|
itemsToShow = Object.values(configItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add each item to the selected list
|
||||||
|
itemsToShow.forEach((item, index) => {
|
||||||
|
const configId = item.configId;
|
||||||
|
const configName = item.configName;
|
||||||
|
|
||||||
|
const listItem = document.createElement('div');
|
||||||
|
listItem.className = 'flex items-center justify-between p-2 mb-2 bg-white dark:bg-secondary-800 border border-secondary-200 dark:border-secondary-700 rounded-lg';
|
||||||
|
listItem.setAttribute('data-id', configId);
|
||||||
|
|
||||||
|
listItem.innerHTML = `
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="inline-flex items-center justify-center h-6 w-6 rounded-full bg-primary-100 dark:bg-primary-900 mr-2 text-primary-700 dark:text-primary-300 text-sm">${index + 1}</span>
|
||||||
|
<span class="font-medium text-secondary-700 dark:text-secondary-300">${configName}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-1">
|
||||||
|
<button type="button" class="move-up p-1 rounded hover:bg-secondary-100 dark:hover:bg-secondary-700" title="Move up">
|
||||||
|
<i class="fas fa-arrow-up text-secondary-500"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="move-down p-1 rounded hover:bg-secondary-100 dark:hover:bg-secondary-700" title="Move down">
|
||||||
|
<i class="fas fa-arrow-down text-secondary-500"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
selectedList.appendChild(listItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update hidden order inputs
|
||||||
|
updateOrderInputs();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update hidden inputs with the current order
|
||||||
|
const updateOrderInputs = () => {
|
||||||
|
const items = selectedList.querySelectorAll('.flex.items-center.justify-between');
|
||||||
|
if (items.length === 0) return;
|
||||||
|
|
||||||
|
// Remove any existing order input to avoid duplicates
|
||||||
|
const existingOrderInput = form.querySelector('input[name="config_order"]');
|
||||||
|
if (existingOrderInput) {
|
||||||
|
existingOrderInput.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new input with the current order
|
||||||
|
const orderedIds = Array.from(items).map(item => item.getAttribute('data-id'));
|
||||||
|
|
||||||
|
// Create a hidden input to store the order
|
||||||
|
const configOrderInput = document.createElement('input');
|
||||||
|
configOrderInput.type = 'hidden';
|
||||||
|
configOrderInput.name = 'config_order';
|
||||||
|
configOrderInput.value = orderedIds.join(',');
|
||||||
|
|
||||||
|
// Add the input to the form
|
||||||
|
form.appendChild(configOrderInput);
|
||||||
|
|
||||||
|
// Update the visible order numbers
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
const orderNum = index + 1;
|
||||||
|
const orderSpan = item.querySelector('span.rounded-full');
|
||||||
|
if (orderSpan) {
|
||||||
|
orderSpan.textContent = orderNum;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Updated order input:', configOrderInput.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize the selected list with saved order if available
|
||||||
|
updateSelectedItems(true);
|
||||||
|
|
||||||
|
// Handle checkbox changes
|
||||||
|
configList.addEventListener('change', (e) => {
|
||||||
|
if (e.target.matches('input[type="checkbox"]')) {
|
||||||
|
updateSelectedItems(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle reordering
|
||||||
|
selectedList.addEventListener('click', (e) => {
|
||||||
|
const listItem = e.target.closest('.flex.items-center.justify-between');
|
||||||
|
if (!listItem) return;
|
||||||
|
|
||||||
|
if (e.target.closest('.move-up')) {
|
||||||
|
const prev = listItem.previousElementSibling;
|
||||||
|
if (prev) {
|
||||||
|
selectedList.insertBefore(listItem, prev);
|
||||||
|
updateOrderInputs();
|
||||||
|
}
|
||||||
|
} else if (e.target.closest('.move-down')) {
|
||||||
|
const next = listItem.nextElementSibling;
|
||||||
|
if (next) {
|
||||||
|
selectedList.insertBefore(next, listItem);
|
||||||
|
updateOrderInputs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure the order input is updated before submission
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
updateOrderInputs();
|
||||||
|
console.log('Form submitted with order:', form.querySelector('input[name="config_order"]')?.value);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setup ordering for new job form
|
||||||
|
setupJobOrdering('config-list', 'selected-configs', 'new-job-form', null);
|
||||||
|
|
||||||
|
// Setup ordering for edit job form
|
||||||
|
const editJobForm = document.getElementById('edit-job-form');
|
||||||
|
const savedOrderEdit = editJobForm ? editJobForm.getAttribute('data-config-order') : null;
|
||||||
|
setupJobOrdering('config-list-edit', 'selected-configs-edit', 'edit-job-form', savedOrderEdit);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
@@ -95,6 +261,7 @@ templ JobForm(ctx context.Context, data JobFormData) {
|
|||||||
|
|
||||||
if data.IsNew {
|
if data.IsNew {
|
||||||
<form
|
<form
|
||||||
|
id="new-job-form"
|
||||||
class="space-y-6"
|
class="space-y-6"
|
||||||
hx-post="/jobs"
|
hx-post="/jobs"
|
||||||
hx-target="body"
|
hx-target="body"
|
||||||
@@ -161,10 +328,19 @@ templ JobForm(ctx context.Context, data JobFormData) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="mt-2 text-sm text-secondary-500 dark:text-secondary-400">
|
<!-- Selected Configurations Order List -->
|
||||||
<i class="fas fa-info-circle mr-1"></i>
|
<div class="mt-4">
|
||||||
Select one or more configurations to run on this schedule.
|
<label class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-2">
|
||||||
</p>
|
<i class="fas fa-sort-amount-down mr-1"></i> Execution Order
|
||||||
|
</label>
|
||||||
|
<div id="selected-configs" class="border border-secondary-300 dark:border-secondary-700 rounded-md p-3 min-h-20 bg-secondary-50 dark:bg-secondary-900">
|
||||||
|
<!-- Selected items will be populated by JavaScript -->
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-secondary-500 dark:text-secondary-400">
|
||||||
|
<i class="fas fa-info-circle mr-1"></i>
|
||||||
|
Use the arrows to change the order in which configurations will execute.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -341,10 +517,12 @@ templ JobForm(ctx context.Context, data JobFormData) {
|
|||||||
</form>
|
</form>
|
||||||
} else {
|
} else {
|
||||||
<form
|
<form
|
||||||
|
id="edit-job-form"
|
||||||
class="space-y-6"
|
class="space-y-6"
|
||||||
hx-post={ fmt.Sprintf("/jobs/%d", data.Job.ID) }
|
hx-post={ fmt.Sprintf("/jobs/%d", data.Job.ID) }
|
||||||
hx-target="body"
|
hx-target="body"
|
||||||
hx-boost="true">
|
hx-boost="true"
|
||||||
|
data-config-order={ data.Job.ConfigIDs }>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<label for="name" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Job Name</label>
|
<label for="name" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">Job Name</label>
|
||||||
@@ -411,10 +589,19 @@ templ JobForm(ctx context.Context, data JobFormData) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="mt-2 text-sm text-secondary-500 dark:text-secondary-400">
|
<!-- Selected Configurations Order List for edit -->
|
||||||
<i class="fas fa-info-circle mr-1"></i>
|
<div class="mt-4">
|
||||||
Select one or more configurations to run on this schedule.
|
<label class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-2">
|
||||||
</p>
|
<i class="fas fa-sort-amount-down mr-1"></i> Execution Order
|
||||||
|
</label>
|
||||||
|
<div id="selected-configs-edit" class="border border-secondary-300 dark:border-secondary-700 rounded-md p-3 min-h-20 bg-secondary-50 dark:bg-secondary-900">
|
||||||
|
<!-- Selected items will be populated by JavaScript -->
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-secondary-500 dark:text-secondary-400">
|
||||||
|
<i class="fas fa-info-circle mr-1"></i>
|
||||||
|
Use the arrows to change the order in which configurations will execute.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -599,7 +786,7 @@ templ JobForm(ctx context.Context, data JobFormData) {
|
|||||||
<div class="px-8 py-4 bg-secondary-50 dark:bg-secondary-800 border-t border-secondary-200 dark:border-secondary-700 text-center">
|
<div class="px-8 py-4 bg-secondary-50 dark:bg-secondary-800 border-t border-secondary-200 dark:border-secondary-700 text-center">
|
||||||
<p class="text-sm text-secondary-600 dark:text-secondary-400">
|
<p class="text-sm text-secondary-600 dark:text-secondary-400">
|
||||||
<i class="fas fa-info-circle mr-1"></i>
|
<i class="fas fa-info-circle mr-1"></i>
|
||||||
Jobs will run according to their schedule and execute the selected transfer configuration
|
Jobs will run according to their schedule and execute the selected transfer configurations in the order specified
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+37
-2
@@ -2,6 +2,7 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -186,6 +187,9 @@ func (j *Job) SetConfigIDsList(ids []uint) {
|
|||||||
// Join with commas
|
// Join with commas
|
||||||
j.ConfigIDs = strings.Join(strIDs, ",")
|
j.ConfigIDs = strings.Join(strIDs, ",")
|
||||||
|
|
||||||
|
// Debug log the final ConfigIDs string
|
||||||
|
log.Printf("SetConfigIDsList: Setting ConfigIDs to: %s (from %v)", j.ConfigIDs, ids)
|
||||||
|
|
||||||
// If there's at least one ID, set ConfigID to the first one for backward compatibility
|
// If there's at least one ID, set ConfigID to the first one for backward compatibility
|
||||||
if len(ids) > 0 {
|
if len(ids) > 0 {
|
||||||
j.ConfigID = ids[0]
|
j.ConfigID = ids[0]
|
||||||
@@ -376,8 +380,25 @@ func (db *DB) GetJob(id uint) (*Job, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) UpdateJob(job *Job) error {
|
func (db *DB) UpdateJob(job *Job) error {
|
||||||
|
log.Printf("UpdateJob: Updating job ID: %d, ConfigIDs: %s", job.ID, job.ConfigIDs)
|
||||||
|
|
||||||
// Use Omit to prevent GORM from updating or creating a new config
|
// Use Omit to prevent GORM from updating or creating a new config
|
||||||
return db.Omit("Config").Save(job).Error
|
return db.Model(&Job{}).
|
||||||
|
Where("id = ?", job.ID).
|
||||||
|
Omit("Config").
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"name": job.Name,
|
||||||
|
"config_id": job.ConfigID,
|
||||||
|
"config_ids": job.ConfigIDs, // Explicitly update config_ids
|
||||||
|
"schedule": job.Schedule,
|
||||||
|
"enabled": job.Enabled,
|
||||||
|
"webhook_enabled": job.WebhookEnabled,
|
||||||
|
"webhook_url": job.WebhookURL,
|
||||||
|
"webhook_secret": job.WebhookSecret,
|
||||||
|
"webhook_headers": job.WebhookHeaders,
|
||||||
|
"notify_on_success": job.NotifyOnSuccess,
|
||||||
|
"notify_on_failure": job.NotifyOnFailure,
|
||||||
|
}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) DeleteJob(id uint) error {
|
func (db *DB) DeleteJob(id uint) error {
|
||||||
@@ -965,7 +986,21 @@ func (db *DB) GetConfigsForJob(jobID uint) ([]TransferConfig, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return configs, nil
|
// Create a map for quick lookup
|
||||||
|
configMap := make(map[uint]TransferConfig)
|
||||||
|
for _, config := range configs {
|
||||||
|
configMap[config.ID] = config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new slice with configs in the correct order
|
||||||
|
orderedConfigs := make([]TransferConfig, 0, len(configs))
|
||||||
|
for _, configID := range configIDs {
|
||||||
|
if config, exists := configMap[configID]; exists {
|
||||||
|
orderedConfigs = append(orderedConfigs, config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return orderedConfigs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSkipProcessedFiles returns the value of SkipProcessedFiles with a default if nil
|
// GetSkipProcessedFiles returns the value of SkipProcessedFiles with a default if nil
|
||||||
|
|||||||
@@ -270,3 +270,182 @@ func (s *Service) sendEmail(toEmail, subject, htmlContent string) error {
|
|||||||
return client.Quit()
|
return client.Quit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendTestEmail sends a test email to verify email configuration
|
||||||
|
func (s *Service) SendTestEmail(toEmail, subject, message string) error {
|
||||||
|
if !s.Config.Email.Enabled {
|
||||||
|
return fmt.Errorf("email service is disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use default subject if not provided
|
||||||
|
if subject == "" {
|
||||||
|
subject = "Test Email from GoMFT"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use default message if not provided
|
||||||
|
if message == "" {
|
||||||
|
message = "This is a test email from GoMFT to verify the email configuration is working correctly."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create email data for template
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"Subject": subject,
|
||||||
|
"Message": message,
|
||||||
|
"AppName": "GoMFT",
|
||||||
|
"Year": time.Now().Year(),
|
||||||
|
"SMTPServer": s.Config.Email.Host,
|
||||||
|
"SMTPPort": s.Config.Email.Port,
|
||||||
|
"FromEmail": s.Config.Email.FromEmail,
|
||||||
|
"CurrentTime": time.Now().Format(time.RFC1123Z),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate email content
|
||||||
|
htmlContent, err := s.generateTestEmailHTML(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the email
|
||||||
|
return s.sendEmail(toEmail, subject, htmlContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateTestEmailHTML generates the HTML content for test emails
|
||||||
|
func (s *Service) generateTestEmailHTML(data map[string]interface{}) (string, error) {
|
||||||
|
// HTML template for test email
|
||||||
|
tmpl, err := template.New("testEmail").Parse(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{.Subject}}</title>
|
||||||
|
<style>
|
||||||
|
/* Base styles */
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
color: #374151;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
margin: 0 auto 15px;
|
||||||
|
background-color: #2563eb;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.logo-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #111827;
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0 0 15px;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
.info-box {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.info-label {
|
||||||
|
font-weight: bold;
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
.note {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
padding: 20px 0;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<div class="logo">
|
||||||
|
<div class="logo-icon">G</div>
|
||||||
|
</div>
|
||||||
|
<h1>{{.Subject}}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{.Message}}</p>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">SMTP Server:</div>
|
||||||
|
<div>{{.SMTPServer}}:{{.SMTPPort}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">From:</div>
|
||||||
|
<div>{{.FromEmail}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">Sent:</div>
|
||||||
|
<div>{{.CurrentTime}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="note">
|
||||||
|
<p>This is a test email sent from the GoMFT admin interface. If you've received this email, your email configuration is working correctly.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>© {{.Year}} {{.AppName}}. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result bytes.Buffer
|
||||||
|
if err := tmpl.Execute(&result, data); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.String(), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -173,6 +173,11 @@ func NewLogger() *Logger {
|
|||||||
filepath.Join(logsDir, "scheduler.log"), maxSize, maxBackups, maxAge, compress, logLevel.String())
|
filepath.Join(logsDir, "scheduler.log"), maxSize, maxBackups, maxAge, compress, logLevel.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if logLevel >= LogLevelDebug {
|
||||||
|
logger.Debug.Printf("Log rotation details: file=%s, maxSize=%dMB, maxBackups=%d, maxAge=%d days, compress=%v",
|
||||||
|
filepath.Join(logsDir, "scheduler.log"), maxSize, maxBackups, maxAge, compress)
|
||||||
|
}
|
||||||
|
|
||||||
return logger
|
return logger
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,6 +263,8 @@ func (s *Scheduler) loadJobs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Scheduler) ScheduleJob(job *db.Job) error {
|
func (s *Scheduler) ScheduleJob(job *db.Job) error {
|
||||||
|
s.log.LogDebug("Attempting to schedule job ID %d: %+v", job.ID, job)
|
||||||
|
|
||||||
s.log.LogInfo("Scheduling job %d: %s with schedule %s", job.ID, job.Name, job.Schedule)
|
s.log.LogInfo("Scheduling job %d: %s with schedule %s", job.ID, job.Name, job.Schedule)
|
||||||
|
|
||||||
// Remove existing job if it exists
|
// Remove existing job if it exists
|
||||||
@@ -279,6 +286,8 @@ func (s *Scheduler) ScheduleJob(job *db.Job) error {
|
|||||||
schedule = "0 " + schedule
|
schedule = "0 " + schedule
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.log.LogDebug("Converted schedule from '%s' to '%s'", job.Schedule, schedule)
|
||||||
|
|
||||||
// Validate cron expression
|
// Validate cron expression
|
||||||
parser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
|
parser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
|
||||||
_, err := parser.Parse(schedule)
|
_, err := parser.Parse(schedule)
|
||||||
@@ -286,6 +295,8 @@ func (s *Scheduler) ScheduleJob(job *db.Job) error {
|
|||||||
return fmt.Errorf("invalid cron expression '%s': %w", job.Schedule, err)
|
return fmt.Errorf("invalid cron expression '%s': %w", job.Schedule, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.log.LogDebug("Validated cron expression '%s' for job %d", schedule, job.ID)
|
||||||
|
|
||||||
// Schedule the job
|
// Schedule the job
|
||||||
entryID, err := s.cron.AddFunc(job.Schedule, func() {
|
entryID, err := s.cron.AddFunc(job.Schedule, func() {
|
||||||
s.executeJob(job.ID)
|
s.executeJob(job.ID)
|
||||||
@@ -296,6 +307,8 @@ func (s *Scheduler) ScheduleJob(job *db.Job) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.log.LogDebug("Scheduled job %d with cron entry ID %d", job.ID, entryID)
|
||||||
|
|
||||||
// Store mapping of job ID to cron entry ID
|
// Store mapping of job ID to cron entry ID
|
||||||
s.jobMutex.Lock()
|
s.jobMutex.Lock()
|
||||||
s.jobs[job.ID] = entryID
|
s.jobs[job.ID] = entryID
|
||||||
@@ -313,6 +326,9 @@ func (s *Scheduler) ScheduleJob(job *db.Job) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Scheduler) executeJob(jobID uint) {
|
func (s *Scheduler) executeJob(jobID uint) {
|
||||||
|
s.log.LogDebug("Entering executeJob for job ID %d", jobID)
|
||||||
|
defer s.log.LogDebug("Exiting executeJob for job ID %d", jobID)
|
||||||
|
|
||||||
s.log.LogInfo("Starting execution of job %d", jobID)
|
s.log.LogInfo("Starting execution of job %d", jobID)
|
||||||
|
|
||||||
// Get job details
|
// Get job details
|
||||||
@@ -322,6 +338,8 @@ func (s *Scheduler) executeJob(jobID uint) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.log.LogDebug("Loaded job details: %+v", job)
|
||||||
|
|
||||||
// Get all configurations associated with this job
|
// Get all configurations associated with this job
|
||||||
configs, err := s.db.GetConfigsForJob(jobID)
|
configs, err := s.db.GetConfigsForJob(jobID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -329,12 +347,45 @@ func (s *Scheduler) executeJob(jobID uint) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.log.LogDebug("Loaded %d configurations for job %d", len(configs), jobID)
|
||||||
|
|
||||||
if len(configs) == 0 {
|
if len(configs) == 0 {
|
||||||
s.log.LogError("Error: job %d has no associated configurations", jobID)
|
s.log.LogError("Error: job %d has no associated configurations", jobID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.log.LogInfo("Loaded job %d with %d configurations", jobID, len(configs))
|
// Get the ordered config IDs from the job
|
||||||
|
orderedConfigIDs := job.GetConfigIDsList()
|
||||||
|
s.log.LogDebug("Ordered config IDs for job %d: %v", jobID, orderedConfigIDs)
|
||||||
|
|
||||||
|
// Create a map of configs for easy lookup
|
||||||
|
configMap := make(map[uint]db.TransferConfig)
|
||||||
|
for _, config := range configs {
|
||||||
|
configMap[config.ID] = config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process configurations in the specified order
|
||||||
|
var orderedConfigs []db.TransferConfig
|
||||||
|
|
||||||
|
// First, add configs in the order specified in the job's ConfigIDs
|
||||||
|
for _, configID := range orderedConfigIDs {
|
||||||
|
if config, exists := configMap[configID]; exists {
|
||||||
|
orderedConfigs = append(orderedConfigs, config)
|
||||||
|
delete(configMap, configID) // Remove from map to avoid duplicates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any remaining configs not in the ordered list (shouldn't happen, but just in case)
|
||||||
|
for _, config := range configMap {
|
||||||
|
orderedConfigs = append(orderedConfigs, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.log.LogInfo("Processing job %d with %d configurations in specified order", jobID, len(orderedConfigs))
|
||||||
|
|
||||||
|
// Log the order of execution
|
||||||
|
for i, config := range orderedConfigs {
|
||||||
|
s.log.LogDebug("Execution order %d/%d: Config ID %d (%s)", i+1, len(orderedConfigs), config.ID, config.Name)
|
||||||
|
}
|
||||||
|
|
||||||
// Update job last run time
|
// Update job last run time
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
@@ -343,9 +394,9 @@ func (s *Scheduler) executeJob(jobID uint) {
|
|||||||
s.log.LogError("Error updating job last run time for job %d: %v", jobID, err)
|
s.log.LogError("Error updating job last run time for job %d: %v", jobID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each configuration
|
// Process each configuration in the specified order
|
||||||
for i, config := range configs {
|
for i, config := range orderedConfigs {
|
||||||
s.processConfiguration(&job, &config, i+1, len(configs))
|
s.processConfiguration(&job, &config, i+1, len(orderedConfigs))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update next run time after execution
|
// Update next run time after execution
|
||||||
@@ -366,6 +417,8 @@ func (s *Scheduler) executeJob(jobID uint) {
|
|||||||
|
|
||||||
// processConfiguration processes a single configuration for a job
|
// processConfiguration processes a single configuration for a job
|
||||||
func (s *Scheduler) processConfiguration(job *db.Job, config *db.TransferConfig, index int, totalConfigs int) {
|
func (s *Scheduler) processConfiguration(job *db.Job, config *db.TransferConfig, index int, totalConfigs int) {
|
||||||
|
s.log.LogDebug("Processing configuration %d: %+v", config.ID, config)
|
||||||
|
|
||||||
s.log.LogInfo("Processing configuration %d (%d/%d) for job %d: source=%s:%s, dest=%s:%s",
|
s.log.LogInfo("Processing configuration %d (%d/%d) for job %d: source=%s:%s, dest=%s:%s",
|
||||||
config.ID,
|
config.ID,
|
||||||
index,
|
index,
|
||||||
@@ -392,12 +445,16 @@ func (s *Scheduler) processConfiguration(job *db.Job, config *db.TransferConfig,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.log.LogDebug("Creating job history record: %+v", history)
|
||||||
|
|
||||||
// Execute the configuration transfer
|
// Execute the configuration transfer
|
||||||
s.executeConfigTransfer(*job, *config, history)
|
s.executeConfigTransfer(*job, *config, history)
|
||||||
}
|
}
|
||||||
|
|
||||||
// executeConfigTransfer performs the actual file transfer for a single configuration
|
// executeConfigTransfer performs the actual file transfer for a single configuration
|
||||||
func (s *Scheduler) executeConfigTransfer(job db.Job, config db.TransferConfig, history *db.JobHistory) {
|
func (s *Scheduler) executeConfigTransfer(job db.Job, config db.TransferConfig, history *db.JobHistory) {
|
||||||
|
s.log.LogDebug("Starting transfer for config %d with params: %+v", config.ID, config)
|
||||||
|
|
||||||
// Track files already processed in this job execution to prevent duplicates
|
// Track files already processed in this job execution to prevent duplicates
|
||||||
processedFiles := make(map[string]bool)
|
processedFiles := make(map[string]bool)
|
||||||
|
|
||||||
@@ -447,7 +504,7 @@ func (s *Scheduler) executeConfigTransfer(job db.Job, config db.TransferConfig,
|
|||||||
listArgs = append(listArgs, sourceListPath)
|
listArgs = append(listArgs, sourceListPath)
|
||||||
|
|
||||||
// Execute lsjson command
|
// Execute lsjson command
|
||||||
s.log.LogInfo("Listing files with metadata for job %d, config %d: rclone %s", job.ID, config.ID, strings.Join(listArgs, " "))
|
s.log.LogDebug("Full lsjson command: %s %v", os.Getenv("RCLONE_PATH"), listArgs)
|
||||||
rclonePath := os.Getenv("RCLONE_PATH")
|
rclonePath := os.Getenv("RCLONE_PATH")
|
||||||
if rclonePath == "" {
|
if rclonePath == "" {
|
||||||
rclonePath = "rclone"
|
rclonePath = "rclone"
|
||||||
@@ -455,6 +512,19 @@ func (s *Scheduler) executeConfigTransfer(job db.Job, config db.TransferConfig,
|
|||||||
listCmd := exec.Command(rclonePath, listArgs...)
|
listCmd := exec.Command(rclonePath, listArgs...)
|
||||||
listOutput, listErr := listCmd.CombinedOutput()
|
listOutput, listErr := listCmd.CombinedOutput()
|
||||||
|
|
||||||
|
// Add debug logging of raw output
|
||||||
|
if listErr == nil {
|
||||||
|
s.log.LogDebug("Raw lsjson output for job %d config %d:\n%s",
|
||||||
|
job.ID,
|
||||||
|
config.ID,
|
||||||
|
string(listOutput))
|
||||||
|
} else {
|
||||||
|
s.log.LogDebug("Raw lsjson output (error case) for job %d config %d:\n%s",
|
||||||
|
job.ID,
|
||||||
|
config.ID,
|
||||||
|
string(listOutput))
|
||||||
|
}
|
||||||
|
|
||||||
if listErr != nil {
|
if listErr != nil {
|
||||||
s.log.LogError("Error listing files for job %d, config %d: %v", job.ID, config.ID, listErr)
|
s.log.LogError("Error listing files for job %d, config %d: %v", job.ID, config.ID, listErr)
|
||||||
// s.log.Debug.Printf("Output: %s", string(listOutput))
|
// s.log.Debug.Printf("Output: %s", string(listOutput))
|
||||||
@@ -550,7 +620,7 @@ func (s *Scheduler) executeConfigTransfer(job db.Job, config db.TransferConfig,
|
|||||||
concurrencySemaphore := make(chan struct{}, maxConcurrent)
|
concurrencySemaphore := make(chan struct{}, maxConcurrent)
|
||||||
|
|
||||||
// Process each file individually
|
// Process each file individually
|
||||||
for _, fileEntry := range files {
|
for i, fileEntry := range files {
|
||||||
fileName, ok := fileEntry["Path"].(string)
|
fileName, ok := fileEntry["Path"].(string)
|
||||||
if !ok || fileName == "" {
|
if !ok || fileName == "" {
|
||||||
continue
|
continue
|
||||||
@@ -666,7 +736,8 @@ func (s *Scheduler) executeConfigTransfer(job db.Job, config db.TransferConfig,
|
|||||||
currentModTime := modTime
|
currentModTime := modTime
|
||||||
|
|
||||||
// Log the file information that will be processed
|
// Log the file information that will be processed
|
||||||
s.log.LogDebug("Processing file: %s, size: %d, hash: %s", currentFileName, currentFileSize, currentFileHash)
|
s.log.LogDebug("Processing file %d/%d: %s (Size: %d, Hash: %s)",
|
||||||
|
i+1, len(files), currentFileName, currentFileSize, currentFileHash)
|
||||||
|
|
||||||
// Start goroutine for concurrent processing
|
// Start goroutine for concurrent processing
|
||||||
go func() {
|
go func() {
|
||||||
@@ -740,13 +811,8 @@ func (s *Scheduler) executeConfigTransfer(job db.Job, config db.TransferConfig,
|
|||||||
transferArgs = append(transferArgs, sourcePath, destPath)
|
transferArgs = append(transferArgs, sourcePath, destPath)
|
||||||
|
|
||||||
// Execute transfer for this file
|
// Execute transfer for this file
|
||||||
s.log.LogInfo("Executing rclone transfer command for job %d, config %d, file %s: rclone %s",
|
s.log.LogDebug("Full transfer command: %s %v", rclonePath, transferArgs)
|
||||||
job.ID, config.ID, currentFileName, strings.Join(transferArgs, " "))
|
s.log.LogDebug("Environment: RCLONE_PATH=%s", os.Getenv("RCLONE_PATH"))
|
||||||
// Get the rclone path from the environment variable or use the default path
|
|
||||||
rclonePath := os.Getenv("RCLONE_PATH")
|
|
||||||
if rclonePath == "" {
|
|
||||||
rclonePath = "rclone"
|
|
||||||
}
|
|
||||||
cmd := exec.Command(rclonePath, transferArgs...)
|
cmd := exec.Command(rclonePath, transferArgs...)
|
||||||
fileOutput, fileErr := cmd.CombinedOutput()
|
fileOutput, fileErr := cmd.CombinedOutput()
|
||||||
|
|
||||||
@@ -1087,6 +1153,8 @@ func (s *Scheduler) sendWebhookNotification(job *db.Job, history *db.JobHistory,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.log.LogDebug("Webhook payload: %s", string(jsonPayload))
|
||||||
|
|
||||||
// Create HTTP request
|
// Create HTTP request
|
||||||
req, err := http.NewRequest("POST", job.WebhookURL, bytes.NewBuffer(jsonPayload))
|
req, err := http.NewRequest("POST", job.WebhookURL, bytes.NewBuffer(jsonPayload))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1116,6 +1184,8 @@ func (s *Scheduler) sendWebhookNotification(job *db.Job, history *db.JobHistory,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.log.LogDebug("Webhook headers: %+v", req.Header)
|
||||||
|
|
||||||
// Send the request with a timeout
|
// Send the request with a timeout
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Timeout: 10 * time.Second,
|
Timeout: 10 * time.Second,
|
||||||
|
|||||||
@@ -63,17 +63,28 @@ func (h *Handlers) HandleAdminTools(c *gin.Context) {
|
|||||||
data.TotalUsers = int(totalUsers)
|
data.TotalUsers = int(totalUsers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get last backup time and backup count
|
// Get backup info (last backup time and count)
|
||||||
data.LastBackupTime, data.BackupCount = h.getBackupInfo()
|
lastBackup, backupCount := h.getBackupInfo()
|
||||||
|
data.LastBackupTime = lastBackup
|
||||||
|
data.BackupCount = backupCount
|
||||||
|
|
||||||
// Get list of backup files
|
// Get list of backup files
|
||||||
data.BackupFiles = h.getBackupFiles()
|
data.BackupFiles = h.getBackupFiles()
|
||||||
|
|
||||||
// Check for maintenance issues
|
// Get maintenance message if any
|
||||||
data.MaintenanceMessage = h.checkMaintenanceIssues()
|
data.MaintenanceMessage = h.checkMaintenanceIssues()
|
||||||
|
|
||||||
// Render the admin tools page
|
// Add SMTP server info if available
|
||||||
components.AdminTools(components.CreateTemplateContext(c), data).Render(c, c.Writer)
|
if h.Email != nil && h.Email.Config != nil && h.Email.Config.Email.Host != "" {
|
||||||
|
smtpServer := h.Email.Config.Email.Host
|
||||||
|
if h.Email.Config.Email.Port != 0 {
|
||||||
|
data.SmtpServer = fmt.Sprintf("%s:%d", smtpServer, h.Email.Config.Email.Port)
|
||||||
|
} else {
|
||||||
|
data.SmtpServer = smtpServer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
components.AdminTools(c.Request.Context(), data).Render(c.Request.Context(), c.Writer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleBackupDatabase handles the backup database request
|
// HandleBackupDatabase handles the backup database request
|
||||||
@@ -1368,3 +1379,43 @@ func (h *Handlers) HandleImportConfigsFromFile(c *gin.Context) {
|
|||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("%d configs imported successfully", imported)})
|
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("%d configs imported successfully", imported)})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleTestEmail handles the POST /admin/test-email route
|
||||||
|
func (h *Handlers) HandleTestEmail(c *gin.Context) {
|
||||||
|
// Parse the form
|
||||||
|
recipient := c.PostForm("recipient")
|
||||||
|
subject := c.PostForm("subject")
|
||||||
|
message := c.PostForm("message")
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if recipient == "" {
|
||||||
|
components.EmailTestToast(false, "Recipient email is required").Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get SMTP server info for display
|
||||||
|
smtpServer := ""
|
||||||
|
if h.Email != nil && h.Email.Config != nil && h.Email.Config.Email.Host != "" {
|
||||||
|
smtpServer = h.Email.Config.Email.Host
|
||||||
|
if h.Email.Config.Email.Port != 0 {
|
||||||
|
smtpServer = fmt.Sprintf("%s:%d", smtpServer, h.Email.Config.Email.Port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the test email
|
||||||
|
if h.Email == nil {
|
||||||
|
components.EmailTestToast(false, "Email service is not configured").Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.Email.SendTestEmail(recipient, subject, message)
|
||||||
|
if err != nil {
|
||||||
|
// Failed to send email
|
||||||
|
components.EmailTestToast(false, fmt.Sprintf("Failed to send email: %v", err)).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email sent successfully
|
||||||
|
successMsg := fmt.Sprintf("Test email sent successfully to %s", recipient)
|
||||||
|
components.EmailTestToast(true, successMsg).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/starfleetcptn/gomft/components"
|
"github.com/starfleetcptn/gomft/components"
|
||||||
@@ -141,6 +143,9 @@ func (h *Handlers) HandleEditJob(c *gin.Context) {
|
|||||||
func (h *Handlers) HandleCreateJob(c *gin.Context) {
|
func (h *Handlers) HandleCreateJob(c *gin.Context) {
|
||||||
userID := c.GetUint("userID")
|
userID := c.GetUint("userID")
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
log.Printf("HandleCreateJob: Form data received: %v", c.Request.PostForm)
|
||||||
|
|
||||||
// Parse form data
|
// Parse form data
|
||||||
var job db.Job
|
var job db.Job
|
||||||
if err := c.ShouldBind(&job); err != nil {
|
if err := c.ShouldBind(&job); err != nil {
|
||||||
@@ -148,6 +153,9 @@ func (h *Handlers) HandleCreateJob(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
log.Printf("HandleCreateJob: Job after binding: %+v", job)
|
||||||
|
|
||||||
// Get multiple config IDs from form
|
// Get multiple config IDs from form
|
||||||
configIDs := c.PostFormArray("config_ids[]")
|
configIDs := c.PostFormArray("config_ids[]")
|
||||||
if len(configIDs) == 0 {
|
if len(configIDs) == 0 {
|
||||||
@@ -155,35 +163,83 @@ func (h *Handlers) HandleCreateJob(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
log.Printf("HandleCreateJob: config_ids[]: %v", configIDs)
|
||||||
|
|
||||||
// Process config IDs
|
// Process config IDs
|
||||||
var configIDsList []uint
|
var configIDsList []uint
|
||||||
for _, configIDStr := range configIDs {
|
|
||||||
configID, err := strconv.ParseUint(configIDStr, 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
c.String(http.StatusBadRequest, "Invalid configuration ID format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that the config exists and belongs to the user
|
// Check if we have an explicit order specified
|
||||||
var config db.TransferConfig
|
configOrder := c.PostForm("config_order")
|
||||||
if err := h.DB.First(&config, configID).Error; err != nil {
|
log.Printf("HandleCreateJob: config_order: %s", configOrder)
|
||||||
c.String(http.StatusBadRequest, "Invalid configuration selected")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the config belongs to the user
|
if configOrder != "" {
|
||||||
if config.CreatedBy != userID {
|
// Parse the ordered list
|
||||||
// Check if user is admin
|
orderStrings := strings.Split(configOrder, ",")
|
||||||
isAdmin, exists := c.Get("isAdmin")
|
log.Printf("HandleCreateJob: order strings: %v", orderStrings)
|
||||||
if !exists || isAdmin != true {
|
|
||||||
c.String(http.StatusForbidden, "You do not have permission to use this configuration")
|
for _, configIDStr := range orderStrings {
|
||||||
|
configID, err := strconv.ParseUint(configIDStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("HandleCreateJob: Error parsing config ID: %v", err)
|
||||||
|
c.String(http.StatusBadRequest, "Invalid configuration ID format in order")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
configIDsList = append(configIDsList, uint(configID))
|
// Verify that the config exists and belongs to the user
|
||||||
|
var config db.TransferConfig
|
||||||
|
if err := h.DB.First(&config, configID).Error; err != nil {
|
||||||
|
log.Printf("HandleCreateJob: Invalid config ID: %d, error: %v", configID, err)
|
||||||
|
c.String(http.StatusBadRequest, "Invalid configuration selected")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the config belongs to the user
|
||||||
|
if config.CreatedBy != userID {
|
||||||
|
// Check if user is admin
|
||||||
|
isAdmin, exists := c.Get("isAdmin")
|
||||||
|
if !exists || isAdmin != true {
|
||||||
|
c.String(http.StatusForbidden, "You do not have permission to use this configuration")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configIDsList = append(configIDsList, uint(configID))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fall back to unordered config IDs
|
||||||
|
log.Printf("HandleCreateJob: No config_order found, using checkbox order")
|
||||||
|
for _, configIDStr := range configIDs {
|
||||||
|
configID, err := strconv.ParseUint(configIDStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusBadRequest, "Invalid configuration ID format")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the config exists and belongs to the user
|
||||||
|
var config db.TransferConfig
|
||||||
|
if err := h.DB.First(&config, configID).Error; err != nil {
|
||||||
|
c.String(http.StatusBadRequest, "Invalid configuration selected")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the config belongs to the user
|
||||||
|
if config.CreatedBy != userID {
|
||||||
|
// Check if user is admin
|
||||||
|
isAdmin, exists := c.Get("isAdmin")
|
||||||
|
if !exists || isAdmin != true {
|
||||||
|
c.String(http.StatusForbidden, "You do not have permission to use this configuration")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configIDsList = append(configIDsList, uint(configID))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
log.Printf("HandleCreateJob: Final configIDsList: %v", configIDsList)
|
||||||
|
|
||||||
// Set the first config ID for backward compatibility
|
// Set the first config ID for backward compatibility
|
||||||
if len(configIDsList) > 0 {
|
if len(configIDsList) > 0 {
|
||||||
job.ConfigID = configIDsList[0]
|
job.ConfigID = configIDsList[0]
|
||||||
@@ -214,6 +270,10 @@ func (h *Handlers) HandleCreateJob(c *gin.Context) {
|
|||||||
// Set the config IDs list
|
// Set the config IDs list
|
||||||
job.SetConfigIDsList(configIDsList)
|
job.SetConfigIDsList(configIDsList)
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
log.Printf("HandleCreateJob: Job after setting ConfigIDsList: %+v", job)
|
||||||
|
log.Printf("HandleCreateJob: Job.ConfigIDs: %s", job.ConfigIDs)
|
||||||
|
|
||||||
// Set the boolean fields - handle both "on" and "true" values for checkboxes
|
// Set the boolean fields - handle both "on" and "true" values for checkboxes
|
||||||
enabledVal := c.Request.FormValue("enabled")
|
enabledVal := c.Request.FormValue("enabled")
|
||||||
jobEnabledValue := enabledVal == "on" || enabledVal == "true"
|
jobEnabledValue := enabledVal == "on" || enabledVal == "true"
|
||||||
@@ -239,10 +299,13 @@ func (h *Handlers) HandleCreateJob(c *gin.Context) {
|
|||||||
|
|
||||||
// Create the job
|
// Create the job
|
||||||
if err := h.DB.CreateJob(&job); err != nil {
|
if err := h.DB.CreateJob(&job); err != nil {
|
||||||
|
log.Printf("HandleCreateJob: Error creating job: %v", err)
|
||||||
c.String(http.StatusInternalServerError, "Failed to create job")
|
c.String(http.StatusInternalServerError, "Failed to create job")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("HandleCreateJob: Job successfully created with ID: %d", job.ID)
|
||||||
|
|
||||||
// Schedule the job with the scheduler
|
// Schedule the job with the scheduler
|
||||||
if err := h.Scheduler.ScheduleJob(&job); err != nil {
|
if err := h.Scheduler.ScheduleJob(&job); err != nil {
|
||||||
c.String(http.StatusInternalServerError, "Job created but scheduling failed: "+err.Error())
|
c.String(http.StatusInternalServerError, "Job created but scheduling failed: "+err.Error())
|
||||||
@@ -257,8 +320,13 @@ func (h *Handlers) HandleUpdateJob(c *gin.Context) {
|
|||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
userID := c.GetUint("userID")
|
userID := c.GetUint("userID")
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
log.Printf("HandleUpdateJob: Updating job ID: %s", id)
|
||||||
|
log.Printf("HandleUpdateJob: Form data received: %v", c.Request.PostForm)
|
||||||
|
|
||||||
var job db.Job
|
var job db.Job
|
||||||
if err := h.DB.First(&job, id).Error; err != nil {
|
if err := h.DB.First(&job, id).Error; err != nil {
|
||||||
|
log.Printf("HandleUpdateJob: Job not found: %v", err)
|
||||||
c.String(http.StatusNotFound, "Job not found")
|
c.String(http.StatusNotFound, "Job not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -275,49 +343,102 @@ func (h *Handlers) HandleUpdateJob(c *gin.Context) {
|
|||||||
|
|
||||||
// Get the old job values for comparison
|
// Get the old job values for comparison
|
||||||
oldJob := job
|
oldJob := job
|
||||||
|
log.Printf("HandleUpdateJob: Original job: %+v", oldJob)
|
||||||
|
log.Printf("HandleUpdateJob: Original job ConfigIDs: %s", oldJob.ConfigIDs)
|
||||||
|
|
||||||
// Parse form data
|
// Parse form data
|
||||||
if err := c.ShouldBind(&job); err != nil {
|
if err := c.ShouldBind(&job); err != nil {
|
||||||
|
log.Printf("HandleUpdateJob: Error binding form data: %v", err)
|
||||||
c.String(http.StatusBadRequest, "Invalid form data")
|
c.String(http.StatusBadRequest, "Invalid form data")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("HandleUpdateJob: Job after binding: %+v", job)
|
||||||
|
|
||||||
// Get multiple config IDs from form
|
// Get multiple config IDs from form
|
||||||
configIDs := c.PostFormArray("config_ids[]")
|
configIDs := c.PostFormArray("config_ids[]")
|
||||||
if len(configIDs) == 0 {
|
if len(configIDs) == 0 {
|
||||||
|
log.Printf("HandleUpdateJob: No config_ids[] found in form data")
|
||||||
c.String(http.StatusBadRequest, "At least one configuration must be selected")
|
c.String(http.StatusBadRequest, "At least one configuration must be selected")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("HandleUpdateJob: config_ids[]: %v", configIDs)
|
||||||
|
|
||||||
// Process config IDs
|
// Process config IDs
|
||||||
var configIDsList []uint
|
var configIDsList []uint
|
||||||
for _, configIDStr := range configIDs {
|
|
||||||
configID, err := strconv.ParseUint(configIDStr, 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
c.String(http.StatusBadRequest, "Invalid configuration ID format")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that the config exists
|
// Check if we have an explicit order specified
|
||||||
var config db.TransferConfig
|
configOrder := c.PostForm("config_order")
|
||||||
if err := h.DB.First(&config, configID).Error; err != nil {
|
log.Printf("HandleUpdateJob: config_order: %s", configOrder)
|
||||||
c.String(http.StatusBadRequest, "Invalid configuration selected")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the config belongs to the user
|
if configOrder != "" {
|
||||||
if config.CreatedBy != userID {
|
// Parse the ordered list
|
||||||
// Check if user is admin
|
orderStrings := strings.Split(configOrder, ",")
|
||||||
isAdmin, exists := c.Get("isAdmin")
|
log.Printf("HandleUpdateJob: order strings: %v", orderStrings)
|
||||||
if !exists || isAdmin != true {
|
|
||||||
c.String(http.StatusForbidden, "You do not have permission to use this configuration")
|
for _, configIDStr := range orderStrings {
|
||||||
|
configID, err := strconv.ParseUint(configIDStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("HandleUpdateJob: Error parsing config ID: %v", err)
|
||||||
|
c.String(http.StatusBadRequest, "Invalid configuration ID format in order")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
configIDsList = append(configIDsList, uint(configID))
|
// Verify that the config exists
|
||||||
|
var config db.TransferConfig
|
||||||
|
if err := h.DB.First(&config, configID).Error; err != nil {
|
||||||
|
log.Printf("HandleUpdateJob: Invalid config ID: %d, error: %v", configID, err)
|
||||||
|
c.String(http.StatusBadRequest, "Invalid configuration selected")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the config belongs to the user
|
||||||
|
if config.CreatedBy != userID {
|
||||||
|
// Check if user is admin
|
||||||
|
isAdmin, exists := c.Get("isAdmin")
|
||||||
|
if !exists || isAdmin != true {
|
||||||
|
c.String(http.StatusForbidden, "You do not have permission to use this configuration")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configIDsList = append(configIDsList, uint(configID))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fall back to unordered config IDs
|
||||||
|
log.Printf("HandleUpdateJob: No config_order found, using checkbox order")
|
||||||
|
for _, configIDStr := range configIDs {
|
||||||
|
configID, err := strconv.ParseUint(configIDStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusBadRequest, "Invalid configuration ID format")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the config exists
|
||||||
|
var config db.TransferConfig
|
||||||
|
if err := h.DB.First(&config, configID).Error; err != nil {
|
||||||
|
c.String(http.StatusBadRequest, "Invalid configuration selected")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the config belongs to the user
|
||||||
|
if config.CreatedBy != userID {
|
||||||
|
// Check if user is admin
|
||||||
|
isAdmin, exists := c.Get("isAdmin")
|
||||||
|
if !exists || isAdmin != true {
|
||||||
|
c.String(http.StatusForbidden, "You do not have permission to use this configuration")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configIDsList = append(configIDsList, uint(configID))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
log.Printf("HandleUpdateJob: Final configIDsList: %v", configIDsList)
|
||||||
|
|
||||||
// Set the first config ID for backward compatibility
|
// Set the first config ID for backward compatibility
|
||||||
if len(configIDsList) > 0 {
|
if len(configIDsList) > 0 {
|
||||||
job.ConfigID = configIDsList[0]
|
job.ConfigID = configIDsList[0]
|
||||||
@@ -332,6 +453,10 @@ func (h *Handlers) HandleUpdateJob(c *gin.Context) {
|
|||||||
// Set the config IDs list
|
// Set the config IDs list
|
||||||
job.SetConfigIDsList(configIDsList)
|
job.SetConfigIDsList(configIDsList)
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
log.Printf("HandleUpdateJob: Job after setting ConfigIDsList: %+v", job)
|
||||||
|
log.Printf("HandleUpdateJob: Job.ConfigIDs: %s", job.ConfigIDs)
|
||||||
|
|
||||||
// Set the boolean fields - handle both "on" and "true" values for checkboxes
|
// Set the boolean fields - handle both "on" and "true" values for checkboxes
|
||||||
enabledVal := c.Request.FormValue("enabled")
|
enabledVal := c.Request.FormValue("enabled")
|
||||||
jobEnabledValue := enabledVal == "on" || enabledVal == "true"
|
jobEnabledValue := enabledVal == "on" || enabledVal == "true"
|
||||||
@@ -357,10 +482,13 @@ func (h *Handlers) HandleUpdateJob(c *gin.Context) {
|
|||||||
job.Config = db.TransferConfig{}
|
job.Config = db.TransferConfig{}
|
||||||
|
|
||||||
if err := h.DB.UpdateJob(&job); err != nil {
|
if err := h.DB.UpdateJob(&job); err != nil {
|
||||||
|
log.Printf("HandleUpdateJob: Error updating job: %v", err)
|
||||||
c.String(http.StatusInternalServerError, "Failed to update job")
|
c.String(http.StatusInternalServerError, "Failed to update job")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("HandleUpdateJob: Job successfully updated")
|
||||||
|
|
||||||
// Reschedule the job with the scheduler
|
// Reschedule the job with the scheduler
|
||||||
if err := h.Scheduler.ScheduleJob(&job); err != nil {
|
if err := h.Scheduler.ScheduleJob(&job); err != nil {
|
||||||
c.String(http.StatusInternalServerError, "Job updated but scheduling failed: "+err.Error())
|
c.String(http.StatusInternalServerError, "Job updated but scheduling failed: "+err.Error())
|
||||||
|
|||||||
@@ -110,6 +110,9 @@ func (h *Handlers) RegisterRoutes(router *gin.Engine) {
|
|||||||
admin.GET("/logs/refresh", h.HandleRefreshLogs)
|
admin.GET("/logs/refresh", h.HandleRefreshLogs)
|
||||||
admin.GET("/logs/view/:fileName", h.HandleViewLog)
|
admin.GET("/logs/view/:fileName", h.HandleViewLog)
|
||||||
admin.GET("/logs/download/:fileName", h.HandleDownloadLog)
|
admin.GET("/logs/download/:fileName", h.HandleDownloadLog)
|
||||||
|
|
||||||
|
// Email test route
|
||||||
|
admin.POST("/test-email", h.HandleTestEmail)
|
||||||
}
|
}
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
|
|||||||
Reference in New Issue
Block a user