Compare commits

..
9 Commits
Author SHA1 Message Date
StarFleetCPTN 11fe75dd57 Merge pull request #65 from StarFleetCPTN/development
Bug Fixes
2025-03-27 16:51:55 -07:00
StarFleetCPTN ec107ca0ce Merge branch 'main' of https://github.com/StarFleetCPTN/GoMFT into development
Bug Fixes
2025-03-27 16:48:35 -07:00
StarFleetCPTN 7172d4da90 Merge pull request #64 from StarFleetCPTN/development
Merge pull request #63 from StarFleetCPTN/main
2025-03-27 16:41:52 -07:00
StarFleetCPTN badcbfdb17 Merge pull request #63 from StarFleetCPTN/main
Bug fixes
2025-03-27 16:30:45 -07:00
StarFleetCPTN 748d5c0939 feat: Update port initialization and handling for FTP/SFTP configurations
- Changed default port initialization for source and destination configurations to 0, triggering default settings based on connection type.
- Implemented logic to set default ports for FTP (21) and SFTP (22) in the frontend templates.
- Enhanced input fields for FTP and SFTP forms to initialize and update port values dynamically based on user selections.
- Updated backend configuration generation to include the correct port values for source and destination settings.
2025-03-27 16:29:50 -07:00
StarFleetCPTN dd7426a01f Merge pull request #62 from StarFleetCPTN/development
feat: Implement frontend asset build process and update Docker config…
2025-03-27 15:30:08 -07:00
StarFleetCPTN fde60b7d68 feat: Implement frontend asset build process and update Docker configuration
- Added a new build script using esbuild to bundle JavaScript and CSS assets, including Tailwind CSS and Font Awesome.
- Updated the Dockerfile to include a multi-stage build process for frontend assets, ensuring efficient image creation.
- Enhanced GitHub workflows to automate the installation of Node.js dependencies and build frontend assets during CI/CD.
- Introduced a .gitignore entry for the dist directory to prevent unnecessary files from being tracked.
- Improved caching and content type handling for static files served by the application.
2025-03-27 15:28:40 -07:00
StarFleetCPTN 297e5ca92b Merge pull request #59 from StarFleetCPTN/development
feat: Add notification service structure to new notification form
2025-03-26 19:07:08 -07:00
StarFleetCPTN 405b2e605e feat: Add notification service structure to new notification form
- Introduced a new struct for NotificationService within the notification form data, encompassing various configuration fields for services like Pushbullet, Ntfy, Gotify, and Pushover.
- This addition enhances the form's capability to manage detailed service configurations, improving user experience and flexibility in notification management.
2025-03-26 19:06:18 -07:00
55 changed files with 2635 additions and 249 deletions
@@ -29,6 +29,23 @@ jobs:
with:
fetch-depth: 0 # Needed to get all tags for versioning
# Set up Node.js for frontend build
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
# Install dependencies
- name: Install dependencies
run: npm ci
# Build frontend assets
- name: Build frontend assets
run: |
node build.js
ls -la static/dist/
# Set version information
- name: Set Version
id: version
+17
View File
@@ -30,6 +30,23 @@ jobs:
with:
fetch-depth: 0 # Needed to get all tags for versioning
# Set up Node.js for frontend build
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
# Install dependencies
- name: Install dependencies
run: npm ci
# Build frontend assets
- name: Build frontend assets
run: |
node build.js
ls -la static/dist/
# Set version information
- name: Set Version
id: version
+20
View File
@@ -27,6 +27,26 @@ jobs:
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install Node.js dependencies
run: npm ci
- name: Build frontend assets
run: |
# Build JavaScript and CSS assets
node build.js
# Ensure the dist directory exists
mkdir -p static/dist
# Verify the build output
ls -la static/dist
- name: Set up Go
uses: actions/setup-go@v5
with:
+2
View File
@@ -79,6 +79,8 @@ backups/
/destination/
/archive/
# Ignore the dist directory
static/dist/
# Ignore binaries
gomft
+44 -3
View File
@@ -1,3 +1,32 @@
# Build frontend assets
FROM node:20-alpine AS frontend-builder
WORKDIR /app
# Copy all files needed for the build first
COPY package.json package-lock.json ./
COPY build.js ./
COPY static/ ./static/
COPY tailwind.config.js ./
# Debug: Show the contents of build.js
RUN echo "Contents of build.js:" && cat build.js
# Remove the build script from postinstall
RUN sed -i 's/"postinstall": "npm run build",//' package.json
# Install dependencies and build with verbose output
RUN npm ci && \
echo "Building frontend assets..." && \
node build.js && \
echo "Build complete. Contents of dist:" && \
ls -la static/dist/ && \
echo "Sample of app.js:" && \
head -n 10 static/dist/app.js && \
echo "Sample of app.css:" && \
head -n 10 static/dist/app.css
# Go build stage
FROM golang:1.24-alpine AS builder
WORKDIR /app
@@ -17,7 +46,20 @@ RUN go install github.com/a-h/templ/cmd/templ@latest
COPY go.mod go.sum ./
RUN go mod download
# Copy the rest of the source code
# Create static directory structure
RUN mkdir -p /app/static/dist
# Copy built frontend assets from frontend-builder BEFORE copying Go source
COPY --from=frontend-builder /app/static/dist/ /app/static/dist/
# Copy the rest of the static files
COPY static/ /app/static/
# Verify static files are in place before Go build
RUN echo "Verifying static files before Go build:" && \
ls -la /app/static/dist/
# Now copy the rest of the source code
COPY . .
# Generate template files from .templ files
@@ -61,8 +103,7 @@ RUN addgroup -g ${GID} ${USERNAME} && \
COPY --from=builder /app/gomft /app/
COPY --from=builder /usr/local/bin/rclone /usr/local/bin/rclone
# Copy static files and configurations
COPY static/ /app/static/
# Copy components
COPY components/ /app/components/
# Copy entrypoint script
+122
View File
@@ -0,0 +1,122 @@
import * as esbuild from 'esbuild';
import path from 'path';
import { fileURLToPath } from 'url';
import { execSync } from 'child_process';
import fs from 'fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const isWatch = process.argv.includes('--watch');
// Ensure the dist directory exists
const distDir = path.join(__dirname, 'static', 'dist');
if (!fs.existsSync(distDir)) {
fs.mkdirSync(distDir, { recursive: true });
}
// Copy Font Awesome files
const fontAwesomeSrcDir = path.join(__dirname, 'node_modules', '@fortawesome', 'fontawesome-free');
const fontAwesomeDestDir = path.join(distDir, 'fontawesome');
// Copy CSS files
const cssFiles = [
'css/all.min.css',
'css/fontawesome.min.css',
'css/solid.min.css',
'css/regular.min.css',
'css/brands.min.css'
];
cssFiles.forEach(file => {
const srcFile = path.join(fontAwesomeSrcDir, file);
const destFile = path.join(fontAwesomeDestDir, file);
const destDir = path.dirname(destFile);
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
}
if (fs.existsSync(srcFile)) {
fs.copyFileSync(srcFile, destFile);
}
});
// Copy webfonts
const webfontsSrcDir = path.join(fontAwesomeSrcDir, 'webfonts');
const webfontsDestDir = path.join(fontAwesomeDestDir, 'webfonts');
if (!fs.existsSync(webfontsDestDir)) {
fs.mkdirSync(webfontsDestDir, { recursive: true });
}
fs.readdirSync(webfontsSrcDir).forEach(file => {
fs.copyFileSync(
path.join(webfontsSrcDir, file),
path.join(webfontsDestDir, file)
);
});
const commonConfig = {
sourcemap: true,
minify: true,
bundle: true,
platform: 'browser',
target: ['es2020'],
};
async function buildTailwind() {
console.log('Building Tailwind CSS...');
execSync('npx tailwindcss -i ./static/css/app.css -o ./static/dist/app.css --minify');
}
async function build() {
try {
// Build vendor JavaScript bundle (CDN dependencies)
await esbuild.build({
...commonConfig,
entryPoints: ['static/js/vendor.js'],
outfile: 'static/dist/vendor.js',
format: 'iife',
});
// Build application JavaScript
await esbuild.build({
...commonConfig,
entryPoints: ['static/js/app.js'],
outfile: 'static/dist/app.js',
format: 'iife',
});
// Build initialization JavaScript
await esbuild.build({
...commonConfig,
entryPoints: ['static/js/init.js'],
outfile: 'static/dist/init.js',
format: 'iife',
});
// Build CSS with Tailwind
await buildTailwind();
console.log('Build completed successfully!');
} catch (error) {
console.error('Build failed:', error);
process.exit(1);
}
}
if (isWatch) {
// Watch mode
console.log('Starting watch mode...');
const ctx = await esbuild.context(commonConfig);
await ctx.watch();
// Watch Tailwind CSS
execSync('npx tailwindcss -i ./static/css/app.css -o ./static/dist/app.css --watch');
console.log('Watching for changes...');
} else {
// Single build
build();
}
+6 -13
View File
@@ -87,7 +87,7 @@ templ AdminRoles(ctx context.Context, data RolesData) {
<button
data-role-id={ fmt.Sprint(role.ID) }
data-role-name={ role.Name }
onclick={ showRoleDialog(fmt.Sprintf("delete-role-dialog-%d", role.ID)) }
onclick={ showModal(fmt.Sprintf("delete-role-dialog-%d", role.ID)) }
class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300">
<i class="fas fa-trash"></i>
</button>
@@ -269,7 +269,10 @@ templ AdminRoles(ctx context.Context, data RolesData) {
// RoleDialog for confirmation actions
templ RoleDialog(id string, title string, message string, confirmClass string, confirmText string, action string, roleID uint, roleName string) {
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm">
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<!-- Backdrop -->
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
<!-- Modal content -->
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
<div class="p-6 text-center">
@@ -288,7 +291,7 @@ templ RoleDialog(id string, title string, message string, confirmClass string, c
onclick={ triggerRoleDelete(id, roleID, roleName) }>
{ confirmText }
</button>
<button type="button" data-modal-hide={ id } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
<button type="button" onclick={ closeModal(id) } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
Cancel
</button>
</div>
@@ -297,16 +300,6 @@ templ RoleDialog(id string, title string, message string, confirmClass string, c
</div>
}
script hideRoleDialog(id string) {
document.getElementById(id).classList.add("hidden");
document.getElementById(id).classList.remove("flex");
}
script showRoleDialog(id string) {
document.getElementById(id).classList.remove("hidden");
document.getElementById(id).classList.add("flex");
}
script triggerRoleDelete(dialogId string, roleID uint, roleName string) {
// Hide the dialog
document.getElementById(dialogId).classList.add("hidden");
+34 -4
View File
@@ -27,7 +27,7 @@ func getInitialData(config *db.TransferConfig) string {
sourceType := "local"
sourcePath := ""
sourceHost := ""
sourcePort := 22
sourcePort := 0 // Initialize to 0 to trigger default setting
sourceUser := ""
sourcePassword := ""
sourceKeyFile := ""
@@ -55,7 +55,7 @@ func getInitialData(config *db.TransferConfig) string {
destinationType := "local"
destinationPath := ""
destHost := ""
destPort := 22
destPort := 0 // Initialize to 0 to trigger default setting
destUser := ""
destPassword := ""
destKeyFile := ""
@@ -386,11 +386,41 @@ templ ConfigForm(ctx context.Context, data ConfigFormData) {
// Ensure initial form state displays correctly on load
sourceType = sourceType || 'local';
destinationType = destinationType || 'local';
sourcePort = sourcePort || 22;
destPort = destPort || 22;
// Set default ports based on connection type
if (sourcePort === 0 || !sourcePort) {
if (sourceType === 'sftp') {
sourcePort = 22;
} else if (sourceType === 'ftp') {
sourcePort = 21;
}
}
if (destPort === 0 || !destPort) {
if (destinationType === 'sftp') {
destPort = 22;
} else if (destinationType === 'ftp') {
destPort = 21;
}
}
// Initialize command requirements
updateCommandRequirements();
})"
x-effect="if (sourceType === 'sftp' && (sourcePort === 0 || sourcePort === 21)) {
sourcePort = 22;
console.log('Updating source port to 22 for SFTP');
} else if (sourceType === 'ftp' && (sourcePort === 0 || sourcePort === 22)) {
sourcePort = 21;
console.log('Updating source port to 21 for FTP');
}"
x-effect="if (destinationType === 'sftp' && (destPort === 0 || destPort === 21)) {
destPort = 22;
console.log('Updating destination port to 22 for SFTP');
} else if (destinationType === 'ftp' && (destPort === 0 || destPort === 22)) {
destPort = 21;
console.log('Updating destination port to 21 for FTP');
}"
>
<!-- Configuration Details Section -->
+24 -9
View File
@@ -8,7 +8,10 @@ import (
// 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) {
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm">
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<!-- Backdrop -->
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
<!-- Modal content -->
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
<div class="p-6 text-center">
@@ -30,7 +33,7 @@ templ ConfigDialog(id string, title string, message string, confirmClass string,
onclick={ triggerConfigDelete(id, configID, configName) }>
{ confirmText }
</button>
<button type="button" data-modal-hide={ id } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
<button type="button" onclick={ closeModal(id) } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
Cancel
</button>
</div>
@@ -39,14 +42,26 @@ templ ConfigDialog(id string, title string, message string, confirmClass string,
</div>
}
script hideConfigDialog(id string) {
document.getElementById(id).classList.add("hidden");
document.getElementById(id).classList.remove("flex");
script closeModal(id string) {
const modal = document.getElementById(id);
const backdrop = document.getElementById(id + '-backdrop');
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
if (backdrop) {
backdrop.remove();
}
document.body.style.overflow = '';
}
script showConfigDialog(id string) {
document.getElementById(id).classList.remove("hidden");
document.getElementById(id).classList.add("flex");
script showModal(id string) {
const modal = document.getElementById(id);
if (modal) {
modal.classList.remove('hidden');
modal.classList.add('flex');
document.body.style.overflow = 'hidden';
}
}
script triggerConfigDelete(dialogId string, configID uint, configName string) {
@@ -429,7 +444,7 @@ templ Configs(ctx context.Context, data ConfigsData) {
)
<button
type="button"
onclick={ showConfigDialog(fmt.Sprintf("delete-config-dialog-%d", config.ID)) }
onclick={ showModal(fmt.Sprintf("delete-config-dialog-%d", config.ID)) }
class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800">
<i class="fas fa-trash-alt w-3.5 h-3.5 mr-1.5"></i>
Delete
+10 -21
View File
@@ -76,7 +76,10 @@ func formatFileSize(size int64) string {
// Updated Dialog component using Flowbite
templ FileMetadataDialog(id string, title string, message string, confirmClass string, confirmText string, action string, fileID uint, fileName string, section string) {
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm">
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<!-- Backdrop -->
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
<!-- Modal content -->
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
<div class="p-6 text-center">
@@ -112,7 +115,7 @@ templ FileMetadataDialog(id string, title string, message string, confirmClass s
{ confirmText }
</button>
}
<button type="button" data-modal-hide={ id } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
<button type="button" onclick={ closeModal(id) } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
Cancel
</button>
</div>
@@ -121,16 +124,6 @@ templ FileMetadataDialog(id string, title string, message string, confirmClass s
</div>
}
script hideFileDialog(id string) {
document.getElementById(id).classList.add("hidden");
document.getElementById(id).classList.remove("flex");
}
script showFileDialog(id string) {
document.getElementById(id).classList.remove("hidden");
document.getElementById(id).classList.add("flex");
}
script triggerFileDelete(dialogId string, fileID uint, fileName string) {
// Hide the dialog
document.getElementById(dialogId).classList.add("hidden");
@@ -211,7 +204,7 @@ templ FileMetadataListPartial(data FileMetadataListData) {
</a>
<button
type="button"
onclick={ showFileDialog(fmt.Sprintf("delete-file-dialog-%d", file.ID)) }
onclick={ showModal(fmt.Sprintf("delete-file-dialog-%d", file.ID)) }
class="font-medium text-red-600 dark:text-red-500 hover:underline">
<i class="fas fa-trash"></i>
</button>
@@ -392,9 +385,7 @@ templ FileMetadataList(ctx context.Context, data FileMetadataListData) {
});
// Track all HTMX events for debugging
document.addEventListener('htmx:beforeRequest', function(event) {
console.log("HTMX before request:", event.detail);
document.addEventListener('htmx:beforeRequest', function(event) {
// Check if this is a DELETE request by examining the URL and method
const path = event.detail.path;
const method = event.detail.verb;
@@ -411,9 +402,7 @@ templ FileMetadataList(ctx context.Context, data FileMetadataListData) {
});
// Track HTMX after-request events for file deletion
document.addEventListener('htmx:afterRequest', function(event) {
console.log("HTMX after request:", event.detail);
document.addEventListener('htmx:afterRequest', function(event) {
// Check for file deletion multiple ways
const isDeleteRequest =
// Check global flag from the triggerFileDelete function
@@ -970,7 +959,7 @@ templ FileMetadataDetails(ctx context.Context, data FileMetadataDetailsData) {
</a>
<button
type="button"
onclick={ showFileDialog(fmt.Sprintf("delete-file-dialog-%d", data.File.ID)) }
onclick={ showModal(fmt.Sprintf("delete-file-dialog-%d", data.File.ID)) }
class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800">
<i class="fas fa-trash mr-2"></i> Delete Record
</button>
@@ -1336,7 +1325,7 @@ templ FileMetadataSearchContent(data FileMetadataSearchData) {
</a>
<button
type="button"
onclick={ showFileDialog(fmt.Sprintf("delete-file-dialog-%d", file.ID)) }
onclick={ showModal(fmt.Sprintf("delete-file-dialog-%d", file.ID)) }
class="font-medium text-red-600 dark:text-red-500 hover:underline">
<i class="fas fa-trash"></i>
</button>
+8 -19
View File
@@ -8,7 +8,10 @@ import (
// Dialog component for confirmation dialogs using Flowbite modal
templ JobDialog(id string, title string, message string, confirmClass string, confirmText string, action string, jobID uint, jobName string) {
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm">
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<!-- Backdrop -->
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
<!-- Modal content -->
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
<div class="p-6 text-center">
@@ -30,7 +33,7 @@ templ JobDialog(id string, title string, message string, confirmClass string, co
onclick={ triggerJobDelete(id, jobID, jobName) }>
{ confirmText }
</button>
<button type="button" data-modal-hide={ id } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
<button type="button" onclick={ closeModal(id) } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
Cancel
</button>
</div>
@@ -39,16 +42,6 @@ templ JobDialog(id string, title string, message string, confirmClass string, co
</div>
}
script hideJobDialog(id string) {
document.getElementById(id).classList.add("hidden");
document.getElementById(id).classList.remove("flex");
}
script showJobDialog(id string) {
document.getElementById(id).classList.remove("hidden");
document.getElementById(id).classList.add("flex");
}
script triggerJobDelete(dialogId string, jobID uint, jobName string) {
// Hide the dialog
document.getElementById(dialogId).classList.add("hidden");
@@ -196,9 +189,7 @@ templ Jobs(ctx context.Context, data JobsData) {
});
// Track all HTMX events for debugging
document.addEventListener('htmx:beforeRequest', function(event) {
console.log("HTMX before request:", event.detail);
document.addEventListener('htmx:beforeRequest', function(event) {
// Check if this is a DELETE request by examining the URL and method
const path = event.detail.path;
const method = event.detail.verb;
@@ -215,9 +206,7 @@ templ Jobs(ctx context.Context, data JobsData) {
});
// Track HTMX after-request events for job deletion
document.addEventListener('htmx:afterRequest', function(event) {
console.log("HTMX after request:", event.detail);
document.addEventListener('htmx:afterRequest', function(event) {
// Check for job deletion multiple ways
const isDeleteRequest =
// Check global flag from the triggerJobDelete function
@@ -395,7 +384,7 @@ templ Jobs(ctx context.Context, data JobsData) {
)
<button
type="button"
onclick={ showJobDialog(fmt.Sprintf("delete-job-dialog-%d", job.ID)) }
onclick={ showModal(fmt.Sprintf("delete-job-dialog-%d", job.ID)) }
class="focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-xs px-3 py-1.5 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900">
<i class="fas fa-trash mr-1"></i>
Delete
+116 -96
View File
@@ -54,42 +54,36 @@ templ LayoutWithContext(title string, ctx context.Context) {
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png"/>
<link rel="manifest" href="/static/site.webmanifest"/>
<title>{ title } - GoMFT</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<script src="https://unpkg.com/alpinejs@3.13.5/dist/cdn.min.js" defer></script>
<!-- Tailwind CSS with Flowbite -->
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.2.1/flowbite.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.2.1/flowbite.min.js"></script>
<script src="/static/js/app.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"/>
<link rel="stylesheet" href="/static/css/app.css"/>
<!-- Font Awesome -->
<link rel="stylesheet" href="/static/dist/fontawesome/css/all.min.css"/>
<!-- Application assets -->
<link rel="stylesheet" href="/static/dist/app.css"/>
<link rel="stylesheet" href="https://rsms.me/inter/inter.css"/>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
950: '#082f49',
// Immediate theme application
// This script runs before the DOM is fully loaded
(function() {
const isDark = localStorage.theme === 'dark' ||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches);
if (isDark) {
document.documentElement.classList.add('dark');
document.addEventListener('DOMContentLoaded', function() {
document.body.classList.add('dark');
// Apply dark theme to containers
const containers = ['jobs-container', 'configs-container'];
containers.forEach(function(id) {
const container = document.getElementById(id);
if (container) {
container.classList.add('dark');
container.style.backgroundColor = '#111827';
}
},
fontFamily: {
sans: ['Inter var', 'ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'sans-serif'],
}
}
});
});
}
}
})();
</script>
<style>
html, body {
@@ -146,32 +140,6 @@ templ LayoutWithContext(title string, ctx context.Context) {
background-color: #1f2937 !important;
}
</style>
<link rel="stylesheet" href="https://rsms.me/inter/inter.css"/>
<script>
// Immediate theme application
// This script runs before the DOM is fully loaded
(function() {
const isDark = localStorage.theme === 'dark' ||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches);
if (isDark) {
document.documentElement.classList.add('dark');
document.addEventListener('DOMContentLoaded', function() {
document.body.classList.add('dark');
// Apply dark theme to containers
const containers = ['jobs-container', 'configs-container'];
containers.forEach(function(id) {
const container = document.getElementById(id);
if (container) {
container.classList.add('dark');
container.style.backgroundColor = '#111827';
}
});
});
}
})();
</script>
</head>
<body class="min-h-full bg-gray-50 dark:bg-gray-900" style="min-height: 100vh; display: flex; flex-direction: column;">
if isLoggedIn(ctx) {
@@ -269,20 +237,100 @@ templ LayoutWithContext(title string, ctx context.Context) {
<button data-drawer-target="mobile-menu" data-drawer-toggle="mobile-menu" class="p-2 text-gray-500 rounded-lg hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700">
<i class="fas fa-bars text-lg"></i>
</button>
<!-- User menu (same as desktop) -->
</div>
</div>
</div>
</div>
<!-- Add this drawer element for mobile menu -->
<div id="mobile-menu" class="fixed top-0 left-0 z-40 h-screen p-4 overflow-y-auto transition-transform -translate-x-full bg-white w-64 dark:bg-gray-800" tabindex="-1">
<div class="flex items-center justify-between">
<a href="/" class="flex items-center text-xl font-bold text-primary-600 dark:text-primary-400">
<i class="fas fa-exchange-alt mr-2"></i>
GoMFT
</a>
<button type="button" data-drawer-hide="mobile-menu" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white">
<i class="fas fa-times"></i>
</button>
</div>
<!-- Copy the same navigation items from the desktop sidebar -->
<nav class="mt-5">
<div class="space-y-1">
<!-- Copy the same links from the desktop sidebar -->
<a href="/dashboard" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
<i class="fas fa-tachometer-alt mr-3 text-gray-500 dark:text-gray-400"></i>
Dashboard
</a>
<a href="/configs" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
<i class="fas fa-cogs mr-3 text-gray-500 dark:text-gray-400"></i>
Configs
</a>
<a href="/jobs" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
<i class="fas fa-tasks mr-3 text-gray-500 dark:text-gray-400"></i>
Jobs
</a>
<a href="/history" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
<i class="fas fa-history mr-3 text-gray-500 dark:text-gray-400"></i>
History
</a>
<a href="/files" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
<i class="fas fa-file-alt mr-3 text-gray-500 dark:text-gray-400"></i>
Files
</a>
if isAdmin(ctx) {
<p class="px-3 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Administration
</p>
<a href="/admin/users" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
<i class="fas fa-users w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
Users
</a>
<a href="/admin/roles" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
<i class="fas fa-user-shield w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
Roles
</a>
<a href="/admin/audit" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
<i class="fas fa-clipboard-list w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
Audit Logs
</a>
<a href="/admin/database" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
<i class="fas fa-database w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
Database Tools
</a>
// Settings Dropdown
<button type="button" class="flex items-center w-full p-2 text-base text-gray-900 transition duration-75 rounded-lg group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700" aria-controls="dropdown-settings" data-collapse-toggle="dropdown-settings">
<i class="fas fa-cog w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
<span class="flex-1 ms-3 text-left rtl:text-right whitespace-nowrap">Settings</span>
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
</svg>
</button>
<ul id="dropdown-settings" class="hidden py-2 space-y-2">
// <li>
// <a href="#" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">General (Coming Soon)</a>
// </li>
<li>
<a href="/admin/settings/auth-providers" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
<i class="fas fa-user-lock w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
Authentication Providers
</a>
</li>
<li>
<a href="/admin/settings/notifications" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
<i class="fas fa-bell w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
Notification Services
</a>
</li>
</ul>
}
</div>
</nav>
</div>
<!-- Main Content -->
<div class="md:pl-64 flex flex-col flex-1 w-full bg-gray-50 dark:bg-gray-900">
<!-- Main header -->
<header class="bg-white dark:bg-gray-800 shadow-sm">
<div class="flex items-center justify-between px-4 py-3 sm:px-6 lg:px-8">
<div class="flex items-center space-x-3">
<button data-drawer-target="desktop-menu" data-drawer-toggle="desktop-menu" class="md:hidden p-2 text-gray-500 rounded-lg hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700">
<i class="fas fa-bars text-lg"></i>
</button>
<h1 class="text-lg font-semibold text-gray-900 dark:text-white">{ title }</h1>
</div>
<!-- Theme toggle and user menu -->
@@ -447,40 +495,12 @@ templ LayoutWithContext(title string, ctx context.Context) {
</footer>
</div>
}
<script>
// Add script to ensure dark mode is properly applied
document.addEventListener('DOMContentLoaded', function() {
// Apply body dark class when theme changes
const isDark = document.documentElement.classList.contains('dark');
if (isDark) {
document.body.classList.add('dark');
// Also apply to containers
const jobsContainer = document.getElementById('jobs-container');
const configsContainer = document.getElementById('configs-container');
if (jobsContainer) jobsContainer.classList.add('dark');
if (configsContainer) configsContainer.classList.add('dark');
}
// Initialize admin dropdown toggle if available
const adminDropdownToggle = document.querySelector('[data-collapse-toggle="dropdown-settings"]');
const adminDropdown = document.getElementById('dropdown-settings');
if (adminDropdownToggle && adminDropdown) {
// Check if we should show the dropdown (if current page is under admin section)
const currentPath = window.location.pathname;
if (currentPath.startsWith('/admin')) {
adminDropdown.classList.remove('hidden');
}
// Add click event listener
adminDropdownToggle.addEventListener('click', function() {
adminDropdown.classList.toggle('hidden');
});
}
});
</script>
<!-- Scripts -->
<!-- Alpine.js and dependencies -->
<script defer src="/static/dist/vendor.js"></script>
<!-- Application scripts -->
<script defer src="/static/dist/app.js"></script>
<script defer src="/static/dist/init.js"></script>
</body>
</html>
}
+8 -19
View File
@@ -7,7 +7,10 @@ import (
// Dialog component for confirmation dialogs using Flowbite modal
templ NotificationDialog(id string, title string, message string, confirmClass string, confirmText string, action string, serviceID uint, serviceName string) {
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm">
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<!-- Backdrop -->
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
<!-- Modal content -->
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
<div class="p-6 text-center">
@@ -28,7 +31,7 @@ templ NotificationDialog(id string, title string, message string, confirmClass s
onclick={ triggerServiceDelete(id, serviceID, serviceName) }>
{ confirmText }
</button>
<button type="button" data-modal-hide={ id } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
<button type="button" onclick={ closeModal(id) } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
Cancel
</button>
</div>
@@ -37,16 +40,6 @@ templ NotificationDialog(id string, title string, message string, confirmClass s
</div>
}
script hideNotificationDialog(id string) {
document.getElementById(id).classList.add("hidden");
document.getElementById(id).classList.remove("flex");
}
script showNotificationDialog(id string) {
document.getElementById(id).classList.remove("hidden");
document.getElementById(id).classList.add("flex");
}
script triggerServiceDelete(dialogId string, serviceID uint, serviceName string) {
// Hide the dialog
document.getElementById(dialogId).classList.add("hidden");
@@ -143,9 +136,7 @@ templ Notifications(ctx context.Context, data SettingsNotificationsData) {
}
// Track all HTMX events for debugging
document.addEventListener('htmx:beforeRequest', function(event) {
console.log("HTMX before request:", event.detail);
document.addEventListener('htmx:beforeRequest', function(event) {
// Check if this is a DELETE request for a notification service
const path = event.detail.path;
const method = event.detail.verb;
@@ -161,9 +152,7 @@ templ Notifications(ctx context.Context, data SettingsNotificationsData) {
}
});
document.addEventListener('htmx:afterRequest', function(event) {
console.log("HTMX after request:", event.detail);
document.addEventListener('htmx:afterRequest', function(event) {
// Check for notification service deletion multiple ways
const isDeleteRequest =
// Check global flag from the triggerServiceDelete function
@@ -369,7 +358,7 @@ templ Notifications(ctx context.Context, data SettingsNotificationsData) {
)
<button
type="button"
onclick={ showNotificationDialog(fmt.Sprintf("delete-notification-dialog-%d", service.ID)) }
onclick={ showModal(fmt.Sprintf("delete-notification-dialog-%d", service.ID)) }
class="text-red-500 bg-white focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 rounded-lg text-sm p-2 dark:bg-gray-800 dark:text-red-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
>
<i class="fas fa-trash-alt"></i>
+7 -5
View File
@@ -8,7 +8,7 @@ templ FTPDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_host" name="dest_host" x-model="destinationHost" x-bind:required="requiresDestination"
<input type="text" id="dest_host" name="dest_host" x-model="destHost" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="ftp.example.com" />
</div>
@@ -21,9 +21,11 @@ templ FTPDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-network-wired text-gray-400 dark:text-gray-500"></i>
</div>
<input type="number" id="dest_port" name="dest_port" x-model="destinationPort"
<input type="number" id="dest_port" name="dest_port" x-model="destPort"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="21" min="1" max="65535" />
placeholder="21" min="1" max="65535"
x-init="if (!destPort || destPort === 0) destPort = 21"
x-effect="if (destinationType === 'ftp' && (destPort === 0 || destPort === 22)) destPort = 21" />
</div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">FTP port number (default: 21)</p>
</div>
@@ -34,7 +36,7 @@ templ FTPDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" x-bind:required="requiresDestination"
<input type="text" id="dest_user" name="dest_user" x-model="destUser" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="username" />
</div>
@@ -47,7 +49,7 @@ templ FTPDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_password" name="dest_password" x-model="destinationPassword" x-bind:required="requiresDestination"
<input type="password" id="dest_password" name="dest_password" x-model="destPassword" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Enter password" />
</div>
@@ -23,7 +23,7 @@ templ GoogleDriveDestinationForm() {
</p>
</div>
<div x-show="!useBuiltinAuthDestination">
<div x-show="!useBuiltinAuthDest">
<div class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300" role="alert">
<div class="flex">
<i class="fas fa-exclamation-triangle mr-2 flex-shrink-0"></i>
@@ -37,9 +37,9 @@ templ GoogleDriveDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-id-card text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_client_id" name="dest_client_id" x-model="destinationClientId"
<input type="text" id="dest_client_id" name="dest_client_id" x-model="destClientId"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Your Google API Client ID" x-bind:required="!useBuiltinAuthDestination" />
placeholder="Your Google API Client ID" x-bind:required="!useBuiltinAuthDest" />
</div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">OAuth 2.0 Client ID from Google Cloud Console</p>
</div>
@@ -50,9 +50,9 @@ templ GoogleDriveDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_client_secret" name="dest_client_secret" x-model="destinationClientSecret"
<input type="password" id="dest_client_secret" name="dest_client_secret" x-model="destClientSecret"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Your Google API Client Secret" x-bind:required="!useBuiltinAuthDestination" />
placeholder="Your Google API Client Secret" x-bind:required="!useBuiltinAuthDest" />
</div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">OAuth 2.0 Client Secret from Google Cloud Console</p>
</div>
@@ -79,7 +79,7 @@ templ GoogleDriveDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-fingerprint text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_drive_id" name="dest_drive_id" x-model="destinationDriveId"
<input type="text" id="dest_drive_id" name="dest_drive_id" x-model="destDriveId"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Specific Google Drive ID" />
</div>
@@ -94,7 +94,7 @@ templ GoogleDriveDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-users text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_team_drive" name="dest_team_drive" x-model="destinationTeamDrive"
<input type="text" id="dest_team_drive" name="dest_team_drive" x-model="destTeamDrive"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Team Drive ID" />
</div>
@@ -1,10 +1,10 @@
package destination
templ GooglePhotosDestinationForm() {
<div class="space-y-6 mt-4" x-init="$watch('useBuiltinAuthDestination', value => {
<div class="space-y-6 mt-4" x-init="$watch('useBuiltinAuthDest', value => {
if(value) {
destinationClientId = '';
destinationClientSecret = '';
destClientId = '';
destClientSecret = '';
}
})">
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-blue-900/30 dark:text-blue-300" role="alert">
@@ -38,7 +38,7 @@ templ GooglePhotosDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-id-card text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_client_id" name="dest_client_id" x-model="destinationClientId"
<input type="text" id="dest_client_id" name="dest_client_id" x-model="destClientId"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
x-bind:disabled="useBuiltinAuthDest"
placeholder="Google Photos OAuth Client ID" />
@@ -57,7 +57,7 @@ templ GooglePhotosDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_client_secret" name="dest_client_secret" x-model="destinationClientSecret"
<input type="password" id="dest_client_secret" name="dest_client_secret" x-model="destClientSecret"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
x-bind:disabled="useBuiltinAuthDest"
placeholder="Google Photos OAuth Client Secret" />
+4 -4
View File
@@ -15,7 +15,7 @@ templ MinIODestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destinationEndpoint" x-bind:required="requiresDestination"
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destEndpoint" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="https://minio.example.com" />
</div>
@@ -30,7 +30,7 @@ templ MinIODestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-archive text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_bucket" name="dest_bucket" x-model="destinationBucket" x-bind:required="requiresDestination"
<input type="text" id="dest_bucket" name="dest_bucket" x-model="destBucket" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="my-bucket" />
</div>
@@ -45,7 +45,7 @@ templ MinIODestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_access_key" name="dest_access_key" x-model="destinationAccessKey" x-bind:required="requiresDestination"
<input type="text" id="dest_access_key" name="dest_access_key" x-model="destAccessKey" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="minioadmin" />
</div>
@@ -60,7 +60,7 @@ templ MinIODestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_secret_key" name="dest_secret_key" x-model="destinationSecretKey" x-bind:required="requiresDestination"
<input type="password" id="dest_secret_key" name="dest_secret_key" x-model="destSecretKey" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="minioadmin" />
</div>
@@ -15,7 +15,7 @@ templ NextCloudDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-cloud text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destinationEndpoint" x-bind:required="requiresDestination"
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destEndpoint" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="https://nextcloud.example.com" />
</div>
@@ -30,7 +30,7 @@ templ NextCloudDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" x-bind:required="requiresDestination"
<input type="text" id="dest_user" name="dest_user" x-model="destUser" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="nextcloud_username" />
</div>
@@ -45,7 +45,7 @@ templ NextCloudDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_password" name="dest_password" x-model="destinationPassword" x-bind:required="requiresDestination"
<input type="password" id="dest_password" name="dest_password" x-model="destPassword" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="NextCloud password" />
</div>
+4 -4
View File
@@ -15,7 +15,7 @@ templ S3DestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-globe text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_region" name="dest_region" x-model="destinationRegion" x-bind:required="requiresDestination"
<input type="text" id="dest_region" name="dest_region" x-model="destRegion" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="us-east-1" />
</div>
@@ -30,7 +30,7 @@ templ S3DestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-archive text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_bucket" name="dest_bucket" x-model="destinationBucket" x-bind:required="requiresDestination"
<input type="text" id="dest_bucket" name="dest_bucket" x-model="destBucket" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="my-bucket" />
</div>
@@ -45,7 +45,7 @@ templ S3DestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_access_key" name="dest_access_key" x-model="destinationAccessKey" x-bind:required="requiresDestination"
<input type="text" id="dest_access_key" name="dest_access_key" x-model="destAccessKey" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="AKIAIOSFODNN7EXAMPLE" />
</div>
@@ -60,7 +60,7 @@ templ S3DestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_secret_key" name="dest_secret_key" x-model="destinationSecretKey" x-bind:required="requiresDestination"
<input type="password" id="dest_secret_key" name="dest_secret_key" x-model="destSecretKey" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" />
</div>
+6 -6
View File
@@ -8,7 +8,7 @@ templ SFTPDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_host" name="dest_host" x-model="destinationHost" x-bind:required="requiresDestination"
<input type="text" id="dest_host" name="dest_host" x-model="destHost" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="sftp.example.com" />
</div>
@@ -21,9 +21,9 @@ templ SFTPDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-network-wired text-gray-400 dark:text-gray-500"></i>
</div>
<input type="number" id="dest_port" name="dest_port" x-model="destinationPort"
<input type="number" id="dest_port" name="dest_port" x-model="destPort"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="22" min="1" max="65535" />
placeholder="22" min="1" max="65535" x-init="if (!destPort || destPort === 0) destPort = 22" />
</div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">SFTP port number (default: 22)</p>
</div>
@@ -34,7 +34,7 @@ templ SFTPDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" x-bind:required="requiresDestination"
<input type="text" id="dest_user" name="dest_user" x-model="destUser" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="username" />
</div>
@@ -61,7 +61,7 @@ templ SFTPDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_password" name="dest_password" x-model="destinationPassword"
<input type="password" id="dest_password" name="dest_password" x-model="destPassword"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Enter password" x-bind:required="destAuthType === 'password'" />
</div>
@@ -74,7 +74,7 @@ templ SFTPDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_key_file" name="dest_key_file" x-model="destinationKeyFile"
<input type="text" id="dest_key_file" name="dest_key_file" x-model="destKeyFile"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="/path/to/id_rsa" x-bind:required="destAuthType === 'key'" />
</div>
+5 -5
View File
@@ -15,7 +15,7 @@ templ SMBDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_host" name="dest_host" x-model="destinationHost" x-bind:required="requiresDestination"
<input type="text" id="dest_host" name="dest_host" x-model="destHost" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="192.168.1.100 or server.example.com" />
</div>
@@ -30,7 +30,7 @@ templ SMBDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-share-alt text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_share" name="dest_share" x-model="destinationShare" x-bind:required="requiresDestination"
<input type="text" id="dest_share" name="dest_share" x-model="destShare" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Documents" />
</div>
@@ -45,7 +45,7 @@ templ SMBDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-sitemap text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_domain" name="dest_domain" x-model="destinationDomain"
<input type="text" id="dest_domain" name="dest_domain" x-model="destDomain"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="WORKGROUP or DOMAIN" />
</div>
@@ -60,7 +60,7 @@ templ SMBDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" x-bind:required="requiresDestination"
<input type="text" id="dest_user" name="dest_user" x-model="destUser" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="smb_username" />
</div>
@@ -75,7 +75,7 @@ templ SMBDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_password" name="dest_password" x-model="destinationPassword" x-bind:required="requiresDestination"
<input type="password" id="dest_password" name="dest_password" x-model="destPassword" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="SMB password" />
</div>
@@ -15,7 +15,7 @@ templ WebDAVDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-globe text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destinationEndpoint" x-bind:required="requiresDestination"
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destEndpoint" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="https://webdav.example.com" />
</div>
@@ -30,7 +30,7 @@ templ WebDAVDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" x-bind:required="requiresDestination"
<input type="text" id="dest_user" name="dest_user" x-model="destUser" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="webdav_username" />
</div>
@@ -45,7 +45,7 @@ templ WebDAVDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_password" name="dest_password" x-model="destinationPassword" x-bind:required="requiresDestination"
<input type="password" id="dest_password" name="dest_password" x-model="destPassword" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="WebDAV password" />
</div>
+1 -1
View File
@@ -23,7 +23,7 @@ templ FTPSourceForm() {
</div>
<input type="number" id="source_port" name="source_port" x-model="sourcePort"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="21" min="1" max="65535" />
placeholder="21" min="1" max="65535" value="21"/>
</div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">FTP port number (default: 21)</p>
</div>
+7 -16
View File
@@ -117,9 +117,7 @@ templ UserManagementContent(data UserManagementData) {
});
// Track user operations in HTMX events
document.addEventListener('htmx:beforeRequest', function(event) {
console.log("HTMX before request:", event.detail);
document.addEventListener('htmx:beforeRequest', function(event) {
// Check if this is a DELETE request for a user
const path = event.detail.path;
const method = event.detail.verb;
@@ -421,7 +419,7 @@ templ userList(users []db.User) {
class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800"
data-user-email={ user.Email }
data-user-id={ strconv.Itoa(int(user.ID)) }
onclick={ showUserDialog(fmt.Sprintf("delete-user-dialog-%d", user.ID)) }
onclick={ showModal(fmt.Sprintf("delete-user-dialog-%d", user.ID)) }
>
<i class="fas fa-trash-alt w-3.5 h-3.5 mr-1.5"></i> Delete
</button>
@@ -634,7 +632,10 @@ func hasRole(user *db.User, roleID uint) bool {
// UserDialog for confirmation actions
templ UserDialog(id string, title string, message string, confirmClass string, confirmText string, action string, userID uint, userEmail string) {
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm">
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<!-- Backdrop -->
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
<!-- Modal content -->
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
<div class="p-6 text-center">
@@ -654,7 +655,7 @@ templ UserDialog(id string, title string, message string, confirmClass string, c
onclick={ triggerUserDelete(id, userID, userEmail) }>
{ confirmText }
</button>
<button type="button" data-modal-hide={ id } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
<button type="button" onclick={ closeModal(id) } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
Cancel
</button>
</div>
@@ -663,16 +664,6 @@ templ UserDialog(id string, title string, message string, confirmClass string, c
</div>
}
script hideUserDialog(id string) {
document.getElementById(id).classList.add("hidden");
document.getElementById(id).classList.remove("flex");
}
script showUserDialog(id string) {
document.getElementById(id).classList.remove("hidden");
document.getElementById(id).classList.add("flex");
}
script triggerUserDelete(dialogId string, userID uint, userEmail string) {
// Hide the dialog
document.getElementById(dialogId).classList.add("hidden");
+9 -4
View File
@@ -637,6 +637,7 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
"host", config.SourceHost,
"user", config.SourceUser,
"pass", config.SourcePassword,
"port", fmt.Sprintf("%d", config.SourcePort),
"--non-interactive",
"--config", configPath,
"--log-level", "ERROR",
@@ -653,7 +654,7 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
case "webdav":
args := []string{
"config", "create", sourceName, "webdav",
"url", config.SourceHost,
"url", config.SourceEndpoint,
"user", config.SourceUser,
"pass", config.SourcePassword,
"--non-interactive",
@@ -668,7 +669,7 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
case "nextcloud":
args := []string{
"config", "create", sourceName, "webdav",
"url", config.SourceHost,
"url", config.SourceEndpoint,
"user", config.SourceUser,
"pass", config.SourcePassword,
"vendor", "nextcloud",
@@ -760,6 +761,9 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
}
destName := fmt.Sprintf("dest_%d", config.ID)
fmt.Println("config", config)
switch config.DestinationType {
case "sftp":
args := []string{
@@ -857,6 +861,7 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
args := []string{
"config", "create", destName, "ftp",
"host", config.DestHost,
"port", fmt.Sprintf("%d", config.DestPort),
"user", config.DestUser,
"pass", config.DestPassword,
"--non-interactive",
@@ -875,7 +880,7 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
case "webdav":
args := []string{
"config", "create", destName, "webdav",
"url", config.DestHost,
"url", config.DestEndpoint,
"user", config.DestUser,
"pass", config.DestPassword,
"--non-interactive",
@@ -890,7 +895,7 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
case "nextcloud":
args := []string{
"config", "create", destName, "webdav",
"url", config.DestHost,
"url", config.DestEndpoint,
"user", config.DestUser,
"pass", config.DestPassword,
"vendor", "nextcloud",
@@ -1169,6 +1169,43 @@ func (h *Handlers) HandleNewNotificationPage(c *gin.Context) {
data := components.NotificationFormData{
IsNew: true,
NotificationService: &struct {
ID uint
Name string
Description string
Type string
IsEnabled bool
EventTriggers []string
RetryPolicy string
WebhookURL string
Method string
Headers string
PayloadTemplate string
SecretKey string
PushbulletAPIKey string
PushbulletDeviceID string
PushbulletTitleTemplate string
PushbulletBodyTemplate string
NtfyServer string
NtfyTopic string
NtfyPriority string
NtfyUsername string
NtfyPassword string
NtfyTitleTemplate string
NtfyMessageTemplate string
GotifyURL string
GotifyToken string
GotifyPriority string
GotifyTitleTemplate string
GotifyMessageTemplate string
PushoverAPIToken string
PushoverUserKey string
PushoverDevice string
PushoverPriority string
PushoverSound string
PushoverTitleTemplate string
PushoverMessageTemplate string
}{},
}
ctx := h.CreateTemplateContext(c)
+49 -2
View File
@@ -8,6 +8,7 @@ import (
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
@@ -23,6 +24,40 @@ import (
//go:embed static
var staticFiles embed.FS
// setCacheHeaders sets appropriate cache headers based on file type
func setCacheHeaders(c *gin.Context, path string) {
// Set cache headers based on file type
if strings.HasSuffix(path, ".js") || strings.HasSuffix(path, ".css") {
c.Header("Cache-Control", "public, max-age=31536000") // Cache for 1 year
} else if strings.HasSuffix(path, ".png") || strings.HasSuffix(path, ".jpg") || strings.HasSuffix(path, ".webp") {
c.Header("Cache-Control", "public, max-age=86400") // Cache for 1 day
} else {
c.Header("Cache-Control", "no-cache")
}
}
// setContentType sets the correct content type based on file extension
func setContentType(c *gin.Context, path string) {
switch {
case strings.HasSuffix(path, ".js"):
c.Header("Content-Type", "application/javascript")
case strings.HasSuffix(path, ".css"):
c.Header("Content-Type", "text/css")
case strings.HasSuffix(path, ".png"):
c.Header("Content-Type", "image/png")
case strings.HasSuffix(path, ".jpg"), strings.HasSuffix(path, ".jpeg"):
c.Header("Content-Type", "image/jpeg")
case strings.HasSuffix(path, ".webp"):
c.Header("Content-Type", "image/webp")
case strings.HasSuffix(path, ".svg"):
c.Header("Content-Type", "image/svg+xml")
case strings.HasSuffix(path, ".woff2"):
c.Header("Content-Type", "font/woff2")
case strings.HasSuffix(path, ".woff"):
c.Header("Content-Type", "font/woff")
}
}
func main() {
// Set Gin to release mode
gin.SetMode(gin.ReleaseMode)
@@ -110,12 +145,24 @@ func main() {
}))
router.Use(gin.Recovery())
// Serve embedded static files
// Serve embedded static files with proper content types and caching
staticFS, err := fs.Sub(staticFiles, "static")
if err != nil {
log.Fatalf("Failed to create sub-filesystem: %v", err)
}
router.StaticFS("/static", http.FS(staticFS))
// Custom static file handler
router.GET("/static/*filepath", func(c *gin.Context) {
path := c.Param("filepath")
// Set content type and cache headers
setContentType(c, path)
setCacheHeaders(c, path)
// Serve the file
c.FileFromFS(path, http.FS(staticFS))
})
log.Printf("Embedded static files configured for serving")
// Initialize web handlers
+1872
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
{
"name": "gomft",
"version": "1.0.0",
"description": "GoMFT static assets bundling",
"type": "module",
"scripts": {
"build": "node build.js",
"watch": "node build.js --watch",
"postinstall": "npm run build"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.4.0",
"alpinejs": "^3.13.5",
"esbuild": "^0.20.1",
"flowbite": "^2.2.1",
"htmx.org": "^1.9.10",
"tailwindcss": "^3.4.1"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 754 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 391 KiB

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 KiB

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 KiB

After

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 825 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 629 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 602 KiB

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 257 KiB

+51
View File
@@ -1,3 +1,54 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Custom styles */
html, body {
margin: 0;
padding: 0;
overflow-x: hidden;
width: 100%;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.animate-fadeIn {
animation: fadeIn 0.3s ease-in-out;
}
.pb-mobile-nav {
padding-bottom: 4rem;
}
@media (min-width: 640px) {
.pb-mobile-nav {
padding-bottom: 0;
}
}
/* Dark mode styles */
body.dark {
background-color: #111827; /* gray-900 */
}
body.dark .configs-page,
body.dark .jobs-page {
background-color: #111827 !important; /* gray-900 */
min-height: 100vh;
}
#jobs-container.dark, #configs-container.dark {
background-color: #111827 !important;
min-height: 100vh;
}
body.dark .bg-white {
background-color: #1f2937 !important;
}
/* Base styles */
:root {
--primary-color: #2563eb;
+32
View File
@@ -0,0 +1,32 @@
// Add script to ensure dark mode is properly applied
document.addEventListener('DOMContentLoaded', function() {
// Apply body dark class when theme changes
const isDark = document.documentElement.classList.contains('dark');
if (isDark) {
document.body.classList.add('dark');
// Also apply to containers
const jobsContainer = document.getElementById('jobs-container');
const configsContainer = document.getElementById('configs-container');
if (jobsContainer) jobsContainer.classList.add('dark');
if (configsContainer) configsContainer.classList.add('dark');
}
// Initialize admin dropdown toggle if available
const adminDropdownToggle = document.querySelector('[data-collapse-toggle="dropdown-settings"]');
const adminDropdown = document.getElementById('dropdown-settings');
if (adminDropdownToggle && adminDropdown) {
// Check if we should show the dropdown (if current page is under admin section)
const currentPath = window.location.pathname;
if (currentPath.startsWith('/admin')) {
adminDropdown.classList.remove('hidden');
}
// Add click event listener
adminDropdownToggle.addEventListener('click', function() {
adminDropdown.classList.toggle('hidden');
});
}
});
+53
View File
@@ -0,0 +1,53 @@
// Import HTMX
import 'htmx.org';
// Import Alpine.js
import Alpine from 'alpinejs';
window.Alpine = Alpine;
// Import Flowbite
import 'flowbite';
import 'flowbite/dist/flowbite.css';
// Initialize Flowbite components
document.addEventListener('DOMContentLoaded', () => {
// Initialize Alpine.js
Alpine.start();
// Initialize Flowbite modals
const modalTriggers = document.querySelectorAll('[data-modal-target]');
modalTriggers.forEach(trigger => {
const targetId = trigger.getAttribute('data-modal-target');
const targetModal = document.getElementById(targetId);
if (targetModal) {
// Show modal
trigger.addEventListener('click', () => {
targetModal.classList.remove('hidden');
targetModal.classList.add('flex');
// Add backdrop
document.body.style.overflow = 'hidden';
});
// Handle modal hide buttons
const hideButtons = targetModal.querySelectorAll('[data-modal-hide]');
hideButtons.forEach(button => {
button.addEventListener('click', () => {
targetModal.classList.add('hidden');
targetModal.classList.remove('flex');
// Remove backdrop
document.body.style.overflow = '';
});
});
// Handle clicking outside the modal
targetModal.addEventListener('click', (event) => {
if (event.target === targetModal || event.target.classList.contains('fixed')) {
targetModal.classList.add('hidden');
targetModal.classList.remove('flex');
// Remove backdrop
document.body.style.overflow = '';
}
});
}
});
});
+33
View File
@@ -0,0 +1,33 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./components/**/*.{html,js,templ,go}",
"./node_modules/flowbite/**/*.js"
],
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
950: '#082f49',
}
},
fontFamily: {
sans: ['Inter var', 'ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'sans-serif'],
}
}
},
plugins: [
require('flowbite/plugin')
],
}