diff --git a/.gitignore b/.gitignore
index 7d8fffa..b8726fd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -81,4 +81,4 @@ backups/
# Ignore binaries
-gomft
+gomft
\ No newline at end of file
diff --git a/README.md b/README.md
index 421ec81..3cd619d 100644
--- a/README.md
+++ b/README.md
@@ -52,6 +52,15 @@ GoMFT is a web-based managed file transfer application built with Go, leveraging
- Custom HTTP headers
- Selectable events (job success, job failure)
- Detailed JSON payload with job information
+- **Multiple Notification Services**: Get job status updates through various notification channels:
+ - Email notifications with configurable SMTP settings
+ - Webhooks with authentication for custom integrations
+ - Pushbullet notifications with optional device targeting
+ - Ntfy.sh (both public and self-hosted) for simple push notifications
+ - Gotify server integration for self-hosted notifications
+ - Pushover notifications with customizable sounds and priorities
+ - Configurable message templates for all notification types
+ - Event-based triggers (job start, completion, errors)
- **Scheduled Transfers**: Configure transfers using cron expressions with flexible scheduling options
- **Transfer Monitoring**: Real-time status updates and detailed transfer logs with bytes and files transferred statistics
- **File Metadata Tracking**: Complete history and status of all transferred files with detailed information:
@@ -77,6 +86,14 @@ GoMFT is a web-based managed file transfer application built with Go, leveraging
- **Transfer Configurations**: Full control over source and destination connection parameters
- **Job Management**: Create, edit, and monitor transfer jobs with scheduling
- **Security**: Role-based access control with admin-managed user accounts and secure password management
+- **Authentication Providers**: Flexible authentication options:
+ - Built-in email/password authentication
+ - Authentik integration for enterprise SSO
+ - OpenID Connect (OIDC) support for standard identity providers
+ - OAuth2 integration for popular providers (Google, GitHub, etc.)
+ - Multiple provider support with fallback options
+ - Automatic user provisioning from external providers
+ - Role mapping from external identity providers
- **Password Recovery**: Self-service password reset via email with secure token-based authentication
- **User Profile Management**: Personal settings including theme preferences
- **Modern UI**: Built with Templ, HTMX and Tailwind CSS for a responsive experience
@@ -355,7 +372,41 @@ Log files contain detailed information about file transfers, job execution, and
- Choose notification triggers (job success, job failure, or both)
- Test your webhook integration with manual job runs
-8. Manage file metadata:
+8. **Webhook Notifications**:
+ - **Webhook Integration**: Send notifications to external systems when jobs complete
+ - **Secure Authentication**: HMAC-SHA256 signature for webhook verification
+ - **Custom Headers**: Add custom HTTP headers to webhook requests
+ - **Flexible Configuration**: Configure different webhooks for different jobs
+ - **Event Selection**: Choose to send notifications on success, failure, or both
+ - **Detailed Payload**: Rich JSON payload with complete job execution details
+
+9. **Multiple Notification Services**:
+ - **Pushbullet Integration**: Send notifications to your devices through Pushbullet
+ - Device targeting support for specific device delivery
+ - Customizable title and message templates
+ - API key-based authentication
+ - **Ntfy Integration**: Use public ntfy.sh or self-hosted ntfy server
+ - Topic-based routing of notifications
+ - Priority levels for different job events
+ - Optional username/password authentication for private servers
+ - Customizable title and message templates
+ - **Gotify Integration**: Send notifications to self-hosted Gotify servers
+ - Application token-based authentication
+ - Priority levels (1-10) for different notification importance
+ - Customizable title and message templates
+ - **Pushover Integration**: Professional notification delivery service
+ - Application and user key authentication
+ - Device targeting for selective delivery
+ - Sound selection for different notification types
+ - Priority levels from lowest to emergency
+ - Customizable title and message templates
+ - **Common Features**:
+ - Variable substitution in notification templates
+ - Job data access in templates (status, files, bytes, times)
+ - Event-based filtering (job start, completion, errors)
+ - Success/failure tracking for diagnostic purposes
+
+10. Manage file metadata:
- Navigate to the "Files" section to view all processed files
- Use filters to quickly find files by status, job ID, or filename
- Click on any file to view detailed metadata including timestamps, size, and hash
@@ -363,7 +414,7 @@ Log files contain detailed information about file transfers, job execution, and
- Delete file metadata records when no longer needed
- View files associated with specific jobs by navigating from the job details
-9. Utilize admin tools (administrators only):
+11. Utilize admin tools (administrators only):
- Access the "Admin Tools" section from the navigation menu
- View system statistics and server information
- Create and manage database backups
@@ -373,19 +424,57 @@ Log files contain detailed information about file transfers, job execution, and
### User Management
-GoMFT uses a role-based access control system:
+GoMFT uses a role-based access control system with flexible authentication options:
- **Administrators**: Can create and manage users, access all features
- **Regular Users**: Can manage transfers and view history
-User management features:
-- Only administrators can create new user accounts
-- User passwords are securely hashed with bcrypt
-- Password history tracking prevents reuse of recent passwords
-- Account lockout after multiple failed login attempts
-- Self-service password reset via secure email links
-- JWT-based authentication with tokens
-- User theme preference settings (light/dark)
+#### Authentication Options
+
+1. **Built-in Authentication**:
+ - Email/password login with secure password hashing
+ - JWT-based session management
+ - Password history tracking
+ - Account lockout protection
+ - Self-service password reset
+
+2. **External Authentication Providers**:
+ - **Authentik Integration**:
+ - Enterprise-grade SSO capabilities
+ - Automatic user provisioning
+ - Role synchronization
+ - Group mapping support
+ - Secure token exchange
+
+ - **OpenID Connect (OIDC)**:
+ - Standard-compliant identity provider support
+ - Automatic user creation and updates
+ - Role mapping from OIDC claims
+ - Multiple provider support
+ - Secure token validation
+
+ - **OAuth2 Providers**:
+ - Google authentication
+ - GitHub integration
+ - Other OAuth2-compliant providers
+ - Custom provider configuration
+ - Automatic profile synchronization
+
+3. **Security Features**:
+ - Secure password hashing with bcrypt
+ - JWT-based authentication with tokens
+ - Password history tracking prevents reuse
+ - Account lockout after failed attempts
+ - Two-factor authentication support
+ - Session management and timeout
+ - Secure token storage and handling
+
+4. **User Profile Management**:
+ - Theme preferences (light/dark mode)
+ - Profile information updates
+ - Password change functionality
+ - Two-factor authentication setup
+ - External account linking
### Two-Factor Authentication (2FA) Implementation
@@ -479,13 +568,16 @@ The following fields have been added to the `users` table:
- Manual execution
- Enable/disable schedules
-8. **Webhook Notifications**:
- - **Webhook Integration**: Send notifications to external systems when jobs complete
- - **Secure Authentication**: HMAC-SHA256 signature for webhook verification
- - **Custom Headers**: Add custom HTTP headers to webhook requests
- - **Flexible Configuration**: Configure different webhooks for different jobs
- - **Event Selection**: Choose to send notifications on success, failure, or both
- - **Detailed Payload**: Rich JSON payload with complete job execution details
+8. **Notification Options**:
+ - **Email Notifications**: Receive job status updates via email
+ - **Webhook Notifications**: Integration with external systems
+ - **Pushbullet**: Push notifications to your devices
+ - **Ntfy**: Simple push notifications via ntfy.sh
+ - **Gotify**: Self-hosted notification server integration
+ - **Pushover**: Professional notification service
+ - Configure event triggers (start, complete, error)
+ - Customize notification message templates
+ - Selective notification based on job status
### Email Notifications
diff --git a/components/admin.templ b/components/admin.templ
new file mode 100644
index 0000000..a48177c
--- /dev/null
+++ b/components/admin.templ
@@ -0,0 +1,125 @@
+package components
+
+import (
+ "context"
+)
+
+// AdminLayout renders the main admin dashboard
+templ AdminLayout(ctx context.Context) {
+ @LayoutWithContext("Administration", ctx) {
+
+
+
+
+
+
+
+
+
+
+
+
Total Users
+
1,234
+
+
+
+
+
+
+
+
+
+
+
Active Sessions
+
42
+
+
+
+
+
+
+
+
+
+
+
System Health
+
98%
+
+
+
+
+
+
+
+
+
+
+
+
Recent Activity
+
+
+
+
+
+
User
+
Action
+
Timestamp
+
+
+
+
+
+
admin@example.com
+
Updated user permissions
+
2024-02-20 14:32:10
+
+
+
+
+
+
+
+ }
+}
\ No newline at end of file
diff --git a/components/admin_audit.templ b/components/admin_audit.templ
new file mode 100644
index 0000000..007f84b
--- /dev/null
+++ b/components/admin_audit.templ
@@ -0,0 +1,384 @@
+package components
+
+import (
+ "context"
+ "fmt"
+ "time"
+)
+
+// AuditLogEntry represents an audit log entry for display
+type AuditLogEntry struct {
+ ID uint
+ Action string
+ EntityType string
+ EntityID uint
+ Username string
+ UserID uint
+ Timestamp time.Time
+ Details map[string]interface{}
+ DetailsSummary string
+}
+
+type AuditLogsData struct {
+ Logs []AuditLogEntry
+ TotalPages int
+ CurrentPage int
+ TotalRecords int
+ FilterAction string
+ FilterEntity string
+ FilterUser string
+ FilterDateFrom string
+ FilterDateTo string
+}
+
+// AdminAuditLogs renders the audit logs page
+templ AdminAuditLogs(ctx context.Context, data AuditLogsData) {
+ @LayoutWithContext("Audit Logs", ctx) {
+
- if data.LastBackupTime != nil {
- Last backup: { data.LastBackupTime.Format("Jan 02, 2006 15:04:05") }
- } else {
- Last backup: Never
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Restore Database
-
-
-
-
-
-
-
-
-
-
- Maintenance Tools
-
-
-
-
- @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", "")
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Email Testing
-
-
-
-
- Test your email configuration by sending a test email to verify the server can send emails properly.
-
- 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:
-
- 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 X-Hub-Signature-256 header.
-
-
-
HTTP Request Details
-
-
-
-
-
Property
-
Value
-
-
-
-
-
Method
-
POST
-
-
-
Content-Type
-
application/json
-
-
-
User-Agent
-
GoMFT-Webhook/1.0
-
-
-
X-Hub-Signature-256
-
HMAC SHA256 signature (if secret configured)
-
-
-
Custom Headers
-
Any additional headers specified in the job configuration
-
-
-
-
-
-
-
-
-
- }
-}
-
-// BackupsList is a separate component for the backups list that can be refreshed via HTMX
-templ BackupsList(data AdminToolsData) {
- if len(data.BackupFiles) > 0 {
-
-}
-
-// 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) {
-
-
-
- if success {
-
- } else {
-
- }
-
-
-
- if success {
- Email Sent Successfully
- } else {
- Email Sending Failed
- }
-
-
- { message }
-
-
-
-
-
-
-
-}
-
-// 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) {
-
-
-
- if success {
-
- } else {
-
- }
-
-
-
- if success {
- Webhook Sent Successfully
- } else {
- Webhook Sending Failed
- }
-
+ }
+}
\ No newline at end of file
diff --git a/components/config_form.templ b/components/config_form.templ
index 710bc92..a3dad3d 100644
--- a/components/config_form.templ
+++ b/components/config_form.templ
@@ -7,7 +7,6 @@ import (
"github.com/starfleetcptn/gomft/components/providers/source"
"github.com/starfleetcptn/gomft/components/providers/destination"
"github.com/starfleetcptn/gomft/components/providers/common"
- "strconv"
)
type ConfigFormData struct {
@@ -47,7 +46,7 @@ func getInitialData(config *db.TransferConfig) string {
sourceTeamDrive := ""
// Google Photos source fields
sourceReadOnly := false
- sourceStartYear, _ := strconv.Atoi(getCurrentYear())
+ sourceStartYear := 2025
sourceIncludeArchived := false
filePattern := ""
@@ -75,7 +74,7 @@ func getInitialData(config *db.TransferConfig) string {
destTeamDrive := ""
// Google Photos destination fields
destReadOnly := false
- destStartYear, _ := strconv.Atoi(getCurrentYear()) // Default to current year int
+ destStartYear := 2025
destIncludeArchived := false
archivePath := ""
@@ -84,6 +83,8 @@ func getInitialData(config *db.TransferConfig) string {
skipProcessedFiles := true
maxConcurrentTransfers := 4
rcloneFlags := ""
+ commandId := uint(1) // Default to 'copy' command
+ commandFlags := ""
useBuiltinAuthSource := true
useBuiltinAuthDest := true
@@ -117,7 +118,7 @@ func getInitialData(config *db.TransferConfig) string {
if config.SourceReadOnly != nil {
sourceReadOnly = *config.SourceReadOnly
}
- sourceStartYear = config.SourceStartYear
+ // sourceStartYear = config.SourceStartYear
if config.SourceIncludeArchived != nil {
sourceIncludeArchived = *config.SourceIncludeArchived
}
@@ -152,7 +153,7 @@ func getInitialData(config *db.TransferConfig) string {
if config.DestReadOnly != nil {
destReadOnly = *config.DestReadOnly
}
- destStartYear = config.DestStartYear
+ // destStartYear = config.DestStartYear
if config.DestIncludeArchived != nil {
destIncludeArchived = *config.DestIncludeArchived
}
@@ -163,6 +164,8 @@ func getInitialData(config *db.TransferConfig) string {
skipProcessedFiles = config.GetSkipProcessedFiles()
maxConcurrentTransfers = config.MaxConcurrentTransfers
rcloneFlags = config.RcloneFlags
+ commandId = config.CommandID
+ commandFlags = config.CommandFlags
if config.UseBuiltinAuthSource != nil {
useBuiltinAuthSource = *config.UseBuiltinAuthSource
} else if sourceClientId != "" || sourceClientSecret != "" {
@@ -238,12 +241,17 @@ func getInitialData(config *db.TransferConfig) string {
skipProcessedFiles: %v,
maxConcurrentTransfers: %d,
rcloneFlags: '%s',
+ commandId: %d,
+ commandFlags: '%s',
// Path validation states
sourcePathValid: null,
sourcePathError: '',
destPathValid: null,
destPathError: '',
+
+ // Command configuration
+ requiresDestination: true,
// Methods for path validation
checkPath(path, type) {
@@ -263,6 +271,68 @@ func getInitialData(config *db.TransferConfig) string {
this[type + 'PathValid'] = false;
this[type + 'PathError'] = 'Error checking path: ' + error.message;
});
+ },
+
+ // Method to check if destination is required based on command type
+ updateCommandRequirements() {
+ // List of commands that don't require destination
+ const listingCommands = ['ls', 'lsd', 'lsl', 'lsf', 'lsjson', 'listremotes'];
+ const infoCommands = ['md5sum', 'sha1sum', 'size', 'version'];
+ const dirCommands = ['mkdir', 'rmdir', 'rmdirs'];
+ const destructiveCommands = ['delete', 'purge'];
+ const specialSinglePathCommands = ['obscure'];
+
+ // Get the command name from the command ID
+ // This will need coordination with your backend to ensure the IDs match the commands
+ let commandName = '';
+ switch(parseInt(this.commandId)) {
+ // Map command IDs to command names - adjust these based on your actual command IDs
+ case 1: commandName = 'copy'; break;
+ case 2: commandName = 'move'; break;
+ case 3: commandName = 'sync'; break;
+ case 4: commandName = 'ls'; break;
+ case 5: commandName = 'lsd'; break;
+ case 6: commandName = 'lsl'; break;
+ case 7: commandName = 'lsf'; break;
+ case 8: commandName = 'lsjson'; break;
+ case 9: commandName = 'md5sum'; break;
+ case 10: commandName = 'sha1sum'; break;
+ case 11: commandName = 'size'; break;
+ case 12: commandName = 'delete'; break;
+ case 13: commandName = 'purge'; break;
+ case 14: commandName = 'mkdir'; break;
+ case 15: commandName = 'rmdir'; break;
+ case 16: commandName = 'rmdirs'; break;
+ case 17: commandName = 'check'; break;
+ case 18: commandName = 'cleanup'; break;
+ case 19: commandName = 'dedupe'; break;
+ case 20: commandName = 'version'; break;
+ case 21: commandName = 'listremotes'; break;
+ case 22: commandName = 'cryptcheck'; break;
+ case 23: commandName = 'bisync'; break;
+ case 24: commandName = 'copyto'; break;
+ case 25: commandName = 'moveto'; break;
+ default: commandName = 'copy'; // Default to copy
+ }
+
+ console.log('Command ID:', this.commandId, 'Command Name:', commandName);
+
+ // Check if the command requires a destination
+ if (
+ listingCommands.includes(commandName) ||
+ infoCommands.includes(commandName) ||
+ (dirCommands.includes(commandName) && !this.rcloneFlags.includes('--dst')) ||
+ destructiveCommands.includes(commandName) ||
+ specialSinglePathCommands.includes(commandName) ||
+ commandName === 'version' ||
+ commandName === 'listremotes'
+ ) {
+ this.requiresDestination = false;
+ console.log('Destination not required for command:', commandName);
+ } else {
+ this.requiresDestination = true;
+ console.log('Destination required for command:', commandName);
+ }
}
}`,
name, sourceType, sourcePath, sourceHost, sourcePort, sourceUser, sourcePassword, sourceKeyFile, sourceAuthType,
@@ -275,171 +345,254 @@ func getInitialData(config *db.TransferConfig) string {
destClientId, destClientSecret, destDriveId, destTeamDrive,
destReadOnly, destStartYear, destIncludeArchived,
useBuiltinAuthSource, useBuiltinAuthDest,
- archivePath, archiveEnabled, deleteAfterTransfer, skipProcessedFiles, maxConcurrentTransfers, rcloneFlags)
+ archivePath, archiveEnabled, deleteAfterTransfer, skipProcessedFiles, maxConcurrentTransfers, rcloneFlags,
+ commandId, commandFlags)
}
templ ConfigForm(ctx context.Context, data ConfigFormData) {
@LayoutWithContext(getConfigFormTitle(data.IsNew), ctx) {
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ { getConfigFormTitle(data.IsNew) }
+
+
+ Configure your file transfer settings using the form below
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Configuration Tips
+
+ Configure your file transfer settings carefully for optimal performance. Use file patterns to filter which files are transferred, and consider using archive options to keep track of processed files.
+
+
+
-
-
- { getConfigFormTitle(data.IsNew) }
-
-
- Set up your file transfer configuration
-
-
-
-
-
-
-
Configure your file transfer settings carefully for optimal performance
-
-
+
}
}
\ No newline at end of file
diff --git a/components/configs.templ b/components/configs.templ
index 2a4a3b3..5703089 100644
--- a/components/configs.templ
+++ b/components/configs.templ
@@ -6,39 +6,34 @@ import (
"github.com/starfleetcptn/gomft/internal/db"
)
-// Dialog component for confirmation dialogs
+// Dialog component for confirmation dialogs using Flowbite modal
templ ConfigDialog(id string, title string, message string, confirmClass string, confirmText string, action string, configID uint, configName string) {
-
-
-
-
-
+
+
+
+
+ if action == "delete" {
+
+ } else {
+
+ }
+
{ message }
+
+
-
- { title }
-
-
-
-
- { message }
-
-
-
-
-
@@ -46,15 +41,18 @@ templ ConfigDialog(id string, title string, message string, confirmClass string,
script hideConfigDialog(id string) {
document.getElementById(id).classList.add("hidden");
+ document.getElementById(id).classList.remove("flex");
}
script showConfigDialog(id string) {
document.getElementById(id).classList.remove("hidden");
+ document.getElementById(id).classList.add("flex");
}
script triggerConfigDelete(dialogId string, configID uint, configName string) {
// Hide the dialog
document.getElementById(dialogId).classList.add("hidden");
+ document.getElementById(dialogId).classList.remove("flex");
// Store data in a way that's accessible to event handlers
window.lastDeletedConfig = {
@@ -75,38 +73,82 @@ type ConfigsData struct {
templ Configs(ctx context.Context, data ConfigsData) {
@LayoutWithContext("Transfer Configurations", ctx) {
+
+
+
-
-
- Configurations define how files are transferred between systems
-
-
-
-
-
-
-
- Google Drive and Google Photos configurations require authentication. Click the "Authenticate" button to complete setup.
-
+
+
+
+
+
+
+
+
Configurations define how files are transferred between systems
+
+
+
+
+
+
+
+
+
Google Drive and Google Photos configurations require authentication. Click the "Authenticate" button to complete setup.
+
+
+
+
+
+
+
+
+
When duplicating configurations, you may need to re-enter sensitive credentials for security reasons. Make sure to edit duplicated configurations to add any required credentials.
+
+
+
+
}
}
\ No newline at end of file
diff --git a/components/dashboard.templ b/components/dashboard.templ
index 16a5a22..db5ca3c 100644
--- a/components/dashboard.templ
+++ b/components/dashboard.templ
@@ -4,7 +4,9 @@ import (
"context"
"fmt"
"github.com/starfleetcptn/gomft/internal/db"
+ "os/exec"
"strconv"
+ "strings"
)
type DashboardData struct {
@@ -13,19 +15,44 @@ type DashboardData struct {
CompletedToday int
FailedTransfers int
Configs map[uint]db.TransferConfig
+ RcloneVersion string
+}
+
+// GetRcloneVersion executes the rclone --version command and returns the version string
+func GetRcloneVersion() string {
+ cmd := exec.Command("rclone", "--version")
+ output, err := cmd.Output()
+ if err != nil {
+ return "Unknown"
+ }
+
+ // Parse the version from output (typically first line contains "rclone v1.XX.X")
+ outputStr := string(output)
+ lines := strings.Split(outputStr, "\n")
+ if len(lines) > 0 {
+ parts := strings.Split(lines[0], " ")
+ for _, part := range parts {
+ if strings.HasPrefix(part, "v") {
+ return part
+ }
+ }
+ }
+
+ return "Unknown"
}
templ Dashboard(ctx context.Context, data DashboardData) {
@LayoutWithContext("Dashboard", ctx) {
-
+ }
+}
\ No newline at end of file
diff --git a/components/dashboard_notifications_page.templ b/components/dashboard_notifications_page.templ
new file mode 100644
index 0000000..b3ad8b1
--- /dev/null
+++ b/components/dashboard_notifications_page.templ
@@ -0,0 +1,315 @@
+package components
+
+import (
+ "context"
+ "fmt"
+ "strconv"
+)
+
+// Note: To implement pagination, update the NotificationsData struct in notifications.templ
+// to include these fields:
+// CurrentPage int
+// TotalPages int
+// TotalCount int
+// PerPage int
+
+templ NotificationsPage(ctx context.Context, data NotificationsData) {
+ @LayoutWithContext("Notifications", ctx) {
+
Notifications keep you updated on important system events and alerts.
+
+
+
+
+
+
+
+
+
Unread notifications are highlighted in blue. Mark them as read to clear the highlight.
+
+
+
+
+
+
+
+
+
Adjust how many notifications to display per page using the dropdown menu.
+
+
+
+
+ }
+}
+
+// Helper function for pagination math
+func minInt(a, b int) int {
+ if a < b {
+ return a
+ }
+ return b
+}
+
+// Helper function for pagination math
+func maxInt(a, b int) int {
+ if a > b {
+ return a
+ }
+ return b
+}
+
+// Add the pageNumbers template for consistent pagination layout
+templ notificationPageNumbers(currentPage int, totalPages int, perPage int) {
+ // Show at most 5 page numbers with the current page in the middle when possible
+ for i := 1; i <= totalPages; i++ {
+ if i == currentPage {
+
+ { fmt.Sprint(i) }
+
+ } else if i == 1 || i == totalPages || (i >= currentPage-2 && i <= currentPage+2) {
+
+ { fmt.Sprint(i) }
+
+
+
+
+ } else if i == currentPage-3 || i == currentPage+3 {
+
+ ...
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/error.templ b/components/error.templ
new file mode 100644
index 0000000..6026410
--- /dev/null
+++ b/components/error.templ
@@ -0,0 +1,91 @@
+package components
+
+import (
+ "context"
+ "fmt"
+)
+
+// ErrorPage renders a generic error page with customizable title, message, and details
+templ ErrorPage(ctx context.Context, code int, title, message, details string) {
+ @LayoutWithContext(fmt.Sprintf("Error %d - %s", code, title), ctx) {
+
- if data.Page > 1 {
-
-
-
- } else {
-
-
-
- }
-
- for i := 1; i <= data.TotalPages; i++ {
- if i == data.Page {
-
- { strconv.Itoa(i) }
-
- } else if i == 1 || i == data.TotalPages || (i >= data.Page-2 && i <= data.Page+2) {
-
- { strconv.Itoa(i) }
-
- } else if i == data.Page-3 || i == data.Page+3 {
- ...
- }
- }
-
- if data.Page < data.TotalPages {
-
-
-
- } else {
-
-
-
- }
-
-
- }
+
+
+ if data.Page == data.TotalPages {
+
+ Next
+
+ } else {
+
+ Next
+
+ }
+
+
+
+ }
+
}
// Update the main FileMetadataList template to use the partial template
templ FileMetadataList(ctx context.Context, data FileMetadataListData) {
@LayoutWithContext("File Metadata", ctx) {
+
+
+
-
diff --git a/components/history.templ b/components/history.templ
index 60cc885..ba0ba9a 100644
--- a/components/history.templ
+++ b/components/history.templ
@@ -49,66 +49,88 @@ func getConfigNameForHistory(history db.JobHistory, configs map[uint]db.Transfer
// HistoryContent renders only the content part of the history page for HTMX requests
templ HistoryContent(ctx context.Context, data HistoryData) {
if len(data.History) == 0 {
-
-
-
+
+
+
+
+
-
No transfer history
+
No transfer history
if data.SearchTerm != "" {
-
No results found for "{ data.SearchTerm }". Try a different search term or .
+
+ No results found for "{ data.SearchTerm }". Try a different search term or
+ .
+
} else if data.CurrentPage > 1 {
-
No more results on this page. .
+
+ No more results on this page.
+ .
+
} else {
-
Transfer history will appear here once jobs have run.
+
Transfer history will appear here once jobs have run.